5 use BookStack\Auth\Access\Ldap;
6 use BookStack\Auth\Access\LdapService;
7 use BookStack\Auth\Role;
8 use BookStack\Auth\User;
9 use Illuminate\Testing\TestResponse;
10 use Mockery\MockInterface;
13 class LdapTest extends TestCase
21 protected $resourceId = 'resource-test';
23 protected function setUp(): void
26 if (!defined('LDAP_OPT_REFERRALS')) {
27 define('LDAP_OPT_REFERRALS', 1);
30 'auth.method' => 'ldap',
31 'auth.defaults.guard' => 'ldap',
32 'services.ldap.server' => 'ldap.example.com',
33 'services.ldap.base_dn' => 'dc=ldap,dc=local',
34 'services.ldap.email_attribute' => 'mail',
35 'services.ldap.display_name_attribute' => 'cn',
36 'services.ldap.id_attribute' => 'uid',
37 'services.ldap.user_to_groups' => false,
38 'services.ldap.version' => '3',
39 'services.ldap.user_filter' => '(&(uid=${user}))',
40 'services.ldap.follow_referrals' => false,
41 'services.ldap.tls_insecure' => false,
42 'services.ldap.thumbnail_attribute' => null,
44 $this->mockLdap = \Mockery::mock(Ldap::class);
45 $this->app[Ldap::class] = $this->mockLdap;
46 $this->mockUser = User::factory()->make();
49 protected function runFailedAuthLogin()
51 $this->commonLdapMocks(1, 1, 1, 1, 1);
52 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
53 ->andReturn(['count' => 0]);
54 $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);
57 protected function mockEscapes($times = 1)
59 $this->mockLdap->shouldReceive('escape')->times($times)->andReturnUsing(function ($val) {
60 return ldap_escape($val);
64 protected function mockExplodes($times = 1)
66 $this->mockLdap->shouldReceive('explodeDn')->times($times)->andReturnUsing(function ($dn, $withAttrib) {
67 return ldap_explode_dn($dn, $withAttrib);
71 protected function mockUserLogin(?string $email = null): TestResponse
73 return $this->post('/login', [
74 'username' => $this->mockUser->name,
75 'password' => $this->mockUser->password,
76 ] + ($email ? ['email' => $email] : []));
80 * Set LDAP method mocks for things we commonly call without altering.
82 protected function commonLdapMocks(int $connects = 1, int $versions = 1, int $options = 2, int $binds = 4, int $escapes = 2, int $explodes = 0)
84 $this->mockLdap->shouldReceive('connect')->times($connects)->andReturn($this->resourceId);
85 $this->mockLdap->shouldReceive('setVersion')->times($versions);
86 $this->mockLdap->shouldReceive('setOption')->times($options);
87 $this->mockLdap->shouldReceive('bind')->times($binds)->andReturn(true);
88 $this->mockEscapes($escapes);
89 $this->mockExplodes($explodes);
92 public function test_login()
94 $this->commonLdapMocks(1, 1, 2, 4, 2);
95 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
96 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
97 ->andReturn(['count' => 1, 0 => [
98 'uid' => [$this->mockUser->name],
99 'cn' => [$this->mockUser->name],
100 'dn' => 'dc=test' . config('services.ldap.base_dn'),
103 $resp = $this->mockUserLogin();
104 $resp->assertRedirect('/login');
105 $resp = $this->followRedirects($resp);
106 $resp->assertSee('Please enter an email to use for this account.');
107 $resp->assertSee($this->mockUser->name);
109 $resp = $this->followingRedirects()->mockUserLogin($this->mockUser->email);
110 $this->withHtml($resp)->assertElementExists('#home-default');
111 $resp->assertSee($this->mockUser->name);
112 $this->assertDatabaseHas('users', [
113 'email' => $this->mockUser->email,
114 'email_confirmed' => false,
115 'external_auth_id' => $this->mockUser->name,
119 public function test_email_domain_restriction_active_on_new_ldap_login()
122 'registration-restrict' => 'testing.com',
125 $this->commonLdapMocks(1, 1, 2, 4, 2);
126 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
127 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
128 ->andReturn(['count' => 1, 0 => [
129 'uid' => [$this->mockUser->name],
130 'cn' => [$this->mockUser->name],
131 'dn' => 'dc=test' . config('services.ldap.base_dn'),
134 $resp = $this->mockUserLogin();
135 $resp->assertRedirect('/login');
136 $this->followRedirects($resp)->assertSee('Please enter an email to use for this account.');
139 $resp = $this->mockUserLogin($email);
140 $resp->assertRedirect('/login');
141 $this->followRedirects($resp)->assertSee('That email domain does not have access to this application');
143 $this->assertDatabaseMissing('users', ['email' => $email]);
146 public function test_login_works_when_no_uid_provided_by_ldap_server()
148 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
150 $this->commonLdapMocks(1, 1, 1, 2, 1);
151 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
152 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
153 ->andReturn(['count' => 1, 0 => [
154 'cn' => [$this->mockUser->name],
156 'mail' => [$this->mockUser->email],
159 $resp = $this->mockUserLogin();
160 $resp->assertRedirect('/');
161 $this->followRedirects($resp)->assertSee($this->mockUser->name);
162 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
165 public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()
167 config()->set(['services.ldap.id_attribute' => 'my_custom_id']);
169 $this->commonLdapMocks(1, 1, 1, 2, 1);
170 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
171 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
172 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
173 ->andReturn(['count' => 1, 0 => [
174 'cn' => [$this->mockUser->name],
176 'my_custom_id' => ['cooluser456'],
177 'mail' => [$this->mockUser->email],
180 $resp = $this->mockUserLogin();
181 $resp->assertRedirect('/');
182 $this->followRedirects($resp)->assertSee($this->mockUser->name);
183 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);
186 public function test_initial_incorrect_credentials()
188 $this->commonLdapMocks(1, 1, 1, 0, 1);
189 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
190 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
191 ->andReturn(['count' => 1, 0 => [
192 'uid' => [$this->mockUser->name],
193 'cn' => [$this->mockUser->name],
194 'dn' => 'dc=test' . config('services.ldap.base_dn'),
196 $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false);
198 $resp = $this->mockUserLogin();
199 $resp->assertRedirect('/login');
200 $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
201 $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
204 public function test_login_not_found_username()
206 $this->commonLdapMocks(1, 1, 1, 1, 1);
207 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
208 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
209 ->andReturn(['count' => 0]);
211 $resp = $this->mockUserLogin();
212 $resp->assertRedirect('/login');
213 $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
214 $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
217 public function test_create_user_form()
219 $userForm = $this->asAdmin()->get('/settings/users/create');
220 $userForm->assertDontSee('Password');
222 $save = $this->post('/settings/users/create', [
223 'name' => $this->mockUser->name,
224 'email' => $this->mockUser->email,
226 $save->assertSessionHasErrors(['external_auth_id' => 'The external auth id field is required.']);
228 $save = $this->post('/settings/users/create', [
229 'name' => $this->mockUser->name,
230 'email' => $this->mockUser->email,
231 'external_auth_id' => $this->mockUser->name,
233 $save->assertRedirect('/settings/users');
234 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
237 public function test_user_edit_form()
239 $editUser = $this->getNormalUser();
240 $editPage = $this->asAdmin()->get("/settings/users/{$editUser->id}");
241 $editPage->assertSee('Edit User');
242 $editPage->assertDontSee('Password');
244 $update = $this->put("/settings/users/{$editUser->id}", [
245 'name' => $editUser->name,
246 'email' => $editUser->email,
247 'external_auth_id' => 'test_auth_id',
249 $update->assertRedirect('/settings/users');
250 $this->assertDatabaseHas('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
253 public function test_registration_disabled()
255 $resp = $this->followingRedirects()->get('/register');
256 $this->withHtml($resp)->assertElementContains('#content', 'Log In');
259 public function test_non_admins_cannot_change_auth_id()
261 $testUser = $this->getNormalUser();
262 $this->actingAs($testUser)
263 ->get('/settings/users/' . $testUser->id)
264 ->assertDontSee('External Authentication');
267 public function test_login_maps_roles_and_retains_existing_roles()
269 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
270 $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);
271 $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);
272 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
273 $this->mockUser->attachRole($existingRole);
276 'services.ldap.user_to_groups' => true,
277 'services.ldap.group_attribute' => 'memberOf',
278 'services.ldap.remove_from_groups' => false,
281 $this->commonLdapMocks(1, 1, 4, 5, 4, 6);
282 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
283 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
284 ->andReturn(['count' => 1, 0 => [
285 'uid' => [$this->mockUser->name],
286 'cn' => [$this->mockUser->name],
287 'dn' => 'dc=test' . config('services.ldap.base_dn'),
288 'mail' => [$this->mockUser->email],
291 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
292 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
296 $this->mockUserLogin()->assertRedirect('/');
298 $user = User::where('email', $this->mockUser->email)->first();
299 $this->assertDatabaseHas('role_user', [
300 'user_id' => $user->id,
301 'role_id' => $roleToReceive->id,
303 $this->assertDatabaseHas('role_user', [
304 'user_id' => $user->id,
305 'role_id' => $roleToReceive2->id,
307 $this->assertDatabaseHas('role_user', [
308 'user_id' => $user->id,
309 'role_id' => $existingRole->id,
313 public function test_login_maps_roles_and_removes_old_roles_if_set()
315 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
316 $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);
317 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
318 $this->mockUser->attachRole($existingRole);
321 'services.ldap.user_to_groups' => true,
322 'services.ldap.group_attribute' => 'memberOf',
323 'services.ldap.remove_from_groups' => true,
326 $this->commonLdapMocks(1, 1, 3, 4, 3, 2);
327 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
328 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
329 ->andReturn(['count' => 1, 0 => [
330 'uid' => [$this->mockUser->name],
331 'cn' => [$this->mockUser->name],
332 'dn' => 'dc=test' . config('services.ldap.base_dn'),
333 'mail' => [$this->mockUser->email],
336 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
340 $this->mockUserLogin()->assertRedirect('/');
342 $user = User::query()->where('email', $this->mockUser->email)->first();
343 $this->assertDatabaseHas('role_user', [
344 'user_id' => $user->id,
345 'role_id' => $roleToReceive->id,
347 $this->assertDatabaseMissing('role_user', [
348 'user_id' => $user->id,
349 'role_id' => $existingRole->id,
353 public function test_dump_user_groups_shows_group_related_details_as_json()
356 'services.ldap.user_to_groups' => true,
357 'services.ldap.group_attribute' => 'memberOf',
358 'services.ldap.remove_from_groups' => true,
359 'services.ldap.dump_user_groups' => true,
362 $userResp = ['count' => 1, 0 => [
363 'uid' => [$this->mockUser->name],
364 'cn' => [$this->mockUser->name],
365 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
366 'mail' => [$this->mockUser->email],
368 $this->commonLdapMocks(1, 1, 4, 5, 4, 2);
369 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
370 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
371 ->andReturn($userResp, ['count' => 1,
373 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
376 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
382 'dn' => 'cn=ldaptester,ou=groups,dc=example,dc=com',
385 0 => 'cn=monsters,ou=groups,dc=example,dc=com',
390 $resp = $this->mockUserLogin();
392 'details_from_ldap' => [
393 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
395 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
399 'parsed_direct_user_groups' => [
402 'parsed_recursive_user_groups' => [
409 public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
411 $role = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
412 $this->asAdmin()->get('/settings/roles/' . $role->id)
413 ->assertSee('ex-auth-a');
416 public function test_login_maps_roles_using_external_auth_ids_if_set()
418 $roleToReceive = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
419 $roleToNotReceive = Role::factory()->create(['display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
422 'services.ldap.user_to_groups' => true,
423 'services.ldap.group_attribute' => 'memberOf',
424 'services.ldap.remove_from_groups' => true,
427 $this->commonLdapMocks(1, 1, 3, 4, 3, 2);
428 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
429 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
430 ->andReturn(['count' => 1, 0 => [
431 'uid' => [$this->mockUser->name],
432 'cn' => [$this->mockUser->name],
433 'dn' => 'dc=test' . config('services.ldap.base_dn'),
434 'mail' => [$this->mockUser->email],
437 0 => 'cn=ex-auth-a,ou=groups,dc=example,dc=com',
441 $this->mockUserLogin()->assertRedirect('/');
443 $user = User::query()->where('email', $this->mockUser->email)->first();
444 $this->assertDatabaseHas('role_user', [
445 'user_id' => $user->id,
446 'role_id' => $roleToReceive->id,
448 $this->assertDatabaseMissing('role_user', [
449 'user_id' => $user->id,
450 'role_id' => $roleToNotReceive->id,
454 public function test_login_group_mapping_does_not_conflict_with_default_role()
456 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
457 $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);
458 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
460 setting()->put('registration-role', $roleToReceive->id);
463 'services.ldap.user_to_groups' => true,
464 'services.ldap.group_attribute' => 'memberOf',
465 'services.ldap.remove_from_groups' => true,
468 $this->commonLdapMocks(1, 1, 4, 5, 4, 6);
469 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
470 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
471 ->andReturn(['count' => 1, 0 => [
472 'uid' => [$this->mockUser->name],
473 'cn' => [$this->mockUser->name],
474 'dn' => 'dc=test' . config('services.ldap.base_dn'),
475 'mail' => [$this->mockUser->email],
478 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
479 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
483 $this->mockUserLogin()->assertRedirect('/');
485 $user = User::query()->where('email', $this->mockUser->email)->first();
486 $this->assertDatabaseHas('role_user', [
487 'user_id' => $user->id,
488 'role_id' => $roleToReceive->id,
490 $this->assertDatabaseHas('role_user', [
491 'user_id' => $user->id,
492 'role_id' => $roleToReceive2->id,
496 public function test_login_uses_specified_display_name_attribute()
499 'services.ldap.display_name_attribute' => 'displayName',
502 $this->commonLdapMocks(1, 1, 2, 4, 2);
503 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
504 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
505 ->andReturn(['count' => 1, 0 => [
506 'uid' => [$this->mockUser->name],
507 'cn' => [$this->mockUser->name],
508 'dn' => 'dc=test' . config('services.ldap.base_dn'),
509 'displayname' => 'displayNameAttribute',
512 $this->mockUserLogin()->assertRedirect('/login');
513 $this->get('/login')->assertSee('Please enter an email to use for this account.');
515 $resp = $this->mockUserLogin($this->mockUser->email);
516 $resp->assertRedirect('/');
517 $this->get('/')->assertSee('displayNameAttribute');
518 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
521 public function test_login_uses_default_display_name_attribute_if_specified_not_present()
524 'services.ldap.display_name_attribute' => 'displayName',
527 $this->commonLdapMocks(1, 1, 2, 4, 2);
528 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
529 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
530 ->andReturn(['count' => 1, 0 => [
531 'uid' => [$this->mockUser->name],
532 'cn' => [$this->mockUser->name],
533 'dn' => 'dc=test' . config('services.ldap.base_dn'),
536 $this->mockUserLogin()->assertRedirect('/login');
537 $this->get('/login')->assertSee('Please enter an email to use for this account.');
539 $resp = $this->mockUserLogin($this->mockUser->email);
540 $resp->assertRedirect('/');
541 $this->get('/')->assertSee($this->mockUser->name);
542 $this->assertDatabaseHas('users', [
543 'email' => $this->mockUser->email,
544 'email_confirmed' => false,
545 'external_auth_id' => $this->mockUser->name,
546 'name' => $this->mockUser->name,
550 protected function checkLdapReceivesCorrectDetails($serverString, $expectedHost, $expectedPort)
553 'services.ldap.server' => $serverString,
557 $this->commonLdapMocks(0, 1, 1, 2, 1);
558 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)->andReturn(['count' => 1, 0 => [
559 'uid' => [$this->mockUser->name],
560 'cn' => [$this->mockUser->name],
561 'dn' => 'dc=test' . config('services.ldap.base_dn'),
564 $this->mockLdap->shouldReceive('connect')->once()
565 ->with($expectedHost, $expectedPort)->andReturn($this->resourceId);
566 $this->mockUserLogin();
569 public function test_ldap_port_provided_on_host_if_host_is_full_uri()
571 $hostName = 'ldaps://bookstack:8080';
572 $this->checkLdapReceivesCorrectDetails($hostName, $hostName, 389);
575 public function test_ldap_port_parsed_from_server_if_host_is_not_full_uri()
577 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com:8080', 'ldap.bookstack.com', 8080);
580 public function test_default_ldap_port_used_if_not_in_server_string_and_not_uri()
582 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com', 'ldap.bookstack.com', 389);
585 public function test_host_fail_over_by_using_semicolon_seperated_hosts()
588 'services.ldap.server' => 'ldap-tiger.example.com;ldap-donkey.example.com:8080',
592 $this->commonLdapMocks(0, 1, 1, 2, 1);
593 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)->andReturn(['count' => 1, 0 => [
594 'uid' => [$this->mockUser->name],
595 'cn' => [$this->mockUser->name],
596 'dn' => 'dc=test' . config('services.ldap.base_dn'),
599 $this->mockLdap->shouldReceive('connect')->once()->with('ldap-tiger.example.com', 389)->andReturn(false);
600 $this->mockLdap->shouldReceive('connect')->once()->with('ldap-donkey.example.com', 8080)->andReturn($this->resourceId);
601 $this->mockUserLogin();
604 public function test_host_fail_over_by_using_semicolon_seperated_hosts_still_throws_error()
607 'services.ldap.server' => 'ldap-tiger.example.com;ldap-donkey.example.com:8080',
610 $this->mockLdap->shouldReceive('connect')->once()->with('ldap-tiger.example.com', 389)->andReturn(false);
611 $this->mockLdap->shouldReceive('connect')->once()->with('ldap-donkey.example.com', 8080)->andReturn(false);
613 $resp = $this->mockUserLogin();
614 $resp->assertStatus(500);
615 $resp->assertSee('Cannot connect to ldap server, Initial connection failed');
618 public function test_forgot_password_routes_inaccessible()
620 $resp = $this->get('/password/email');
621 $this->assertPermissionError($resp);
623 $resp = $this->post('/password/email');
624 $this->assertPermissionError($resp);
626 $resp = $this->get('/password/reset/abc123');
627 $this->assertPermissionError($resp);
629 $resp = $this->post('/password/reset');
630 $this->assertPermissionError($resp);
633 public function test_user_invite_routes_inaccessible()
635 $resp = $this->get('/register/invite/abc123');
636 $this->assertPermissionError($resp);
638 $resp = $this->post('/register/invite/abc123');
639 $this->assertPermissionError($resp);
642 public function test_user_register_routes_inaccessible()
644 $resp = $this->get('/register');
645 $this->assertPermissionError($resp);
647 $resp = $this->post('/register');
648 $this->assertPermissionError($resp);
651 public function test_dump_user_details_option_works()
653 config()->set(['services.ldap.dump_user_details' => true, 'services.ldap.thumbnail_attribute' => 'jpegphoto']);
655 $this->commonLdapMocks(1, 1, 1, 1, 1);
656 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
657 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
658 ->andReturn(['count' => 1, 0 => [
659 'uid' => [$this->mockUser->name],
660 'cn' => [$this->mockUser->name],
661 // Test dumping binary data for avatar responses
662 'jpegphoto' => base64_decode('/9j/4AAQSkZJRg=='),
663 'dn' => 'dc=test' . config('services.ldap.base_dn'),
666 $resp = $this->post('/login', [
667 'username' => $this->mockUser->name,
668 'password' => $this->mockUser->password,
670 $resp->assertJsonStructure([
671 'details_from_ldap' => [],
672 'details_bookstack_parsed' => [],
676 public function test_start_tls_called_if_option_set()
678 config()->set(['services.ldap.start_tls' => true]);
679 $this->mockLdap->shouldReceive('startTls')->once()->andReturn(true);
680 $this->runFailedAuthLogin();
683 public function test_connection_fails_if_tls_fails()
685 config()->set(['services.ldap.start_tls' => true]);
686 $this->mockLdap->shouldReceive('startTls')->once()->andReturn(false);
687 $this->commonLdapMocks(1, 1, 0, 0, 0);
688 $resp = $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);
689 $resp->assertStatus(500);
692 public function test_ldap_attributes_can_be_binary_decoded_if_marked()
694 config()->set(['services.ldap.id_attribute' => 'BIN;uid']);
695 $ldapService = app()->make(LdapService::class);
696 $this->commonLdapMocks(1, 1, 1, 1, 1);
697 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
698 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), ['cn', 'dn', 'uid', 'mail', 'cn'])
699 ->andReturn(['count' => 1, 0 => [
700 'uid' => [hex2bin('FFF8F7')],
701 'cn' => [$this->mockUser->name],
702 'dn' => 'dc=test' . config('services.ldap.base_dn'),
705 $details = $ldapService->getUserDetails('test');
706 $this->assertEquals('fff8f7', $details['uid']);
709 public function test_new_ldap_user_login_with_already_used_email_address_shows_error_message_to_user()
711 $this->commonLdapMocks(1, 1, 2, 4, 2);
712 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
713 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
714 ->andReturn(['count' => 1, 0 => [
715 'uid' => [$this->mockUser->name],
716 'cn' => [$this->mockUser->name],
717 'dn' => 'dc=test' . config('services.ldap.base_dn'),
719 ]], ['count' => 1, 0 => [
722 'dn' => 'dc=bscott' . config('services.ldap.base_dn'),
727 $this->mockUserLogin()->assertRedirect('/');
731 $resp = $this->followingRedirects()->post('/login', ['username' => 'bscott', 'password' => 'pass']);
732 $resp->assertSee('A user with the email
[email protected] already exists but with different credentials');
735 public function test_login_with_email_confirmation_required_maps_groups_but_shows_confirmation_screen()
737 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
738 $user = User::factory()->make();
739 setting()->put('registration-confirmation', 'true');
742 'services.ldap.user_to_groups' => true,
743 'services.ldap.group_attribute' => 'memberOf',
744 'services.ldap.remove_from_groups' => true,
747 $this->commonLdapMocks(1, 1, 6, 8, 6, 4);
748 $this->mockLdap->shouldReceive('searchAndGetEntries')
750 ->andReturn(['count' => 1, 0 => [
751 'uid' => [$user->name],
752 'cn' => [$user->name],
753 'dn' => 'dc=test' . config('services.ldap.base_dn'),
754 'mail' => [$user->email],
757 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
761 $login = $this->followingRedirects()->mockUserLogin();
762 $login->assertSee('Thanks for registering!');
763 $this->assertDatabaseHas('users', [
764 'email' => $user->email,
765 'email_confirmed' => false,
768 $user = User::query()->where('email', '=', $user->email)->first();
769 $this->assertDatabaseHas('role_user', [
770 'user_id' => $user->id,
771 'role_id' => $roleToReceive->id,
774 $this->assertNull(auth()->user());
776 $homePage = $this->get('/');
777 $homePage->assertRedirect('/login');
779 $login = $this->followingRedirects()->mockUserLogin();
780 $login->assertSee('Email Address Not Confirmed');
783 public function test_failed_logins_are_logged_when_message_configured()
785 $log = $this->withTestLogger();
786 config()->set(['logging.failed_login.message' => 'Failed login for %u']);
787 $this->runFailedAuthLogin();
788 $this->assertTrue($log->hasWarningThatContains('Failed login for timmyjenkins'));
791 public function test_thumbnail_attribute_used_as_user_avatar_if_configured()
793 config()->set(['services.ldap.thumbnail_attribute' => 'jpegPhoto']);
795 $this->commonLdapMocks(1, 1, 1, 2, 1);
796 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
797 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
798 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
799 ->andReturn(['count' => 1, 0 => [
800 'cn' => [$this->mockUser->name],
802 'jpegphoto' => [base64_decode('/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8Q
803 EBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=')],
804 'mail' => [$this->mockUser->email],
807 $this->mockUserLogin()
808 ->assertRedirect('/');
810 $user = User::query()->where('email', '=', $this->mockUser->email)->first();
811 $this->assertNotNull($user->avatar);
812 $this->assertEquals('8c90748342f19b195b9c6b4eff742ded', md5_file(public_path($user->avatar->path)));