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_details()
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_create_user_form()
191 $this->asAdmin()->visit('/settings/users/create')
192 ->dontSee('Password')
193 ->type($this->mockUser->name, '#name')
194 ->type($this->mockUser->email, '#email')
196 ->see('The external auth id field is required.')
197 ->type($this->mockUser->name, '#external_auth_id')
199 ->seePageIs('/settings/users')
200 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
203 public function test_user_edit_form()
205 $editUser = $this->getNormalUser();
206 $this->asAdmin()->visit('/settings/users/' . $editUser->id)
208 ->dontSee('Password')
209 ->type('test_auth_id', '#external_auth_id')
211 ->seePageIs('/settings/users')
212 ->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
215 public function test_registration_disabled()
217 $this->visit('/register')
218 ->seePageIs('/login');
221 public function test_non_admins_cannot_change_auth_id()
223 $testUser = $this->getNormalUser();
224 $this->actingAs($testUser)->visit('/settings/users/' . $testUser->id)
225 ->dontSee('External Authentication');
228 public function test_login_maps_roles_and_retains_existing_roles()
230 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
231 $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
232 $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
233 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
234 $this->mockUser->attachRole($existingRole);
237 'services.ldap.user_to_groups' => true,
238 'services.ldap.group_attribute' => 'memberOf',
239 'services.ldap.remove_from_groups' => false,
241 $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
242 $this->mockLdap->shouldReceive('setVersion')->times(1);
243 $this->mockLdap->shouldReceive('setOption')->times(4);
244 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
245 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
246 ->andReturn(['count' => 1, 0 => [
247 'uid' => [$this->mockUser->name],
248 'cn' => [$this->mockUser->name],
249 'dn' => ['dc=test' . config('services.ldap.base_dn')],
250 'mail' => [$this->mockUser->email],
253 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
254 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
257 $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
258 $this->mockEscapes(4);
259 $this->mockExplodes(6);
261 $this->mockUserLogin()->seePageIs('/');
263 $user = User::where('email', $this->mockUser->email)->first();
264 $this->seeInDatabase('role_user', [
265 'user_id' => $user->id,
266 'role_id' => $roleToReceive->id
268 $this->seeInDatabase('role_user', [
269 'user_id' => $user->id,
270 'role_id' => $roleToReceive2->id
272 $this->seeInDatabase('role_user', [
273 'user_id' => $user->id,
274 'role_id' => $existingRole->id
278 public function test_login_maps_roles_and_removes_old_roles_if_set()
280 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
281 $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
282 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
283 $this->mockUser->attachRole($existingRole);
286 'services.ldap.user_to_groups' => true,
287 'services.ldap.group_attribute' => 'memberOf',
288 'services.ldap.remove_from_groups' => true,
290 $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
291 $this->mockLdap->shouldReceive('setVersion')->times(1);
292 $this->mockLdap->shouldReceive('setOption')->times(3);
293 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
294 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
295 ->andReturn(['count' => 1, 0 => [
296 'uid' => [$this->mockUser->name],
297 'cn' => [$this->mockUser->name],
298 'dn' => ['dc=test' . config('services.ldap.base_dn')],
299 'mail' => [$this->mockUser->email],
302 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
305 $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
306 $this->mockEscapes(3);
307 $this->mockExplodes(2);
309 $this->mockUserLogin()->seePageIs('/');
311 $user = User::where('email', $this->mockUser->email)->first();
312 $this->seeInDatabase('role_user', [
313 'user_id' => $user->id,
314 'role_id' => $roleToReceive->id
316 $this->dontSeeInDatabase('role_user', [
317 'user_id' => $user->id,
318 'role_id' => $existingRole->id
322 public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
324 $role = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
325 $this->asAdmin()->visit('/settings/roles/' . $role->id)
329 public function test_login_maps_roles_using_external_auth_ids_if_set()
331 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
332 $roleToNotReceive = factory(Role::class)->create(['name' => 'ldaptester-not-receive', 'display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
335 'services.ldap.user_to_groups' => true,
336 'services.ldap.group_attribute' => 'memberOf',
337 'services.ldap.remove_from_groups' => true,
339 $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
340 $this->mockLdap->shouldReceive('setVersion')->times(1);
341 $this->mockLdap->shouldReceive('setOption')->times(3);
342 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3)
343 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
344 ->andReturn(['count' => 1, 0 => [
345 'uid' => [$this->mockUser->name],
346 'cn' => [$this->mockUser->name],
347 'dn' => ['dc=test' . config('services.ldap.base_dn')],
348 'mail' => [$this->mockUser->email],
351 0 => "cn=ex-auth-a,ou=groups,dc=example,dc=com",
354 $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
355 $this->mockEscapes(3);
356 $this->mockExplodes(2);
358 $this->mockUserLogin()->seePageIs('/');
360 $user = User::where('email', $this->mockUser->email)->first();
361 $this->seeInDatabase('role_user', [
362 'user_id' => $user->id,
363 'role_id' => $roleToReceive->id
365 $this->dontSeeInDatabase('role_user', [
366 'user_id' => $user->id,
367 'role_id' => $roleToNotReceive->id
371 public function test_login_group_mapping_does_not_conflict_with_default_role()
373 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
374 $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
375 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
377 setting()->put('registration-role', $roleToReceive->id);
380 'services.ldap.user_to_groups' => true,
381 'services.ldap.group_attribute' => 'memberOf',
382 'services.ldap.remove_from_groups' => true,
384 $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId);
385 $this->mockLdap->shouldReceive('setVersion')->times(1);
386 $this->mockLdap->shouldReceive('setOption')->times(4);
387 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
388 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
389 ->andReturn(['count' => 1, 0 => [
390 'uid' => [$this->mockUser->name],
391 'cn' => [$this->mockUser->name],
392 'dn' => ['dc=test' . config('services.ldap.base_dn')],
393 'mail' => [$this->mockUser->email],
396 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
397 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
400 $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
401 $this->mockEscapes(4);
402 $this->mockExplodes(6);
404 $this->mockUserLogin()->seePageIs('/');
406 $user = User::where('email', $this->mockUser->email)->first();
407 $this->seeInDatabase('role_user', [
408 'user_id' => $user->id,
409 'role_id' => $roleToReceive->id
411 $this->seeInDatabase('role_user', [
412 'user_id' => $user->id,
413 'role_id' => $roleToReceive2->id
417 public function test_login_uses_specified_display_name_attribute()
420 'services.ldap.display_name_attribute' => 'displayName'
423 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
424 $this->mockLdap->shouldReceive('setVersion')->once();
425 $this->mockLdap->shouldReceive('setOption')->times(2);
426 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
427 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
428 ->andReturn(['count' => 1, 0 => [
429 'uid' => [$this->mockUser->name],
430 'cn' => [$this->mockUser->name],
431 'dn' => ['dc=test' . config('services.ldap.base_dn')],
432 'displayname' => 'displayNameAttribute'
434 $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
435 $this->mockEscapes(2);
437 $this->mockUserLogin()
438 ->seePageIs('/login')->see('Please enter an email to use for this account.');
440 $this->type($this->mockUser->email, '#email')
443 ->see('displayNameAttribute')
444 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
447 public function test_login_uses_default_display_name_attribute_if_specified_not_present()
450 'services.ldap.display_name_attribute' => 'displayName'
453 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
454 $this->mockLdap->shouldReceive('setVersion')->once();
455 $this->mockLdap->shouldReceive('setOption')->times(2);
456 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
457 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
458 ->andReturn(['count' => 1, 0 => [
459 'uid' => [$this->mockUser->name],
460 'cn' => [$this->mockUser->name],
461 'dn' => ['dc=test' . config('services.ldap.base_dn')]
463 $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
464 $this->mockEscapes(2);
466 $this->mockUserLogin()
467 ->seePageIs('/login')->see('Please enter an email to use for this account.');
469 $this->type($this->mockUser->email, '#email')
472 ->see($this->mockUser->name)
473 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => $this->mockUser->name]);
476 protected function checkLdapReceivesCorrectDetails($serverString, $expectedHost, $expectedPort)
479 'services.ldap.server' => $serverString
483 $this->mockLdap->shouldReceive('setVersion')->once();
484 $this->mockLdap->shouldReceive('setOption')->times(1);
485 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)->andReturn(['count' => 1, 0 => [
486 'uid' => [$this->mockUser->name],
487 'cn' => [$this->mockUser->name],
488 'dn' => ['dc=test' . config('services.ldap.base_dn')]
490 $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true);
491 $this->mockEscapes(1);
493 $this->mockLdap->shouldReceive('connect')->once()
494 ->with($expectedHost, $expectedPort)->andReturn($this->resourceId);
495 $this->mockUserLogin();
498 public function test_ldap_port_provided_on_host_if_host_is_full_uri()
500 $hostName = 'ldaps://bookstack:8080';
501 $this->checkLdapReceivesCorrectDetails($hostName, $hostName, 389);
504 public function test_ldap_port_parsed_from_server_if_host_is_not_full_uri()
506 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com:8080', 'ldap.bookstack.com', 8080);
509 public function test_default_ldap_port_used_if_not_in_server_string_and_not_uri()
511 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com', 'ldap.bookstack.com', 389);
514 public function test_forgot_password_routes_inaccessible()
516 $resp = $this->get('/password/email');
517 $this->assertPermissionError($resp);
519 $resp = $this->post('/password/email');
520 $this->assertPermissionError($resp);
522 $resp = $this->get('/password/reset/abc123');
523 $this->assertPermissionError($resp);
525 $resp = $this->post('/password/reset');
526 $this->assertPermissionError($resp);
529 public function test_user_invite_routes_inaccessible()
531 $resp = $this->get('/register/invite/abc123');
532 $this->assertPermissionError($resp);
534 $resp = $this->post('/register/invite/abc123');
535 $this->assertPermissionError($resp);
538 public function test_user_register_routes_inaccessible()
540 $resp = $this->get('/register');
541 $this->assertPermissionError($resp);
543 $resp = $this->post('/register');
544 $this->assertPermissionError($resp);