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)
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);
89 public function test_login()
91 $this->commonLdapMocks(1, 1, 2, 4, 2);
92 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
93 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
94 ->andReturn(['count' => 1, 0 => [
95 'uid' => [$this->mockUser->name],
96 'cn' => [$this->mockUser->name],
97 'dn' => 'dc=test' . config('services.ldap.base_dn'),
100 $resp = $this->mockUserLogin();
101 $resp->assertRedirect('/login');
102 $resp = $this->followRedirects($resp);
103 $resp->assertSee('Please enter an email to use for this account.');
104 $resp->assertSee($this->mockUser->name);
106 $resp = $this->followingRedirects()->mockUserLogin($this->mockUser->email);
107 $this->withHtml($resp)->assertElementExists('#home-default');
108 $resp->assertSee($this->mockUser->name);
109 $this->assertDatabaseHas('users', [
110 'email' => $this->mockUser->email,
111 'email_confirmed' => false,
112 'external_auth_id' => $this->mockUser->name,
116 public function test_email_domain_restriction_active_on_new_ldap_login()
119 'registration-restrict' => 'testing.com',
122 $this->commonLdapMocks(1, 1, 2, 4, 2);
123 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
124 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
125 ->andReturn(['count' => 1, 0 => [
126 'uid' => [$this->mockUser->name],
127 'cn' => [$this->mockUser->name],
128 'dn' => 'dc=test' . config('services.ldap.base_dn'),
131 $resp = $this->mockUserLogin();
132 $resp->assertRedirect('/login');
133 $this->followRedirects($resp)->assertSee('Please enter an email to use for this account.');
136 $resp = $this->mockUserLogin($email);
137 $resp->assertRedirect('/login');
138 $this->followRedirects($resp)->assertSee('That email domain does not have access to this application');
140 $this->assertDatabaseMissing('users', ['email' => $email]);
143 public function test_login_works_when_no_uid_provided_by_ldap_server()
145 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
147 $this->commonLdapMocks(1, 1, 1, 2, 1);
148 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
149 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
150 ->andReturn(['count' => 1, 0 => [
151 'cn' => [$this->mockUser->name],
153 'mail' => [$this->mockUser->email],
156 $resp = $this->mockUserLogin();
157 $resp->assertRedirect('/');
158 $this->followRedirects($resp)->assertSee($this->mockUser->name);
159 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
162 public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()
164 config()->set(['services.ldap.id_attribute' => 'my_custom_id']);
166 $this->commonLdapMocks(1, 1, 1, 2, 1);
167 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
168 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
169 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
170 ->andReturn(['count' => 1, 0 => [
171 'cn' => [$this->mockUser->name],
173 'my_custom_id' => ['cooluser456'],
174 'mail' => [$this->mockUser->email],
177 $resp = $this->mockUserLogin();
178 $resp->assertRedirect('/');
179 $this->followRedirects($resp)->assertSee($this->mockUser->name);
180 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);
183 public function test_user_filter_default_placeholder_format()
185 config()->set('services.ldap.user_filter', '(&(uid={user}))');
186 $this->mockUser->name = 'barryldapuser';
187 $expectedFilter = '(&(uid=\62\61\72\72\79\6c\64\61\70\75\73\65\72))';
189 $this->commonLdapMocks(1, 1, 1, 1, 1);
190 $this->mockLdap->shouldReceive('searchAndGetEntries')
192 ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \Mockery::type('array'))
193 ->andReturn(['count' => 0, 0 => []]);
195 $resp = $this->mockUserLogin();
196 $resp->assertRedirect('/login');
199 public function test_user_filter_old_placeholder_format()
201 config()->set('services.ldap.user_filter', '(&(username=${user}))');
202 $this->mockUser->name = 'barryldapuser';
203 $expectedFilter = '(&(username=\62\61\72\72\79\6c\64\61\70\75\73\65\72))';
205 $this->commonLdapMocks(1, 1, 1, 1, 1);
206 $this->mockLdap->shouldReceive('searchAndGetEntries')
208 ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \Mockery::type('array'))
209 ->andReturn(['count' => 0, 0 => []]);
211 $resp = $this->mockUserLogin();
212 $resp->assertRedirect('/login');
215 public function test_initial_incorrect_credentials()
217 $this->commonLdapMocks(1, 1, 1, 0, 1);
218 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
219 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
220 ->andReturn(['count' => 1, 0 => [
221 'uid' => [$this->mockUser->name],
222 'cn' => [$this->mockUser->name],
223 'dn' => 'dc=test' . config('services.ldap.base_dn'),
225 $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false);
227 $resp = $this->mockUserLogin();
228 $resp->assertRedirect('/login');
229 $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
230 $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
233 public function test_login_not_found_username()
235 $this->commonLdapMocks(1, 1, 1, 1, 1);
236 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
237 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
238 ->andReturn(['count' => 0]);
240 $resp = $this->mockUserLogin();
241 $resp->assertRedirect('/login');
242 $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
243 $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
246 public function test_create_user_form()
248 $userForm = $this->asAdmin()->get('/settings/users/create');
249 $userForm->assertDontSee('Password');
251 $save = $this->post('/settings/users/create', [
252 'name' => $this->mockUser->name,
253 'email' => $this->mockUser->email,
255 $save->assertSessionHasErrors(['external_auth_id' => 'The external auth id field is required.']);
257 $save = $this->post('/settings/users/create', [
258 'name' => $this->mockUser->name,
259 'email' => $this->mockUser->email,
260 'external_auth_id' => $this->mockUser->name,
262 $save->assertRedirect('/settings/users');
263 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
266 public function test_user_edit_form()
268 $editUser = $this->users->viewer();
269 $editPage = $this->asAdmin()->get("/settings/users/{$editUser->id}");
270 $editPage->assertSee('Edit User');
271 $editPage->assertDontSee('Password');
273 $update = $this->put("/settings/users/{$editUser->id}", [
274 'name' => $editUser->name,
275 'email' => $editUser->email,
276 'external_auth_id' => 'test_auth_id',
278 $update->assertRedirect('/settings/users');
279 $this->assertDatabaseHas('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
282 public function test_registration_disabled()
284 $resp = $this->followingRedirects()->get('/register');
285 $this->withHtml($resp)->assertElementContains('#content', 'Log In');
288 public function test_non_admins_cannot_change_auth_id()
290 $testUser = $this->users->viewer();
291 $this->actingAs($testUser)
292 ->get('/settings/users/' . $testUser->id)
293 ->assertDontSee('External Authentication');
296 public function test_login_maps_roles_and_retains_existing_roles()
298 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
299 $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);
300 $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);
301 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
302 $this->mockUser->attachRole($existingRole);
305 'services.ldap.user_to_groups' => true,
306 'services.ldap.group_attribute' => 'memberOf',
307 'services.ldap.remove_from_groups' => false,
310 $this->commonLdapMocks(1, 1, 4, 5, 4, 6);
311 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
312 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
313 ->andReturn(['count' => 1, 0 => [
314 'uid' => [$this->mockUser->name],
315 'cn' => [$this->mockUser->name],
316 'dn' => 'dc=test' . config('services.ldap.base_dn'),
317 'mail' => [$this->mockUser->email],
320 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
321 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
325 $this->mockUserLogin()->assertRedirect('/');
327 $user = User::where('email', $this->mockUser->email)->first();
328 $this->assertDatabaseHas('role_user', [
329 'user_id' => $user->id,
330 'role_id' => $roleToReceive->id,
332 $this->assertDatabaseHas('role_user', [
333 'user_id' => $user->id,
334 'role_id' => $roleToReceive2->id,
336 $this->assertDatabaseHas('role_user', [
337 'user_id' => $user->id,
338 'role_id' => $existingRole->id,
342 public function test_login_maps_roles_and_removes_old_roles_if_set()
344 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
345 $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);
346 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
347 $this->mockUser->attachRole($existingRole);
350 'services.ldap.user_to_groups' => true,
351 'services.ldap.group_attribute' => 'memberOf',
352 'services.ldap.remove_from_groups' => true,
355 $this->commonLdapMocks(1, 1, 3, 4, 3, 2);
356 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
357 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
358 ->andReturn(['count' => 1, 0 => [
359 'uid' => [$this->mockUser->name],
360 'cn' => [$this->mockUser->name],
361 'dn' => 'dc=test' . config('services.ldap.base_dn'),
362 'mail' => [$this->mockUser->email],
365 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
369 $this->mockUserLogin()->assertRedirect('/');
371 $user = User::query()->where('email', $this->mockUser->email)->first();
372 $this->assertDatabaseHas('role_user', [
373 'user_id' => $user->id,
374 'role_id' => $roleToReceive->id,
376 $this->assertDatabaseMissing('role_user', [
377 'user_id' => $user->id,
378 'role_id' => $existingRole->id,
382 public function test_dump_user_groups_shows_group_related_details_as_json()
385 'services.ldap.user_to_groups' => true,
386 'services.ldap.group_attribute' => 'memberOf',
387 'services.ldap.remove_from_groups' => true,
388 'services.ldap.dump_user_groups' => true,
391 $userResp = ['count' => 1, 0 => [
392 'uid' => [$this->mockUser->name],
393 'cn' => [$this->mockUser->name],
394 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
395 'mail' => [$this->mockUser->email],
397 $this->commonLdapMocks(1, 1, 4, 5, 4, 2);
398 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
399 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
400 ->andReturn($userResp, ['count' => 1,
402 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
405 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
411 'dn' => 'cn=ldaptester,ou=groups,dc=example,dc=com',
414 0 => 'cn=monsters,ou=groups,dc=example,dc=com',
419 $resp = $this->mockUserLogin();
421 'details_from_ldap' => [
422 'dn' => 'dc=test,' . config('services.ldap.base_dn'),
424 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
428 'parsed_direct_user_groups' => [
431 'parsed_recursive_user_groups' => [
438 public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
440 $role = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
441 $this->asAdmin()->get('/settings/roles/' . $role->id)
442 ->assertSee('ex-auth-a');
445 public function test_login_maps_roles_using_external_auth_ids_if_set()
447 $roleToReceive = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
448 $roleToNotReceive = Role::factory()->create(['display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
451 'services.ldap.user_to_groups' => true,
452 'services.ldap.group_attribute' => 'memberOf',
453 'services.ldap.remove_from_groups' => true,
456 $this->commonLdapMocks(1, 1, 3, 4, 3, 2);
457 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
458 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
459 ->andReturn(['count' => 1, 0 => [
460 'uid' => [$this->mockUser->name],
461 'cn' => [$this->mockUser->name],
462 'dn' => 'dc=test' . config('services.ldap.base_dn'),
463 'mail' => [$this->mockUser->email],
466 0 => 'cn=ex-auth-a,ou=groups,dc=example,dc=com',
470 $this->mockUserLogin()->assertRedirect('/');
472 $user = User::query()->where('email', $this->mockUser->email)->first();
473 $this->assertDatabaseHas('role_user', [
474 'user_id' => $user->id,
475 'role_id' => $roleToReceive->id,
477 $this->assertDatabaseMissing('role_user', [
478 'user_id' => $user->id,
479 'role_id' => $roleToNotReceive->id,
483 public function test_login_group_mapping_does_not_conflict_with_default_role()
485 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
486 $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);
487 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
489 setting()->put('registration-role', $roleToReceive->id);
492 'services.ldap.user_to_groups' => true,
493 'services.ldap.group_attribute' => 'memberOf',
494 'services.ldap.remove_from_groups' => true,
497 $this->commonLdapMocks(1, 1, 4, 5, 4, 6);
498 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
499 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
500 ->andReturn(['count' => 1, 0 => [
501 'uid' => [$this->mockUser->name],
502 'cn' => [$this->mockUser->name],
503 'dn' => 'dc=test' . config('services.ldap.base_dn'),
504 'mail' => [$this->mockUser->email],
507 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
508 1 => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
512 $this->mockUserLogin()->assertRedirect('/');
514 $user = User::query()->where('email', $this->mockUser->email)->first();
515 $this->assertDatabaseHas('role_user', [
516 'user_id' => $user->id,
517 'role_id' => $roleToReceive->id,
519 $this->assertDatabaseHas('role_user', [
520 'user_id' => $user->id,
521 'role_id' => $roleToReceive2->id,
525 public function test_login_uses_specified_display_name_attribute()
528 'services.ldap.display_name_attribute' => 'displayName',
531 $this->commonLdapMocks(1, 1, 2, 4, 2);
532 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
533 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
534 ->andReturn(['count' => 1, 0 => [
535 'uid' => [$this->mockUser->name],
536 'cn' => [$this->mockUser->name],
537 'dn' => 'dc=test' . config('services.ldap.base_dn'),
538 'displayname' => 'displayNameAttribute',
541 $this->mockUserLogin()->assertRedirect('/login');
542 $this->get('/login')->assertSee('Please enter an email to use for this account.');
544 $resp = $this->mockUserLogin($this->mockUser->email);
545 $resp->assertRedirect('/');
546 $this->get('/')->assertSee('displayNameAttribute');
547 $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
550 public function test_login_uses_default_display_name_attribute_if_specified_not_present()
553 'services.ldap.display_name_attribute' => 'displayName',
556 $this->commonLdapMocks(1, 1, 2, 4, 2);
557 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
558 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
559 ->andReturn(['count' => 1, 0 => [
560 'uid' => [$this->mockUser->name],
561 'cn' => [$this->mockUser->name],
562 'dn' => 'dc=test' . config('services.ldap.base_dn'),
565 $this->mockUserLogin()->assertRedirect('/login');
566 $this->get('/login')->assertSee('Please enter an email to use for this account.');
568 $resp = $this->mockUserLogin($this->mockUser->email);
569 $resp->assertRedirect('/');
570 $this->get('/')->assertSee($this->mockUser->name);
571 $this->assertDatabaseHas('users', [
572 'email' => $this->mockUser->email,
573 'email_confirmed' => false,
574 'external_auth_id' => $this->mockUser->name,
575 'name' => $this->mockUser->name,
579 protected function checkLdapReceivesCorrectDetails($serverString, $expectedHostString): void
581 app('config')->set(['services.ldap.server' => $serverString]);
583 $this->mockLdap->shouldReceive('connect')
585 ->with($expectedHostString)
588 $this->mockUserLogin();
591 public function test_ldap_receives_correct_connect_host_from_config()
593 $expectedResultByInput = [
594 'ldaps://bookstack:8080' => 'ldaps://bookstack:8080',
595 'ldap.bookstack.com:8080' => 'ldap://ldap.bookstack.com:8080',
596 'ldap.bookstack.com' => 'ldap://ldap.bookstack.com',
597 'ldaps://ldap.bookstack.com' => 'ldaps://ldap.bookstack.com',
598 'ldaps://ldap.bookstack.com ldap://a.b.com' => 'ldaps://ldap.bookstack.com ldap://a.b.com',
601 foreach ($expectedResultByInput as $input => $expectedResult) {
602 $this->checkLdapReceivesCorrectDetails($input, $expectedResult);
603 $this->refreshApplication();
608 public function test_forgot_password_routes_inaccessible()
610 $resp = $this->get('/password/email');
611 $this->assertPermissionError($resp);
613 $resp = $this->post('/password/email');
614 $this->assertPermissionError($resp);
616 $resp = $this->get('/password/reset/abc123');
617 $this->assertPermissionError($resp);
619 $resp = $this->post('/password/reset');
620 $this->assertPermissionError($resp);
623 public function test_user_invite_routes_inaccessible()
625 $resp = $this->get('/register/invite/abc123');
626 $this->assertPermissionError($resp);
628 $resp = $this->post('/register/invite/abc123');
629 $this->assertPermissionError($resp);
632 public function test_user_register_routes_inaccessible()
634 $resp = $this->get('/register');
635 $this->assertPermissionError($resp);
637 $resp = $this->post('/register');
638 $this->assertPermissionError($resp);
641 public function test_dump_user_details_option_works()
643 config()->set(['services.ldap.dump_user_details' => true, 'services.ldap.thumbnail_attribute' => 'jpegphoto']);
645 $this->commonLdapMocks(1, 1, 1, 1, 1);
646 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
647 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
648 ->andReturn(['count' => 1, 0 => [
649 'uid' => [$this->mockUser->name],
650 'cn' => [$this->mockUser->name],
651 // Test dumping binary data for avatar responses
652 'jpegphoto' => base64_decode('/9j/4AAQSkZJRg=='),
653 'dn' => 'dc=test' . config('services.ldap.base_dn'),
656 $resp = $this->post('/login', [
657 'username' => $this->mockUser->name,
658 'password' => $this->mockUser->password,
660 $resp->assertJsonStructure([
661 'details_from_ldap' => [],
662 'details_bookstack_parsed' => [],
666 public function test_start_tls_called_if_option_set()
668 config()->set(['services.ldap.start_tls' => true]);
669 $this->mockLdap->shouldReceive('startTls')->once()->andReturn(true);
670 $this->runFailedAuthLogin();
673 public function test_connection_fails_if_tls_fails()
675 config()->set(['services.ldap.start_tls' => true]);
676 $this->mockLdap->shouldReceive('startTls')->once()->andReturn(false);
677 $this->commonLdapMocks(1, 1, 0, 0, 0);
678 $resp = $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);
679 $resp->assertStatus(500);
682 public function test_ldap_attributes_can_be_binary_decoded_if_marked()
684 config()->set(['services.ldap.id_attribute' => 'BIN;uid']);
685 $ldapService = app()->make(LdapService::class);
686 $this->commonLdapMocks(1, 1, 1, 1, 1);
687 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
688 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), ['cn', 'dn', 'uid', 'mail', 'cn'])
689 ->andReturn(['count' => 1, 0 => [
690 'uid' => [hex2bin('FFF8F7')],
691 'cn' => [$this->mockUser->name],
692 'dn' => 'dc=test' . config('services.ldap.base_dn'),
695 $details = $ldapService->getUserDetails('test');
696 $this->assertEquals('fff8f7', $details['uid']);
699 public function test_new_ldap_user_login_with_already_used_email_address_shows_error_message_to_user()
701 $this->commonLdapMocks(1, 1, 2, 4, 2);
702 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
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 'dn' => 'dc=test' . config('services.ldap.base_dn'),
709 ]], ['count' => 1, 0 => [
712 'dn' => 'dc=bscott' . config('services.ldap.base_dn'),
717 $this->mockUserLogin()->assertRedirect('/');
721 $resp = $this->followingRedirects()->post('/login', ['username' => 'bscott', 'password' => 'pass']);
722 $resp->assertSee('A user with the email
[email protected] already exists but with different credentials');
725 public function test_login_with_email_confirmation_required_maps_groups_but_shows_confirmation_screen()
727 $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
728 $user = User::factory()->make();
729 setting()->put('registration-confirmation', 'true');
732 'services.ldap.user_to_groups' => true,
733 'services.ldap.group_attribute' => 'memberOf',
734 'services.ldap.remove_from_groups' => true,
737 $this->commonLdapMocks(1, 1, 6, 8, 6, 4);
738 $this->mockLdap->shouldReceive('searchAndGetEntries')
740 ->andReturn(['count' => 1, 0 => [
741 'uid' => [$user->name],
742 'cn' => [$user->name],
743 'dn' => 'dc=test' . config('services.ldap.base_dn'),
744 'mail' => [$user->email],
747 0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
751 $login = $this->followingRedirects()->mockUserLogin();
752 $login->assertSee('Thanks for registering!');
753 $this->assertDatabaseHas('users', [
754 'email' => $user->email,
755 'email_confirmed' => false,
758 $user = User::query()->where('email', '=', $user->email)->first();
759 $this->assertDatabaseHas('role_user', [
760 'user_id' => $user->id,
761 'role_id' => $roleToReceive->id,
764 $this->assertNull(auth()->user());
766 $homePage = $this->get('/');
767 $homePage->assertRedirect('/login');
769 $login = $this->followingRedirects()->mockUserLogin();
770 $login->assertSee('Email Address Not Confirmed');
773 public function test_failed_logins_are_logged_when_message_configured()
775 $log = $this->withTestLogger();
776 config()->set(['logging.failed_login.message' => 'Failed login for %u']);
777 $this->runFailedAuthLogin();
778 $this->assertTrue($log->hasWarningThatContains('Failed login for timmyjenkins'));
781 public function test_thumbnail_attribute_used_as_user_avatar_if_configured()
783 config()->set(['services.ldap.thumbnail_attribute' => 'jpegPhoto']);
785 $this->commonLdapMocks(1, 1, 1, 2, 1);
786 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
787 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
788 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
789 ->andReturn(['count' => 1, 0 => [
790 'cn' => [$this->mockUser->name],
792 'jpegphoto' => [base64_decode('/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8Q
793 EBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=')],
794 'mail' => [$this->mockUser->email],
797 $this->mockUserLogin()
798 ->assertRedirect('/');
800 $user = User::query()->where('email', '=', $this->mockUser->email)->first();
801 $this->assertNotNull($user->avatar);
802 $this->assertEquals('8c90748342f19b195b9c6b4eff742ded', md5_file(public_path($user->avatar->path)));
805 public function test_tls_ca_cert_option_throws_if_set_to_invalid_location()
807 $path = 'non_found_' . time();
808 config()->set(['services.ldap.tls_ca_cert' => $path]);
810 $this->commonLdapMocks(0, 0, 0, 0, 0);
812 $this->assertThrows(function () {
813 $this->withoutExceptionHandling()->mockUserLogin();
814 }, LdapException::class, "Provided path [{$path}] for LDAP TLS CA certs could not be resolved to an existing location");
817 public function test_tls_ca_cert_option_used_if_set_to_a_folder()
819 $path = $this->files->testFilePath('');
820 config()->set(['services.ldap.tls_ca_cert' => $path]);
822 $this->mockLdap->shouldReceive('setOption')->once()->with(null, LDAP_OPT_X_TLS_CACERTDIR, rtrim($path, '/'))->andReturn(true);
823 $this->runFailedAuthLogin();
826 public function test_tls_ca_cert_option_used_if_set_to_a_file()
828 $path = $this->files->testFilePath('test-file.txt');
829 config()->set(['services.ldap.tls_ca_cert' => $path]);
831 $this->mockLdap->shouldReceive('setOption')->once()->with(null, LDAP_OPT_X_TLS_CACERTFILE, $path)->andReturn(true);
832 $this->runFailedAuthLogin();