3 use BookStack\Auth\Role;
4 use BookStack\Auth\Access\Ldap;
5 use BookStack\Auth\User;
6 use Mockery\MockInterface;
8 class LdapTest extends BrowserKitTest
17 protected $resourceId = 'resource-test';
19 public function setUp(): void
22 if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
24 'auth.method' => 'ldap',
25 'auth.defaults.guard' => 'ldap',
26 'services.ldap.base_dn' => 'dc=ldap,dc=local',
27 'services.ldap.email_attribute' => 'mail',
28 'services.ldap.display_name_attribute' => 'cn',
29 'services.ldap.id_attribute' => 'uid',
30 'services.ldap.user_to_groups' => false,
31 'services.ldap.version' => '3',
32 'services.ldap.user_filter' => '(&(uid=${user}))',
33 'services.ldap.follow_referrals' => false,
34 'services.ldap.tls_insecure' => false,
36 $this->mockLdap = \Mockery::mock(Ldap::class);
37 $this->app[Ldap::class] = $this->mockLdap;
38 $this->mockUser = factory(User::class)->make();
41 protected function mockEscapes($times = 1)
43 $this->mockLdap->shouldReceive('escape')->times($times)->andReturnUsing(function($val) {
44 return ldap_escape($val);
48 protected function mockExplodes($times = 1)
50 $this->mockLdap->shouldReceive('explodeDn')->times($times)->andReturnUsing(function($dn, $withAttrib) {
51 return ldap_explode_dn($dn, $withAttrib);
55 protected function mockUserLogin()
57 return $this->visit('/login')
59 ->type($this->mockUser->name, '#username')
60 ->type($this->mockUser->password, '#password')
64 public function test_login()
66 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
67 $this->mockLdap->shouldReceive('setVersion')->once();
68 $this->mockLdap->shouldReceive('setOption')->times(2);
69 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
70 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
71 ->andReturn(['count' => 1, 0 => [
72 'uid' => [$this->mockUser->name],
73 'cn' => [$this->mockUser->name],
74 'dn' => ['dc=test' . config('services.ldap.base_dn')]
76 $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
77 $this->mockEscapes(2);
79 $this->mockUserLogin()
80 ->seePageIs('/login')->see('Please enter an email to use for this account.');
82 $this->type($this->mockUser->email, '#email')
85 ->see($this->mockUser->name)
86 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name]);
89 public function test_email_domain_restriction_active_on_new_ldap_login()
92 'registration-restrict' => 'testing.com'
95 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
96 $this->mockLdap->shouldReceive('setVersion')->once();
97 $this->mockLdap->shouldReceive('setOption')->times(2);
98 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
99 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
100 ->andReturn(['count' => 1, 0 => [
101 'uid' => [$this->mockUser->name],
102 'cn' => [$this->mockUser->name],
103 'dn' => ['dc=test' . config('services.ldap.base_dn')]
105 $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
106 $this->mockEscapes(2);
108 $this->mockUserLogin()
109 ->seePageIs('/login')
110 ->see('Please enter an email to use for this account.');
114 $this->type($email, '#email')
116 ->seePageIs('/login')
117 ->see('That email domain does not have access to this application')
118 ->dontSeeInDatabase('users', ['email' => $email]);
121 public function test_login_works_when_no_uid_provided_by_ldap_server()
123 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
124 $this->mockLdap->shouldReceive('setVersion')->once();
125 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
126 $this->mockLdap->shouldReceive('setOption')->times(1);
127 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
128 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
129 ->andReturn(['count' => 1, 0 => [
130 'cn' => [$this->mockUser->name],
132 'mail' => [$this->mockUser->email]
134 $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true);
135 $this->mockEscapes(1);
137 $this->mockUserLogin()
139 ->see($this->mockUser->name)
140 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
143 public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()
145 config()->set(['services.ldap.id_attribute' => 'my_custom_id']);
146 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
147 $this->mockLdap->shouldReceive('setVersion')->once();
148 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
149 $this->mockLdap->shouldReceive('setOption')->times(1);
150 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
151 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
152 ->andReturn(['count' => 1, 0 => [
153 'cn' => [$this->mockUser->name],
155 'my_custom_id' => ['cooluser456'],
156 'mail' => [$this->mockUser->email]
160 $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true);
161 $this->mockEscapes(1);
163 $this->mockUserLogin()
165 ->see($this->mockUser->name)
166 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);
169 public function test_initial_incorrect_credentials()
171 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
172 $this->mockLdap->shouldReceive('setVersion')->once();
173 $this->mockLdap->shouldReceive('setOption')->times(1);
174 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
175 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
176 ->andReturn(['count' => 1, 0 => [
177 'uid' => [$this->mockUser->name],
178 'cn' => [$this->mockUser->name],
179 'dn' => ['dc=test' . config('services.ldap.base_dn')]
181 $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false);
182 $this->mockEscapes(1);
184 $this->mockUserLogin()
185 ->seePageIs('/login')->see('These credentials do not match our records.')
186 ->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
189 public function test_login_not_found_username()
191 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
192 $this->mockLdap->shouldReceive('setVersion')->once();
193 $this->mockLdap->shouldReceive('setOption')->times(1);
194 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
195 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
196 ->andReturn(['count' => 0]);
197 $this->mockLdap->shouldReceive('bind')->times(1)->andReturn(true, false);
198 $this->mockEscapes(1);
200 $this->mockUserLogin()
201 ->seePageIs('/login')->see('These credentials do not match our records.')
202 ->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
206 public function test_create_user_form()
208 $this->asAdmin()->visit('/settings/users/create')
209 ->dontSee('Password')
210 ->type($this->mockUser->name, '#name')
211 ->type($this->mockUser->email, '#email')
213 ->see('The external auth id field is required.')
214 ->type($this->mockUser->name, '#external_auth_id')
216 ->seePageIs('/settings/users')
217 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
220 public function test_user_edit_form()
222 $editUser = $this->getNormalUser();
223 $this->asAdmin()->visit('/settings/users/' . $editUser->id)
225 ->dontSee('Password')
226 ->type('test_auth_id', '#external_auth_id')
228 ->seePageIs('/settings/users')
229 ->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
232 public function test_registration_disabled()
234 $this->visit('/register')
235 ->seePageIs('/login');
238 public function test_non_admins_cannot_change_auth_id()
240 $testUser = $this->getNormalUser();
241 $this->actingAs($testUser)->visit('/settings/users/' . $testUser->id)
242 ->dontSee('External Authentication');
245 public function test_login_maps_roles_and_retains_existing_roles()
247 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
248 $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
249 $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
250 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
251 $this->mockUser->attachRole($existingRole);
254 'services.ldap.user_to_groups' => true,
255 'services.ldap.group_attribute' => 'memberOf',
256 'services.ldap.remove_from_groups' => false,
258 $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
259 $this->mockLdap->shouldReceive('setVersion')->times(1);
260 $this->mockLdap->shouldReceive('setOption')->times(4);
261 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
262 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
263 ->andReturn(['count' => 1, 0 => [
264 'uid' => [$this->mockUser->name],
265 'cn' => [$this->mockUser->name],
266 'dn' => ['dc=test' . config('services.ldap.base_dn')],
267 'mail' => [$this->mockUser->email],
270 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
271 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
274 $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
275 $this->mockEscapes(4);
276 $this->mockExplodes(6);
278 $this->mockUserLogin()->seePageIs('/');
280 $user = User::where('email', $this->mockUser->email)->first();
281 $this->seeInDatabase('role_user', [
282 'user_id' => $user->id,
283 'role_id' => $roleToReceive->id
285 $this->seeInDatabase('role_user', [
286 'user_id' => $user->id,
287 'role_id' => $roleToReceive2->id
289 $this->seeInDatabase('role_user', [
290 'user_id' => $user->id,
291 'role_id' => $existingRole->id
295 public function test_login_maps_roles_and_removes_old_roles_if_set()
297 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
298 $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
299 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
300 $this->mockUser->attachRole($existingRole);
303 'services.ldap.user_to_groups' => true,
304 'services.ldap.group_attribute' => 'memberOf',
305 'services.ldap.remove_from_groups' => true,
307 $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
308 $this->mockLdap->shouldReceive('setVersion')->times(1);
309 $this->mockLdap->shouldReceive('setOption')->times(3);
310 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
311 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
312 ->andReturn(['count' => 1, 0 => [
313 'uid' => [$this->mockUser->name],
314 'cn' => [$this->mockUser->name],
315 'dn' => ['dc=test' . config('services.ldap.base_dn')],
316 'mail' => [$this->mockUser->email],
319 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
322 $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
323 $this->mockEscapes(3);
324 $this->mockExplodes(2);
326 $this->mockUserLogin()->seePageIs('/');
328 $user = User::where('email', $this->mockUser->email)->first();
329 $this->seeInDatabase('role_user', [
330 'user_id' => $user->id,
331 'role_id' => $roleToReceive->id
333 $this->dontSeeInDatabase('role_user', [
334 'user_id' => $user->id,
335 'role_id' => $existingRole->id
339 public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
341 $role = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
342 $this->asAdmin()->visit('/settings/roles/' . $role->id)
346 public function test_login_maps_roles_using_external_auth_ids_if_set()
348 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
349 $roleToNotReceive = factory(Role::class)->create(['name' => 'ldaptester-not-receive', 'display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
352 'services.ldap.user_to_groups' => true,
353 'services.ldap.group_attribute' => 'memberOf',
354 'services.ldap.remove_from_groups' => true,
356 $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
357 $this->mockLdap->shouldReceive('setVersion')->times(1);
358 $this->mockLdap->shouldReceive('setOption')->times(3);
359 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
360 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
361 ->andReturn(['count' => 1, 0 => [
362 'uid' => [$this->mockUser->name],
363 'cn' => [$this->mockUser->name],
364 'dn' => ['dc=test' . config('services.ldap.base_dn')],
365 'mail' => [$this->mockUser->email],
368 0 => "cn=ex-auth-a,ou=groups,dc=example,dc=com",
371 $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
372 $this->mockEscapes(3);
373 $this->mockExplodes(2);
375 $this->mockUserLogin()->seePageIs('/');
377 $user = User::where('email', $this->mockUser->email)->first();
378 $this->seeInDatabase('role_user', [
379 'user_id' => $user->id,
380 'role_id' => $roleToReceive->id
382 $this->dontSeeInDatabase('role_user', [
383 'user_id' => $user->id,
384 'role_id' => $roleToNotReceive->id
388 public function test_login_group_mapping_does_not_conflict_with_default_role()
390 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
391 $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
392 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
394 setting()->put('registration-role', $roleToReceive->id);
397 'services.ldap.user_to_groups' => true,
398 'services.ldap.group_attribute' => 'memberOf',
399 'services.ldap.remove_from_groups' => true,
401 $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
402 $this->mockLdap->shouldReceive('setVersion')->times(1);
403 $this->mockLdap->shouldReceive('setOption')->times(4);
404 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
405 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
406 ->andReturn(['count' => 1, 0 => [
407 'uid' => [$this->mockUser->name],
408 'cn' => [$this->mockUser->name],
409 'dn' => ['dc=test' . config('services.ldap.base_dn')],
410 'mail' => [$this->mockUser->email],
413 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
414 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
417 $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
418 $this->mockEscapes(4);
419 $this->mockExplodes(6);
421 $this->mockUserLogin()->seePageIs('/');
423 $user = User::where('email', $this->mockUser->email)->first();
424 $this->seeInDatabase('role_user', [
425 'user_id' => $user->id,
426 'role_id' => $roleToReceive->id
428 $this->seeInDatabase('role_user', [
429 'user_id' => $user->id,
430 'role_id' => $roleToReceive2->id
434 public function test_login_uses_specified_display_name_attribute()
437 'services.ldap.display_name_attribute' => 'displayName'
440 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
441 $this->mockLdap->shouldReceive('setVersion')->once();
442 $this->mockLdap->shouldReceive('setOption')->times(2);
443 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
444 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
445 ->andReturn(['count' => 1, 0 => [
446 'uid' => [$this->mockUser->name],
447 'cn' => [$this->mockUser->name],
448 'dn' => ['dc=test' . config('services.ldap.base_dn')],
449 'displayname' => 'displayNameAttribute'
451 $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
452 $this->mockEscapes(2);
454 $this->mockUserLogin()
455 ->seePageIs('/login')->see('Please enter an email to use for this account.');
457 $this->type($this->mockUser->email, '#email')
460 ->see('displayNameAttribute')
461 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
464 public function test_login_uses_default_display_name_attribute_if_specified_not_present()
467 'services.ldap.display_name_attribute' => 'displayName'
470 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
471 $this->mockLdap->shouldReceive('setVersion')->once();
472 $this->mockLdap->shouldReceive('setOption')->times(2);
473 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
474 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
475 ->andReturn(['count' => 1, 0 => [
476 'uid' => [$this->mockUser->name],
477 'cn' => [$this->mockUser->name],
478 'dn' => ['dc=test' . config('services.ldap.base_dn')]
480 $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
481 $this->mockEscapes(2);
483 $this->mockUserLogin()
484 ->seePageIs('/login')->see('Please enter an email to use for this account.');
486 $this->type($this->mockUser->email, '#email')
489 ->see($this->mockUser->name)
490 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => $this->mockUser->name]);
493 protected function checkLdapReceivesCorrectDetails($serverString, $expectedHost, $expectedPort)
496 'services.ldap.server' => $serverString
500 $this->mockLdap->shouldReceive('setVersion')->once();
501 $this->mockLdap->shouldReceive('setOption')->times(1);
502 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)->andReturn(['count' => 1, 0 => [
503 'uid' => [$this->mockUser->name],
504 'cn' => [$this->mockUser->name],
505 'dn' => ['dc=test' . config('services.ldap.base_dn')]
507 $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true);
508 $this->mockEscapes(1);
510 $this->mockLdap->shouldReceive('connect')->once()
511 ->with($expectedHost, $expectedPort)->andReturn($this->resourceId);
512 $this->mockUserLogin();
515 public function test_ldap_port_provided_on_host_if_host_is_full_uri()
517 $hostName = 'ldaps://bookstack:8080';
518 $this->checkLdapReceivesCorrectDetails($hostName, $hostName, 389);
521 public function test_ldap_port_parsed_from_server_if_host_is_not_full_uri()
523 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com:8080', 'ldap.bookstack.com', 8080);
526 public function test_default_ldap_port_used_if_not_in_server_string_and_not_uri()
528 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com', 'ldap.bookstack.com', 389);
531 public function test_forgot_password_routes_inaccessible()
533 $resp = $this->get('/password/email');
534 $this->assertPermissionError($resp);
536 $resp = $this->post('/password/email');
537 $this->assertPermissionError($resp);
539 $resp = $this->get('/password/reset/abc123');
540 $this->assertPermissionError($resp);
542 $resp = $this->post('/password/reset');
543 $this->assertPermissionError($resp);
546 public function test_user_invite_routes_inaccessible()
548 $resp = $this->get('/register/invite/abc123');
549 $this->assertPermissionError($resp);
551 $resp = $this->post('/register/invite/abc123');
552 $this->assertPermissionError($resp);
555 public function test_user_register_routes_inaccessible()
557 $resp = $this->get('/register');
558 $this->assertPermissionError($resp);
560 $resp = $this->post('/register');
561 $this->assertPermissionError($resp);