5 use BookStack\Auth\Access\Ldap\LdapConnection;
6 use BookStack\Auth\Access\Ldap\LdapConnectionManager;
7 use BookStack\Auth\Access\Ldap\LdapService;
8 use BookStack\Auth\Role;
9 use BookStack\Auth\User;
10 use Illuminate\Testing\TestResponse;
11 use Mockery\MockInterface;
14 class LdapTest extends TestCase
22 protected $resourceId = 'resource-test';
24 protected function setUp(): void
28 if (!defined('LDAP_OPT_REFERRALS')) {
29 define('LDAP_OPT_REFERRALS', 1);
33 'auth.method' => 'ldap',
34 'auth.defaults.guard' => 'ldap',
35 'services.ldap.server' => 'ldap.example.com',
36 'services.ldap.base_dn' => 'dc=ldap,dc=local',
37 'services.ldap.email_attribute' => 'mail',
38 'services.ldap.display_name_attribute' => 'cn',
39 'services.ldap.id_attribute' => 'uid',
40 'services.ldap.user_to_groups' => false,
41 'services.ldap.version' => '3',
42 'services.ldap.user_filter' => '(&(uid=${user}))',
43 'services.ldap.follow_referrals' => false,
44 'services.ldap.tls_insecure' => false,
45 'services.ldap.thumbnail_attribute' => null,
48 $lcm = $this->mock(LdapConnectionManager::class);
49 // TODO - Properly mock
51 $this->mockLdap = \Mockery::mock(LdapConnection::class);
52 $this->app[LdapConnection::class] = $this->mockLdap;
53 $this->mockUser = User::factory()->make();
56 protected function runFailedAuthLogin()
58 $this->commonLdapMocks(1, 1, 1, 1, 1);
59 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
60 ->andReturn(['count' => 0]);
61 $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);
64 protected function mockEscapes($times = 1)
66 $this->mockLdap->shouldReceive('escape')->times($times)->andReturnUsing(function ($val) {
67 return ldap_escape($val);
71 protected function mockExplodes($times = 1)
73 $this->mockLdap->shouldReceive('explodeDn')->times($times)->andReturnUsing(function ($dn, $withAttrib) {
74 return ldap_explode_dn($dn, $withAttrib);
78 protected function mockUserLogin(?string $email = null): TestResponse
80 return $this->post('/login', [
81 'username' => $this->mockUser->name,
82 'password' => $this->mockUser->password,
83 ] + ($email ? ['email' => $email] : []));
87 * Set LDAP method mocks for things we commonly call without altering.
89 protected function commonLdapMocks(int $connects = 1, int $versions = 1, int $options = 2, int $binds = 4, int $escapes = 2, int $explodes = 0)
91 $this->mockLdap->shouldReceive('setVersion')->times($versions);
92 $this->mockLdap->shouldReceive('setOption')->times($options);
93 $this->mockLdap->shouldReceive('bind')->times($binds)->andReturn(true);
94 $this->mockEscapes($escapes);
95 $this->mockExplodes($explodes);
98 public function test_login()
100 $this->commonLdapMocks(1, 1, 2, 4, 2);
101 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
102 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
103 ->andReturn(['count' => 1, 0 => [
104 'uid' => [$this->mockUser->name],
105 'cn' => [$this->mockUser->name],
106 'dn' => 'dc=test' . config('services.ldap.base_dn'),
109 $resp = $this->mockUserLogin();
110 $resp->assertRedirect('/login');
111 $resp = $this->followRedirects($resp);
112 $resp->assertSee('Please enter an email to use for this account.');
113 $resp->assertSee($this->mockUser->name);
115 $resp = $this->followingRedirects()->mockUserLogin($this->mockUser->email);
116 $this->withHtml($resp)->assertElementExists('#home-default');
117 $resp->assertSee($this->mockUser->name);
118 $this->assertDatabaseHas('users', [
119 'email' => $this->mockUser->email,
120 'email_confirmed' => false,
121 'external_auth_id' => $this->mockUser->name,
125 public function test_email_domain_restriction_active_on_new_ldap_login()
128 'registration-restrict' => 'testing.com',
131 $this->commonLdapMocks(1, 1, 2, 4, 2);
132 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
133 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
134 ->andReturn(['count' => 1, 0 => [
135 'uid' => [$this->mockUser->name],
136 'cn' => [$this->mockUser->name],
137 'dn' => 'dc=test' . config('services.ldap.base_dn'),
140 $resp = $this->mockUserLogin();
141 $resp->assertRedirect('/login');
142 $this->followRedirects($resp)->assertSee('Please enter an email to use for this account.');
145 $resp = $this->mockUserLogin($email);
146 $resp->assertRedirect('/login');
147 $this->followRedirects($resp)->assertSee('That email domain does not have access to this application');
149 $this->assertDatabaseMissing('users', ['email' => $email]);
152 public function test_login_works_when_no_uid_provided_by_ldap_server()
154 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
156 $this->commonLdapMocks(1, 1, 1, 2, 1);
157 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
158 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
159 ->andReturn(['count' => 1, 0 => [
160 'cn' => [$this->mockUser->name],
162 'mail' => [$this->mockUser->email],
165 $resp = $this->mockUserLogin();
166 $resp->assertRedirect('/');
167 $this->followRedirects($resp)->assertSee($this->mockUser->name);
168 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
171 public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()
173 config()->set(['services.ldap.id_attribute' => 'my_custom_id']);
175 $this->commonLdapMocks(1, 1, 1, 2, 1);
176 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
177 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
178 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
179 ->andReturn(['count' => 1, 0 => [
180 'cn' => [$this->mockUser->name],
182 'my_custom_id' => ['cooluser456'],
183 'mail' => [$this->mockUser->email],
186 $resp = $this->mockUserLogin();
187 $resp->assertRedirect('/');
188 $this->followRedirects($resp)->assertSee($this->mockUser->name);
189 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);
192 public function test_initial_incorrect_credentials()
194 $this->commonLdapMocks(1, 1, 1, 0, 1);
195 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
196 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
197 ->andReturn(['count' => 1, 0 => [
198 'uid' => [$this->mockUser->name],
199 'cn' => [$this->mockUser->name],
200 'dn' => 'dc=test' . config('services.ldap.base_dn'),
202 $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false);
204 $resp = $this->mockUserLogin();
205 $resp->assertRedirect('/login');
206 $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
207 $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
210 public function test_login_not_found_username()
212 $this->commonLdapMocks(1, 1, 1, 1, 1);
213 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
214 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
215 ->andReturn(['count' => 0]);
217 $resp = $this->mockUserLogin();
218 $resp->assertRedirect('/login');
219 $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
220 $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
223 public function test_create_user_form()
225 $userForm = $this->asAdmin()->get('/settings/users/create');
226 $userForm->assertDontSee('Password');
228 $save = $this->post('/settings/users/create', [
229 'name' => $this->mockUser->name,
230 'email' => $this->mockUser->email,
232 $save->assertSessionHasErrors(['external_auth_id' => 'The external auth id field is required.']);
234 $save = $this->post('/settings/users/create', [
235 'name' => $this->mockUser->name,
236 'email' => $this->mockUser->email,
237 'external_auth_id' => $this->mockUser->name,
239 $save->assertRedirect('/settings/users');
240 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
243 public function test_user_edit_form()
245 $editUser = $this->getNormalUser();
246 $editPage = $this->asAdmin()->get("/settings/users/{$editUser->id}");
247 $editPage->assertSee('Edit User');
248 $editPage->assertDontSee('Password');
250 $update = $this->put("/settings/users/{$editUser->id}", [
251 'name' => $editUser->name,
252 'email' => $editUser->email,
253 'external_auth_id' => 'test_auth_id',
255 $update->assertRedirect('/settings/users');
256 $this->assertDatabaseHas('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
259 public function test_registration_disabled()
261 $resp = $this->followingRedirects()->get('/register');
262 $this->withHtml($resp)->assertElementContains('#content', 'Log In');
265 public function test_non_admins_cannot_change_auth_id()
267 $testUser = $this->getNormalUser();
268 $this->actingAs($testUser)
269 ->get('/settings/users/' . $testUser->id)
270 ->assertDontSee('External Authentication');
273 public function test_login_maps_roles_and_retains_existing_roles()
275 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
276 $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);
277 $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);
278 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
279 $this->mockUser->attachRole($existingRole);
282 'services.ldap.user_to_groups' => true,
283 'services.ldap.group_attribute' => 'memberOf',
284 'services.ldap.remove_from_groups' => false,
287 $this->commonLdapMocks(1, 1, 4, 5, 4, 6);
288 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
289 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
290 ->andReturn(['count' => 1, 0 => [
291 'uid' => [$this->mockUser->name],
292 'cn' => [$this->mockUser->name],
293 'dn' => 'dc=test' . config('services.ldap.base_dn'),
294 'mail' => [$this->mockUser->email],
297 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
298 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
302 $this->mockUserLogin()->assertRedirect('/');
304 $user = User::where('email', $this->mockUser->email)->first();
305 $this->assertDatabaseHas('role_user', [
306 'user_id' => $user->id,
307 'role_id' => $roleToReceive->id,
309 $this->assertDatabaseHas('role_user', [
310 'user_id' => $user->id,
311 'role_id' => $roleToReceive2->id,
313 $this->assertDatabaseHas('role_user', [
314 'user_id' => $user->id,
315 'role_id' => $existingRole->id,
319 public function test_login_maps_roles_and_removes_old_roles_if_set()
321 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
322 $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);
323 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
324 $this->mockUser->attachRole($existingRole);
327 'services.ldap.user_to_groups' => true,
328 'services.ldap.group_attribute' => 'memberOf',
329 'services.ldap.remove_from_groups' => true,
332 $this->commonLdapMocks(1, 1, 3, 4, 3, 2);
333 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
334 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
335 ->andReturn(['count' => 1, 0 => [
336 'uid' => [$this->mockUser->name],
337 'cn' => [$this->mockUser->name],
338 'dn' => 'dc=test' . config('services.ldap.base_dn'),
339 'mail' => [$this->mockUser->email],
342 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
346 $this->mockUserLogin()->assertRedirect('/');
348 $user = User::query()->where('email', $this->mockUser->email)->first();
349 $this->assertDatabaseHas('role_user', [
350 'user_id' => $user->id,
351 'role_id' => $roleToReceive->id,
353 $this->assertDatabaseMissing('role_user', [
354 'user_id' => $user->id,
355 'role_id' => $existingRole->id,
359 public function test_dump_user_groups_shows_group_related_details_as_json()
362 'services.ldap.user_to_groups' => true,
363 'services.ldap.group_attribute' => 'memberOf',
364 'services.ldap.remove_from_groups' => true,
365 'services.ldap.dump_user_groups' => true,
368 $userResp = ['count' => 1, 0 => [
369 'uid' => [$this->mockUser->name],
370 'cn' => [$this->mockUser->name],
371 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
372 'mail' => [$this->mockUser->email],
374 $this->commonLdapMocks(1, 1, 4, 5, 4, 2);
375 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
376 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
377 ->andReturn($userResp, ['count' => 1,
379 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
382 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
388 'dn' => 'cn=ldaptester,ou=groups,dc=example,dc=com',
391 0 => 'cn=monsters,ou=groups,dc=example,dc=com',
396 $resp = $this->mockUserLogin();
398 'details_from_ldap' => [
399 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
401 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
405 'parsed_direct_user_groups' => [
408 'parsed_recursive_user_groups' => [
415 public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
417 $role = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
418 $this->asAdmin()->get('/settings/roles/' . $role->id)
419 ->assertSee('ex-auth-a');
422 public function test_login_maps_roles_using_external_auth_ids_if_set()
424 $roleToReceive = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
425 $roleToNotReceive = Role::factory()->create(['display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
428 'services.ldap.user_to_groups' => true,
429 'services.ldap.group_attribute' => 'memberOf',
430 'services.ldap.remove_from_groups' => true,
433 $this->commonLdapMocks(1, 1, 3, 4, 3, 2);
434 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
435 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
436 ->andReturn(['count' => 1, 0 => [
437 'uid' => [$this->mockUser->name],
438 'cn' => [$this->mockUser->name],
439 'dn' => 'dc=test' . config('services.ldap.base_dn'),
440 'mail' => [$this->mockUser->email],
443 0 => 'cn=ex-auth-a,ou=groups,dc=example,dc=com',
447 $this->mockUserLogin()->assertRedirect('/');
449 $user = User::query()->where('email', $this->mockUser->email)->first();
450 $this->assertDatabaseHas('role_user', [
451 'user_id' => $user->id,
452 'role_id' => $roleToReceive->id,
454 $this->assertDatabaseMissing('role_user', [
455 'user_id' => $user->id,
456 'role_id' => $roleToNotReceive->id,
460 public function test_login_group_mapping_does_not_conflict_with_default_role()
462 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
463 $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);
464 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
466 setting()->put('registration-role', $roleToReceive->id);
469 'services.ldap.user_to_groups' => true,
470 'services.ldap.group_attribute' => 'memberOf',
471 'services.ldap.remove_from_groups' => true,
474 $this->commonLdapMocks(1, 1, 4, 5, 4, 6);
475 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
476 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
477 ->andReturn(['count' => 1, 0 => [
478 'uid' => [$this->mockUser->name],
479 'cn' => [$this->mockUser->name],
480 'dn' => 'dc=test' . config('services.ldap.base_dn'),
481 'mail' => [$this->mockUser->email],
484 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
485 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
489 $this->mockUserLogin()->assertRedirect('/');
491 $user = User::query()->where('email', $this->mockUser->email)->first();
492 $this->assertDatabaseHas('role_user', [
493 'user_id' => $user->id,
494 'role_id' => $roleToReceive->id,
496 $this->assertDatabaseHas('role_user', [
497 'user_id' => $user->id,
498 'role_id' => $roleToReceive2->id,
502 public function test_login_uses_specified_display_name_attribute()
505 'services.ldap.display_name_attribute' => 'displayName',
508 $this->commonLdapMocks(1, 1, 2, 4, 2);
509 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
510 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
511 ->andReturn(['count' => 1, 0 => [
512 'uid' => [$this->mockUser->name],
513 'cn' => [$this->mockUser->name],
514 'dn' => 'dc=test' . config('services.ldap.base_dn'),
515 'displayname' => 'displayNameAttribute',
518 $this->mockUserLogin()->assertRedirect('/login');
519 $this->get('/login')->assertSee('Please enter an email to use for this account.');
521 $resp = $this->mockUserLogin($this->mockUser->email);
522 $resp->assertRedirect('/');
523 $this->get('/')->assertSee('displayNameAttribute');
524 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
527 public function test_login_uses_default_display_name_attribute_if_specified_not_present()
530 'services.ldap.display_name_attribute' => 'displayName',
533 $this->commonLdapMocks(1, 1, 2, 4, 2);
534 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
535 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
536 ->andReturn(['count' => 1, 0 => [
537 'uid' => [$this->mockUser->name],
538 'cn' => [$this->mockUser->name],
539 'dn' => 'dc=test' . config('services.ldap.base_dn'),
542 $this->mockUserLogin()->assertRedirect('/login');
543 $this->get('/login')->assertSee('Please enter an email to use for this account.');
545 $resp = $this->mockUserLogin($this->mockUser->email);
546 $resp->assertRedirect('/');
547 $this->get('/')->assertSee($this->mockUser->name);
548 $this->assertDatabaseHas('users', [
549 'email' => $this->mockUser->email,
550 'email_confirmed' => false,
551 'external_auth_id' => $this->mockUser->name,
552 'name' => $this->mockUser->name,
556 protected function checkLdapReceivesCorrectDetails($serverString, $expectedHost, $expectedPort)
559 'services.ldap.server' => $serverString,
563 $this->commonLdapMocks(0, 1, 1, 2, 1);
564 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)->andReturn(['count' => 1, 0 => [
565 'uid' => [$this->mockUser->name],
566 'cn' => [$this->mockUser->name],
567 'dn' => 'dc=test' . config('services.ldap.base_dn'),
570 $this->mockLdap->shouldReceive('connect')->once()
571 ->with($expectedHost, $expectedPort)->andReturn($this->resourceId);
572 $this->mockUserLogin();
575 public function test_ldap_port_provided_on_host_if_host_is_full_uri()
577 $hostName = 'ldaps://bookstack:8080';
578 $this->checkLdapReceivesCorrectDetails($hostName, $hostName, 389);
581 public function test_ldap_port_parsed_from_server_if_host_is_not_full_uri()
583 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com:8080', 'ldap.bookstack.com', 8080);
586 public function test_default_ldap_port_used_if_not_in_server_string_and_not_uri()
588 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com', 'ldap.bookstack.com', 389);
591 public function test_host_fail_over_by_using_semicolon_seperated_hosts()
594 'services.ldap.server' => 'ldap-tiger.example.com;ldap-donkey.example.com:8080',
598 $this->commonLdapMocks(0, 1, 1, 2, 1);
599 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)->andReturn(['count' => 1, 0 => [
600 'uid' => [$this->mockUser->name],
601 'cn' => [$this->mockUser->name],
602 'dn' => 'dc=test' . config('services.ldap.base_dn'),
605 $this->mockLdap->shouldReceive('connect')->once()->with('ldap-tiger.example.com', 389)->andReturn(false);
606 $this->mockLdap->shouldReceive('connect')->once()->with('ldap-donkey.example.com', 8080)->andReturn($this->resourceId);
607 $this->mockUserLogin();
610 public function test_host_fail_over_by_using_semicolon_seperated_hosts_still_throws_error()
613 'services.ldap.server' => 'ldap-tiger.example.com;ldap-donkey.example.com:8080',
616 $this->mockLdap->shouldReceive('connect')->once()->with('ldap-tiger.example.com', 389)->andReturn(false);
617 $this->mockLdap->shouldReceive('connect')->once()->with('ldap-donkey.example.com', 8080)->andReturn(false);
619 $resp = $this->mockUserLogin();
620 $resp->assertStatus(500);
621 $resp->assertSee('Cannot connect to ldap server, Initial connection failed');
624 public function test_forgot_password_routes_inaccessible()
626 $resp = $this->get('/password/email');
627 $this->assertPermissionError($resp);
629 $resp = $this->post('/password/email');
630 $this->assertPermissionError($resp);
632 $resp = $this->get('/password/reset/abc123');
633 $this->assertPermissionError($resp);
635 $resp = $this->post('/password/reset');
636 $this->assertPermissionError($resp);
639 public function test_user_invite_routes_inaccessible()
641 $resp = $this->get('/register/invite/abc123');
642 $this->assertPermissionError($resp);
644 $resp = $this->post('/register/invite/abc123');
645 $this->assertPermissionError($resp);
648 public function test_user_register_routes_inaccessible()
650 $resp = $this->get('/register');
651 $this->assertPermissionError($resp);
653 $resp = $this->post('/register');
654 $this->assertPermissionError($resp);
657 public function test_dump_user_details_option_works()
659 config()->set(['services.ldap.dump_user_details' => true, 'services.ldap.thumbnail_attribute' => 'jpegphoto']);
661 $this->commonLdapMocks(1, 1, 1, 1, 1);
662 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
663 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
664 ->andReturn(['count' => 1, 0 => [
665 'uid' => [$this->mockUser->name],
666 'cn' => [$this->mockUser->name],
667 // Test dumping binary data for avatar responses
668 'jpegphoto' => base64_decode('/9j/4AAQSkZJRg=='),
669 'dn' => 'dc=test' . config('services.ldap.base_dn'),
672 $resp = $this->post('/login', [
673 'username' => $this->mockUser->name,
674 'password' => $this->mockUser->password,
676 $resp->assertJsonStructure([
677 'details_from_ldap' => [],
678 'details_bookstack_parsed' => [],
682 public function test_start_tls_called_if_option_set()
684 config()->set(['services.ldap.start_tls' => true]);
685 $this->mockLdap->shouldReceive('startTls')->once()->andReturn(true);
686 $this->runFailedAuthLogin();
689 public function test_connection_fails_if_tls_fails()
691 config()->set(['services.ldap.start_tls' => true]);
692 $this->mockLdap->shouldReceive('startTls')->once()->andReturn(false);
693 $this->commonLdapMocks(1, 1, 0, 0, 0);
694 $resp = $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);
695 $resp->assertStatus(500);
698 public function test_ldap_attributes_can_be_binary_decoded_if_marked()
700 config()->set(['services.ldap.id_attribute' => 'BIN;uid']);
701 $ldapService = app()->make(LdapService::class);
702 $this->commonLdapMocks(1, 1, 1, 1, 1);
703 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
704 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), ['cn', 'dn', 'uid', 'mail', 'cn'])
705 ->andReturn(['count' => 1, 0 => [
706 'uid' => [hex2bin('FFF8F7')],
707 'cn' => [$this->mockUser->name],
708 'dn' => 'dc=test' . config('services.ldap.base_dn'),
711 $details = $ldapService->getUserDetails('test');
712 $this->assertEquals('fff8f7', $details['uid']);
715 public function test_new_ldap_user_login_with_already_used_email_address_shows_error_message_to_user()
717 $this->commonLdapMocks(1, 1, 2, 4, 2);
718 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
719 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
720 ->andReturn(['count' => 1, 0 => [
721 'uid' => [$this->mockUser->name],
722 'cn' => [$this->mockUser->name],
723 'dn' => 'dc=test' . config('services.ldap.base_dn'),
725 ]], ['count' => 1, 0 => [
728 'dn' => 'dc=bscott' . config('services.ldap.base_dn'),
733 $this->mockUserLogin()->assertRedirect('/');
737 $resp = $this->followingRedirects()->post('/login', ['username' => 'bscott', 'password' => 'pass']);
738 $resp->assertSee('A user with the email
[email protected] already exists but with different credentials');
741 public function test_login_with_email_confirmation_required_maps_groups_but_shows_confirmation_screen()
743 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
744 $user = User::factory()->make();
745 setting()->put('registration-confirmation', 'true');
748 'services.ldap.user_to_groups' => true,
749 'services.ldap.group_attribute' => 'memberOf',
750 'services.ldap.remove_from_groups' => true,
753 $this->commonLdapMocks(1, 1, 6, 8, 6, 4);
754 $this->mockLdap->shouldReceive('searchAndGetEntries')
756 ->andReturn(['count' => 1, 0 => [
757 'uid' => [$user->name],
758 'cn' => [$user->name],
759 'dn' => 'dc=test' . config('services.ldap.base_dn'),
760 'mail' => [$user->email],
763 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
767 $login = $this->followingRedirects()->mockUserLogin();
768 $login->assertSee('Thanks for registering!');
769 $this->assertDatabaseHas('users', [
770 'email' => $user->email,
771 'email_confirmed' => false,
774 $user = User::query()->where('email', '=', $user->email)->first();
775 $this->assertDatabaseHas('role_user', [
776 'user_id' => $user->id,
777 'role_id' => $roleToReceive->id,
780 $this->assertNull(auth()->user());
782 $homePage = $this->get('/');
783 $homePage->assertRedirect('/login');
785 $login = $this->followingRedirects()->mockUserLogin();
786 $login->assertSee('Email Address Not Confirmed');
789 public function test_failed_logins_are_logged_when_message_configured()
791 $log = $this->withTestLogger();
792 config()->set(['logging.failed_login.message' => 'Failed login for %u']);
793 $this->runFailedAuthLogin();
794 $this->assertTrue($log->hasWarningThatContains('Failed login for timmyjenkins'));
797 public function test_thumbnail_attribute_used_as_user_avatar_if_configured()
799 config()->set(['services.ldap.thumbnail_attribute' => 'jpegPhoto']);
801 $this->commonLdapMocks(1, 1, 1, 2, 1);
802 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
803 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
804 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
805 ->andReturn(['count' => 1, 0 => [
806 'cn' => [$this->mockUser->name],
808 'jpegphoto' => [base64_decode('/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8Q
809 EBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=')],
810 'mail' => [$this->mockUser->email],
813 $this->mockUserLogin()
814 ->assertRedirect('/');
816 $user = User::query()->where('email', '=', $this->mockUser->email)->first();
817 $this->assertNotNull($user->avatar);
818 $this->assertEquals('8c90748342f19b195b9c6b4eff742ded', md5_file(public_path($user->avatar->path)));