5 use BookStack\Access\Ldap;
6 use BookStack\Access\LdapService;
7 use BookStack\Exceptions\LdapException;
8 use BookStack\Users\Models\Role;
9 use BookStack\Users\Models\User;
10 use Illuminate\Testing\TestResponse;
11 use Mockery\MockInterface;
14 class LdapTest extends TestCase
16 protected MockInterface $mockLdap;
18 protected User $mockUser;
19 protected string $resourceId = 'resource-test';
21 protected function setUp(): void
24 if (!defined('LDAP_OPT_REFERRALS')) {
25 define('LDAP_OPT_REFERRALS', 1);
28 'auth.method' => 'ldap',
29 'auth.defaults.guard' => 'ldap',
30 'services.ldap.base_dn' => 'dc=ldap,dc=local',
31 'services.ldap.email_attribute' => 'mail',
32 'services.ldap.display_name_attribute' => ['cn'],
33 'services.ldap.id_attribute' => 'uid',
34 'services.ldap.user_to_groups' => false,
35 'services.ldap.version' => '3',
36 'services.ldap.user_filter' => '(&(uid={user}))',
37 'services.ldap.follow_referrals' => false,
38 'services.ldap.tls_insecure' => false,
39 'services.ldap.tls_ca_cert' => false,
40 'services.ldap.thumbnail_attribute' => null,
42 $this->mockLdap = $this->mock(Ldap::class);
43 $this->mockUser = User::factory()->make();
46 protected function runFailedAuthLogin()
48 $this->commonLdapMocks(1, 1, 1, 1, 1);
49 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
50 ->andReturn(['count' => 0]);
51 $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);
54 protected function mockEscapes($times = 1)
56 $this->mockLdap->shouldReceive('escape')->times($times)->andReturnUsing(function ($val) {
57 return ldap_escape($val);
61 protected function mockExplodes($times = 1)
63 $this->mockLdap->shouldReceive('explodeDn')->times($times)->andReturnUsing(function ($dn, $withAttrib) {
64 return ldap_explode_dn($dn, $withAttrib);
68 protected function mockUserLogin(?string $email = null): TestResponse
70 return $this->post('/login', [
71 'username' => $this->mockUser->name,
72 'password' => $this->mockUser->password,
73 ] + ($email ? ['email' => $email] : []));
77 * Set LDAP method mocks for things we commonly call without altering.
79 protected function commonLdapMocks(int $connects = 1, int $versions = 1, int $options = 2, int $binds = 4, int $escapes = 2, int $explodes = 0, int $groups = 0)
81 $this->mockLdap->shouldReceive('connect')->times($connects)->andReturn($this->resourceId);
82 $this->mockLdap->shouldReceive('setVersion')->times($versions);
83 $this->mockLdap->shouldReceive('setOption')->times($options);
84 $this->mockLdap->shouldReceive('bind')->times($binds)->andReturn(true);
85 $this->mockEscapes($escapes);
86 $this->mockExplodes($explodes);
87 $this->mockGroupLookups($groups);
90 protected function mockGroupLookups(int $times = 1): void
92 $this->mockLdap->shouldReceive('read')->times($times)->andReturn(['count' => 0]);
93 $this->mockLdap->shouldReceive('getEntries')->times($times)->andReturn(['count' => 0]);
96 public function test_login()
98 $this->commonLdapMocks(1, 1, 2, 4, 2);
99 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
100 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
101 ->andReturn(['count' => 1, 0 => [
102 'uid' => [$this->mockUser->name],
103 'cn' => [$this->mockUser->name],
104 'dn' => 'dc=test' . config('services.ldap.base_dn'),
107 $resp = $this->mockUserLogin();
108 $resp->assertRedirect('/login');
109 $resp = $this->followRedirects($resp);
110 $resp->assertSee('Please enter an email to use for this account.');
111 $resp->assertSee($this->mockUser->name);
113 $resp = $this->followingRedirects()->mockUserLogin($this->mockUser->email);
114 $this->withHtml($resp)->assertElementExists('#home-default');
115 $resp->assertSee($this->mockUser->name);
116 $this->assertDatabaseHas('users', [
117 'email' => $this->mockUser->email,
118 'email_confirmed' => false,
119 'external_auth_id' => $this->mockUser->name,
123 public function test_email_domain_restriction_active_on_new_ldap_login()
126 'registration-restrict' => 'testing.com',
129 $this->commonLdapMocks(1, 1, 2, 4, 2);
130 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
131 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
132 ->andReturn(['count' => 1, 0 => [
133 'uid' => [$this->mockUser->name],
134 'cn' => [$this->mockUser->name],
135 'dn' => 'dc=test' . config('services.ldap.base_dn'),
138 $resp = $this->mockUserLogin();
139 $resp->assertRedirect('/login');
140 $this->followRedirects($resp)->assertSee('Please enter an email to use for this account.');
143 $resp = $this->mockUserLogin($email);
144 $resp->assertRedirect('/login');
145 $this->followRedirects($resp)->assertSee('That email domain does not have access to this application');
147 $this->assertDatabaseMissing('users', ['email' => $email]);
150 public function test_login_works_when_no_uid_provided_by_ldap_server()
152 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
154 $this->commonLdapMocks(1, 1, 1, 2, 1);
155 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
156 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
157 ->andReturn(['count' => 1, 0 => [
158 'cn' => [$this->mockUser->name],
160 'mail' => [$this->mockUser->email],
163 $resp = $this->mockUserLogin();
164 $resp->assertRedirect('/');
165 $this->followRedirects($resp)->assertSee($this->mockUser->name);
166 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
169 public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()
171 config()->set(['services.ldap.id_attribute' => 'my_custom_id']);
173 $this->commonLdapMocks(1, 1, 1, 2, 1);
174 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
175 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
176 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
177 ->andReturn(['count' => 1, 0 => [
178 'cn' => [$this->mockUser->name],
180 'my_custom_id' => ['cooluser456'],
181 'mail' => [$this->mockUser->email],
184 $resp = $this->mockUserLogin();
185 $resp->assertRedirect('/');
186 $this->followRedirects($resp)->assertSee($this->mockUser->name);
187 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);
190 public function test_user_filter_default_placeholder_format()
192 config()->set('services.ldap.user_filter', '(&(uid={user}))');
193 $this->mockUser->name = 'barryldapuser';
194 $expectedFilter = '(&(uid=\62\61\72\72\79\6c\64\61\70\75\73\65\72))';
196 $this->commonLdapMocks(1, 1, 1, 1, 1);
197 $this->mockLdap->shouldReceive('searchAndGetEntries')
199 ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \Mockery::type('array'))
200 ->andReturn(['count' => 0, 0 => []]);
202 $resp = $this->mockUserLogin();
203 $resp->assertRedirect('/login');
206 public function test_user_filter_old_placeholder_format()
208 config()->set('services.ldap.user_filter', '(&(username=${user}))');
209 $this->mockUser->name = 'barryldapuser';
210 $expectedFilter = '(&(username=\62\61\72\72\79\6c\64\61\70\75\73\65\72))';
212 $this->commonLdapMocks(1, 1, 1, 1, 1);
213 $this->mockLdap->shouldReceive('searchAndGetEntries')
215 ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \Mockery::type('array'))
216 ->andReturn(['count' => 0, 0 => []]);
218 $resp = $this->mockUserLogin();
219 $resp->assertRedirect('/login');
222 public function test_initial_incorrect_credentials()
224 $this->commonLdapMocks(1, 1, 1, 0, 1);
225 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
226 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
227 ->andReturn(['count' => 1, 0 => [
228 'uid' => [$this->mockUser->name],
229 'cn' => [$this->mockUser->name],
230 'dn' => 'dc=test' . config('services.ldap.base_dn'),
232 $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false);
234 $resp = $this->mockUserLogin();
235 $resp->assertRedirect('/login');
236 $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
237 $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
240 public function test_login_not_found_username()
242 $this->commonLdapMocks(1, 1, 1, 1, 1);
243 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
244 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
245 ->andReturn(['count' => 0]);
247 $resp = $this->mockUserLogin();
248 $resp->assertRedirect('/login');
249 $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
250 $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
253 public function test_create_user_form()
255 $userForm = $this->asAdmin()->get('/settings/users/create');
256 $userForm->assertDontSee('Password');
258 $save = $this->post('/settings/users/create', [
259 'name' => $this->mockUser->name,
260 'email' => $this->mockUser->email,
262 $save->assertSessionHasErrors(['external_auth_id' => 'The external auth id field is required.']);
264 $save = $this->post('/settings/users/create', [
265 'name' => $this->mockUser->name,
266 'email' => $this->mockUser->email,
267 'external_auth_id' => $this->mockUser->name,
269 $save->assertRedirect('/settings/users');
270 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
273 public function test_user_edit_form()
275 $editUser = $this->users->viewer();
276 $editPage = $this->asAdmin()->get("/settings/users/{$editUser->id}");
277 $editPage->assertSee('Edit User');
278 $editPage->assertDontSee('Password');
280 $update = $this->put("/settings/users/{$editUser->id}", [
281 'name' => $editUser->name,
282 'email' => $editUser->email,
283 'external_auth_id' => 'test_auth_id',
285 $update->assertRedirect('/settings/users');
286 $this->assertDatabaseHas('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
289 public function test_registration_disabled()
291 $resp = $this->followingRedirects()->get('/register');
292 $this->withHtml($resp)->assertElementContains('#content', 'Log In');
295 public function test_non_admins_cannot_change_auth_id()
297 $testUser = $this->users->viewer();
298 $this->actingAs($testUser)
299 ->get('/settings/users/' . $testUser->id)
300 ->assertDontSee('External Authentication');
303 public function test_login_maps_roles_and_retains_existing_roles()
305 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
306 $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);
307 $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);
308 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
309 $this->mockUser->attachRole($existingRole);
312 'services.ldap.user_to_groups' => true,
313 'services.ldap.group_attribute' => 'memberOf',
314 'services.ldap.remove_from_groups' => false,
317 $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 2);
318 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
319 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
320 ->andReturn(['count' => 1, 0 => [
321 'uid' => [$this->mockUser->name],
322 'cn' => [$this->mockUser->name],
323 'dn' => 'dc=test' . config('services.ldap.base_dn'),
324 'mail' => [$this->mockUser->email],
327 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
328 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
332 $this->mockUserLogin()->assertRedirect('/');
334 $user = User::where('email', $this->mockUser->email)->first();
335 $this->assertDatabaseHas('role_user', [
336 'user_id' => $user->id,
337 'role_id' => $roleToReceive->id,
339 $this->assertDatabaseHas('role_user', [
340 'user_id' => $user->id,
341 'role_id' => $roleToReceive2->id,
343 $this->assertDatabaseHas('role_user', [
344 'user_id' => $user->id,
345 'role_id' => $existingRole->id,
349 public function test_login_maps_roles_and_removes_old_roles_if_set()
351 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
352 $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);
353 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
354 $this->mockUser->attachRole($existingRole);
357 'services.ldap.user_to_groups' => true,
358 'services.ldap.group_attribute' => 'memberOf',
359 'services.ldap.remove_from_groups' => true,
362 $this->commonLdapMocks(1, 1, 3, 4, 2, 1, 1);
363 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
364 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
365 ->andReturn(['count' => 1, 0 => [
366 'uid' => [$this->mockUser->name],
367 'cn' => [$this->mockUser->name],
368 'dn' => 'dc=test' . config('services.ldap.base_dn'),
369 'mail' => [$this->mockUser->email],
372 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
376 $this->mockUserLogin()->assertRedirect('/');
378 $user = User::query()->where('email', $this->mockUser->email)->first();
379 $this->assertDatabaseHas('role_user', [
380 'user_id' => $user->id,
381 'role_id' => $roleToReceive->id,
383 $this->assertDatabaseMissing('role_user', [
384 'user_id' => $user->id,
385 'role_id' => $existingRole->id,
389 public function test_dump_user_groups_shows_group_related_details_as_json()
392 'services.ldap.user_to_groups' => true,
393 'services.ldap.group_attribute' => 'memberOf',
394 'services.ldap.remove_from_groups' => true,
395 'services.ldap.dump_user_groups' => true,
398 $userResp = ['count' => 1, 0 => [
399 'uid' => [$this->mockUser->name],
400 'cn' => [$this->mockUser->name],
401 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
402 'mail' => [$this->mockUser->email],
404 $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 0);
405 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
406 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
407 ->andReturn($userResp, ['count' => 1,
409 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
412 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
417 $this->mockLdap->shouldReceive('read')->times(2);
418 $this->mockLdap->shouldReceive('getEntries')->times(2)
422 'dn' => 'cn=ldaptester,ou=groups,dc=example,dc=com',
425 0 => 'cn=monsters,ou=groups,dc=example,dc=com',
430 $resp = $this->mockUserLogin();
432 'details_from_ldap' => [
433 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
435 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
439 'parsed_direct_user_groups' => [
440 'cn=ldaptester,ou=groups,dc=example,dc=com',
442 'parsed_recursive_user_groups' => [
443 'cn=ldaptester,ou=groups,dc=example,dc=com',
444 'cn=monsters,ou=groups,dc=example,dc=com',
446 'parsed_resulting_group_names' => [
453 public function test_recursive_group_search_queries_via_full_dn()
456 'services.ldap.user_to_groups' => true,
457 'services.ldap.group_attribute' => 'memberOf',
460 $userResp = ['count' => 1, 0 => [
461 'uid' => [$this->mockUser->name],
462 'cn' => [$this->mockUser->name],
463 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
464 'mail' => [$this->mockUser->email],
466 $groupResp = ['count' => 1,
468 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
471 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
476 $this->commonLdapMocks(1, 1, 3, 4, 2, 1);
478 $escapedName = ldap_escape($this->mockUser->name);
479 $this->mockLdap->shouldReceive('searchAndGetEntries')->twice()
480 ->with($this->resourceId, config('services.ldap.base_dn'), "(&(uid={$escapedName}))", \Mockery::type('array'))
481 ->andReturn($userResp, $groupResp);
483 $this->mockLdap->shouldReceive('read')->times(1)
484 ->with($this->resourceId, 'cn=ldaptester,ou=groups,dc=example,dc=com', '(objectClass=*)', ['memberof'])
485 ->andReturn(['count' => 0]);
486 $this->mockLdap->shouldReceive('getEntries')->times(1)
487 ->with($this->resourceId, ['count' => 0])
488 ->andReturn(['count' => 0]);
490 $resp = $this->mockUserLogin();
491 $resp->assertRedirect('/');
494 public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
496 $role = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
497 $this->asAdmin()->get('/settings/roles/' . $role->id)
498 ->assertSee('ex-auth-a');
501 public function test_login_maps_roles_using_external_auth_ids_if_set()
503 $roleToReceive = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
504 $roleToNotReceive = Role::factory()->create(['display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
507 'services.ldap.user_to_groups' => true,
508 'services.ldap.group_attribute' => 'memberOf',
509 'services.ldap.remove_from_groups' => true,
512 $this->commonLdapMocks(1, 1, 3, 4, 2, 1, 1);
513 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
514 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
515 ->andReturn(['count' => 1, 0 => [
516 'uid' => [$this->mockUser->name],
517 'cn' => [$this->mockUser->name],
518 'dn' => 'dc=test' . config('services.ldap.base_dn'),
519 'mail' => [$this->mockUser->email],
522 0 => 'cn=ex-auth-a,ou=groups,dc=example,dc=com',
526 $this->mockUserLogin()->assertRedirect('/');
528 $user = User::query()->where('email', $this->mockUser->email)->first();
529 $this->assertDatabaseHas('role_user', [
530 'user_id' => $user->id,
531 'role_id' => $roleToReceive->id,
533 $this->assertDatabaseMissing('role_user', [
534 'user_id' => $user->id,
535 'role_id' => $roleToNotReceive->id,
539 public function test_login_group_mapping_does_not_conflict_with_default_role()
541 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
542 $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);
543 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
545 setting()->put('registration-role', $roleToReceive->id);
548 'services.ldap.user_to_groups' => true,
549 'services.ldap.group_attribute' => 'memberOf',
550 'services.ldap.remove_from_groups' => true,
553 $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 2);
554 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
555 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
556 ->andReturn(['count' => 1, 0 => [
557 'uid' => [$this->mockUser->name],
558 'cn' => [$this->mockUser->name],
559 'dn' => 'dc=test' . config('services.ldap.base_dn'),
560 'mail' => [$this->mockUser->email],
563 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
564 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
568 $this->mockUserLogin()->assertRedirect('/');
570 $user = User::query()->where('email', $this->mockUser->email)->first();
571 $this->assertDatabaseHas('role_user', [
572 'user_id' => $user->id,
573 'role_id' => $roleToReceive->id,
575 $this->assertDatabaseHas('role_user', [
576 'user_id' => $user->id,
577 'role_id' => $roleToReceive2->id,
581 public function test_login_uses_specified_display_name_attribute()
584 'services.ldap.display_name_attribute' => ['displayName'],
587 $this->commonLdapMocks(1, 1, 2, 4, 2);
588 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
589 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
590 ->andReturn(['count' => 1, 0 => [
591 'uid' => [$this->mockUser->name],
592 'cn' => [$this->mockUser->name],
593 'dn' => 'dc=test' . config('services.ldap.base_dn'),
594 'displayname' => 'displayNameAttribute',
597 $this->mockUserLogin()->assertRedirect('/login');
598 $this->get('/login')->assertSee('Please enter an email to use for this account.');
600 $resp = $this->mockUserLogin($this->mockUser->email);
601 $resp->assertRedirect('/');
602 $this->get('/')->assertSee('displayNameAttribute');
603 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
606 public function test_login_uses_default_display_name_attribute_if_specified_not_present()
609 'services.ldap.display_name_attribute' => ['displayName'],
612 $this->commonLdapMocks(1, 1, 2, 4, 2);
613 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
614 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
615 ->andReturn(['count' => 1, 0 => [
616 'uid' => [$this->mockUser->name],
617 'cn' => [$this->mockUser->name],
618 'dn' => 'dc=test' . config('services.ldap.base_dn'),
621 $this->mockUserLogin()->assertRedirect('/login');
622 $this->get('/login')->assertSee('Please enter an email to use for this account.');
624 $resp = $this->mockUserLogin($this->mockUser->email);
625 $resp->assertRedirect('/');
626 $this->get('/')->assertSee($this->mockUser->name);
627 $this->assertDatabaseHas('users', [
628 'email' => $this->mockUser->email,
629 'email_confirmed' => false,
630 'external_auth_id' => $this->mockUser->name,
631 'name' => $this->mockUser->name,
635 protected function checkLdapReceivesCorrectDetails($serverString, $expectedHostString): void
637 app('config')->set(['services.ldap.server' => $serverString]);
639 $this->mockLdap->shouldReceive('connect')
641 ->with($expectedHostString)
644 $this->mockUserLogin();
647 public function test_ldap_receives_correct_connect_host_from_config()
649 $expectedResultByInput = [
650 'ldaps://bookstack:8080' => 'ldaps://bookstack:8080',
651 'ldap.bookstack.com:8080' => 'ldap://ldap.bookstack.com:8080',
652 'ldap.bookstack.com' => 'ldap://ldap.bookstack.com',
653 'ldaps://ldap.bookstack.com' => 'ldaps://ldap.bookstack.com',
654 'ldaps://ldap.bookstack.com ldap://a.b.com' => 'ldaps://ldap.bookstack.com ldap://a.b.com',
657 foreach ($expectedResultByInput as $input => $expectedResult) {
658 $this->checkLdapReceivesCorrectDetails($input, $expectedResult);
659 $this->refreshApplication();
664 public function test_forgot_password_routes_inaccessible()
666 $resp = $this->get('/password/email');
667 $this->assertPermissionError($resp);
669 $resp = $this->post('/password/email');
670 $this->assertPermissionError($resp);
672 $resp = $this->get('/password/reset/abc123');
673 $this->assertPermissionError($resp);
675 $resp = $this->post('/password/reset');
676 $this->assertPermissionError($resp);
679 public function test_user_invite_routes_inaccessible()
681 $resp = $this->get('/register/invite/abc123');
682 $this->assertPermissionError($resp);
684 $resp = $this->post('/register/invite/abc123');
685 $this->assertPermissionError($resp);
688 public function test_user_register_routes_inaccessible()
690 $resp = $this->get('/register');
691 $this->assertPermissionError($resp);
693 $resp = $this->post('/register');
694 $this->assertPermissionError($resp);
697 public function test_dump_user_details_option_works()
699 config()->set(['services.ldap.dump_user_details' => true, 'services.ldap.thumbnail_attribute' => 'jpegphoto']);
701 $this->commonLdapMocks(1, 1, 1, 1, 1);
702 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
703 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
704 ->andReturn(['count' => 1, 0 => [
705 'uid' => [$this->mockUser->name],
706 'cn' => [$this->mockUser->name],
707 // Test dumping binary data for avatar responses
708 'jpegphoto' => base64_decode('/9j/4AAQSkZJRg=='),
709 'dn' => 'dc=test' . config('services.ldap.base_dn'),
712 $resp = $this->post('/login', [
713 'username' => $this->mockUser->name,
714 'password' => $this->mockUser->password,
716 $resp->assertJsonStructure([
717 'details_from_ldap' => [],
718 'details_bookstack_parsed' => [],
722 public function test_start_tls_called_if_option_set()
724 config()->set(['services.ldap.start_tls' => true]);
725 $this->mockLdap->shouldReceive('startTls')->once()->andReturn(true);
726 $this->runFailedAuthLogin();
729 public function test_connection_fails_if_tls_fails()
731 config()->set(['services.ldap.start_tls' => true]);
732 $this->mockLdap->shouldReceive('startTls')->once()->andReturn(false);
733 $this->commonLdapMocks(1, 1, 0, 0, 0);
734 $resp = $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);
735 $resp->assertStatus(500);
738 public function test_ldap_attributes_can_be_binary_decoded_if_marked()
740 config()->set(['services.ldap.id_attribute' => 'BIN;uid']);
741 $ldapService = app()->make(LdapService::class);
742 $this->commonLdapMocks(1, 1, 1, 1, 1);
743 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
744 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), ['cn', 'dn', 'uid', 'mail', 'cn'])
745 ->andReturn(['count' => 1, 0 => [
746 'uid' => [hex2bin('FFF8F7')],
747 'cn' => [$this->mockUser->name],
748 'dn' => 'dc=test' . config('services.ldap.base_dn'),
751 $details = $ldapService->getUserDetails('test');
752 $this->assertEquals('fff8f7', $details['uid']);
755 public function test_new_ldap_user_login_with_already_used_email_address_shows_error_message_to_user()
757 $this->commonLdapMocks(1, 1, 2, 4, 2);
758 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
759 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
760 ->andReturn(['count' => 1, 0 => [
761 'uid' => [$this->mockUser->name],
762 'cn' => [$this->mockUser->name],
763 'dn' => 'dc=test' . config('services.ldap.base_dn'),
765 ]], ['count' => 1, 0 => [
768 'dn' => 'dc=bscott' . config('services.ldap.base_dn'),
773 $this->mockUserLogin()->assertRedirect('/');
777 $resp = $this->followingRedirects()->post('/login', ['username' => 'bscott', 'password' => 'pass']);
778 $resp->assertSee('A user with the email
[email protected] already exists but with different credentials');
781 public function test_login_with_email_confirmation_required_maps_groups_but_shows_confirmation_screen()
783 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
784 $user = User::factory()->make();
785 setting()->put('registration-confirmation', 'true');
788 'services.ldap.user_to_groups' => true,
789 'services.ldap.group_attribute' => 'memberOf',
790 'services.ldap.remove_from_groups' => true,
793 $this->commonLdapMocks(1, 1, 6, 8, 4, 2, 2);
794 $this->mockLdap->shouldReceive('searchAndGetEntries')
796 ->andReturn(['count' => 1, 0 => [
797 'uid' => [$user->name],
798 'cn' => [$user->name],
799 'dn' => 'dc=test' . config('services.ldap.base_dn'),
800 'mail' => [$user->email],
803 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
807 $login = $this->followingRedirects()->mockUserLogin();
808 $login->assertSee('Thanks for registering!');
809 $this->assertDatabaseHas('users', [
810 'email' => $user->email,
811 'email_confirmed' => false,
814 $user = User::query()->where('email', '=', $user->email)->first();
815 $this->assertDatabaseHas('role_user', [
816 'user_id' => $user->id,
817 'role_id' => $roleToReceive->id,
820 $this->assertNull(auth()->user());
822 $homePage = $this->get('/');
823 $homePage->assertRedirect('/login');
825 $login = $this->followingRedirects()->mockUserLogin();
826 $login->assertSee('Email Address Not Confirmed');
829 public function test_failed_logins_are_logged_when_message_configured()
831 $log = $this->withTestLogger();
832 config()->set(['logging.failed_login.message' => 'Failed login for %u']);
833 $this->runFailedAuthLogin();
834 $this->assertTrue($log->hasWarningThatContains('Failed login for timmyjenkins'));
837 public function test_thumbnail_attribute_used_as_user_avatar_if_configured()
839 config()->set(['services.ldap.thumbnail_attribute' => 'jpegPhoto']);
841 $this->commonLdapMocks(1, 1, 1, 2, 1);
842 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
843 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
844 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
845 ->andReturn(['count' => 1, 0 => [
846 'cn' => [$this->mockUser->name],
848 'jpegphoto' => [base64_decode('/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8Q
849 EBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=')],
850 'mail' => [$this->mockUser->email],
853 $this->mockUserLogin()
854 ->assertRedirect('/');
856 $user = User::query()->where('email', '=', $this->mockUser->email)->first();
857 $this->assertNotNull($user->avatar);
858 $this->assertEquals('8c90748342f19b195b9c6b4eff742ded', md5_file(public_path($user->avatar->path)));
861 public function test_tls_ca_cert_option_throws_if_set_to_invalid_location()
863 $path = 'non_found_' . time();
864 config()->set(['services.ldap.tls_ca_cert' => $path]);
866 $this->commonLdapMocks(0, 0, 0, 0, 0);
868 $this->assertThrows(function () {
869 $this->withoutExceptionHandling()->mockUserLogin();
870 }, LdapException::class, "Provided path [{$path}] for LDAP TLS CA certs could not be resolved to an existing location");
873 public function test_tls_ca_cert_option_used_if_set_to_a_folder()
875 $path = $this->files->testFilePath('');
876 config()->set(['services.ldap.tls_ca_cert' => $path]);
878 $this->mockLdap->shouldReceive('setOption')->once()->with(null, LDAP_OPT_X_TLS_CACERTDIR, rtrim($path, '/'))->andReturn(true);
879 $this->runFailedAuthLogin();
882 public function test_tls_ca_cert_option_used_if_set_to_a_file()
884 $path = $this->files->testFilePath('test-file.txt');
885 config()->set(['services.ldap.tls_ca_cert' => $path]);
887 $this->mockLdap->shouldReceive('setOption')->once()->with(null, LDAP_OPT_X_TLS_CACERTFILE, $path)->andReturn(true);
888 $this->runFailedAuthLogin();