2 use BookStack\Auth\Role;
3 use BookStack\Auth\Access\Ldap;
4 use BookStack\Auth\User;
5 use Mockery\MockInterface;
7 class LdapTest extends BrowserKitTest
16 protected $resourceId = 'resource-test';
18 public function setUp(): void
21 if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
23 'auth.method' => 'ldap',
24 'services.ldap.base_dn' => 'dc=ldap,dc=local',
25 'services.ldap.email_attribute' => 'mail',
26 'services.ldap.display_name_attribute' => 'cn',
27 'services.ldap.id_attribute' => 'uid',
28 'services.ldap.user_to_groups' => false,
29 'auth.providers.users.driver' => 'ldap',
31 $this->mockLdap = \Mockery::mock(Ldap::class);
32 $this->app[Ldap::class] = $this->mockLdap;
33 $this->mockUser = factory(User::class)->make();
36 protected function mockEscapes($times = 1)
38 $this->mockLdap->shouldReceive('escape')->times($times)->andReturnUsing(function($val) {
39 return ldap_escape($val);
43 protected function mockExplodes($times = 1)
45 $this->mockLdap->shouldReceive('explodeDn')->times($times)->andReturnUsing(function($dn, $withAttrib) {
46 return ldap_explode_dn($dn, $withAttrib);
50 protected function mockUserLogin()
52 return $this->visit('/login')
54 ->type($this->mockUser->name, '#username')
55 ->type($this->mockUser->password, '#password')
59 public function test_login()
61 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
62 $this->mockLdap->shouldReceive('setVersion')->once();
63 $this->mockLdap->shouldReceive('setOption')->times(4);
64 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
65 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
66 ->andReturn(['count' => 1, 0 => [
67 'uid' => [$this->mockUser->name],
68 'cn' => [$this->mockUser->name],
69 'dn' => ['dc=test' . config('services.ldap.base_dn')]
71 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
72 $this->mockEscapes(4);
74 $this->mockUserLogin()
75 ->seePageIs('/login')->see('Please enter an email to use for this account.');
77 $this->type($this->mockUser->email, '#email')
80 ->see($this->mockUser->name)
81 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name]);
84 public function test_login_works_when_no_uid_provided_by_ldap_server()
86 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
87 $this->mockLdap->shouldReceive('setVersion')->once();
88 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
89 $this->mockLdap->shouldReceive('setOption')->times(2);
90 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
91 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
92 ->andReturn(['count' => 1, 0 => [
93 'cn' => [$this->mockUser->name],
95 'mail' => [$this->mockUser->email]
97 $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
98 $this->mockEscapes(2);
100 $this->mockUserLogin()
102 ->see($this->mockUser->name)
103 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
106 public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()
108 config()->set(['services.ldap.id_attribute' => 'my_custom_id']);
109 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
110 $this->mockLdap->shouldReceive('setVersion')->once();
111 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
112 $this->mockLdap->shouldReceive('setOption')->times(2);
113 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
114 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
115 ->andReturn(['count' => 1, 0 => [
116 'cn' => [$this->mockUser->name],
118 'my_custom_id' => ['cooluser456'],
119 'mail' => [$this->mockUser->email]
123 $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
124 $this->mockEscapes(2);
126 $this->mockUserLogin()
128 ->see($this->mockUser->name)
129 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);
132 public function test_initial_incorrect_details()
134 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
135 $this->mockLdap->shouldReceive('setVersion')->once();
136 $this->mockLdap->shouldReceive('setOption')->times(2);
137 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
138 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
139 ->andReturn(['count' => 1, 0 => [
140 'uid' => [$this->mockUser->name],
141 'cn' => [$this->mockUser->name],
142 'dn' => ['dc=test' . config('services.ldap.base_dn')]
144 $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
145 $this->mockEscapes(2);
147 $this->mockUserLogin()
148 ->seePageIs('/login')->see('These credentials do not match our records.')
149 ->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
152 public function test_create_user_form()
154 $this->asAdmin()->visit('/settings/users/create')
155 ->dontSee('Password')
156 ->type($this->mockUser->name, '#name')
157 ->type($this->mockUser->email, '#email')
159 ->see('The external auth id field is required.')
160 ->type($this->mockUser->name, '#external_auth_id')
162 ->seePageIs('/settings/users')
163 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
166 public function test_user_edit_form()
168 $editUser = $this->getNormalUser();
169 $this->asAdmin()->visit('/settings/users/' . $editUser->id)
171 ->dontSee('Password')
172 ->type('test_auth_id', '#external_auth_id')
174 ->seePageIs('/settings/users')
175 ->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
178 public function test_registration_disabled()
180 $this->visit('/register')
181 ->seePageIs('/login');
184 public function test_non_admins_cannot_change_auth_id()
186 $testUser = $this->getNormalUser();
187 $this->actingAs($testUser)->visit('/settings/users/' . $testUser->id)
188 ->dontSee('External Authentication');
191 public function test_login_maps_roles_and_retains_existing_roles()
193 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
194 $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
195 $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
196 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
197 $this->mockUser->attachRole($existingRole);
200 'services.ldap.user_to_groups' => true,
201 'services.ldap.group_attribute' => 'memberOf',
202 'services.ldap.remove_from_groups' => false,
204 $this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
205 $this->mockLdap->shouldReceive('setVersion')->times(2);
206 $this->mockLdap->shouldReceive('setOption')->times(5);
207 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(5)
208 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
209 ->andReturn(['count' => 1, 0 => [
210 'uid' => [$this->mockUser->name],
211 'cn' => [$this->mockUser->name],
212 'dn' => ['dc=test' . config('services.ldap.base_dn')],
213 'mail' => [$this->mockUser->email],
216 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
217 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
220 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
221 $this->mockEscapes(5);
222 $this->mockExplodes(6);
224 $this->mockUserLogin()->seePageIs('/');
226 $user = User::where('email', $this->mockUser->email)->first();
227 $this->seeInDatabase('role_user', [
228 'user_id' => $user->id,
229 'role_id' => $roleToReceive->id
231 $this->seeInDatabase('role_user', [
232 'user_id' => $user->id,
233 'role_id' => $roleToReceive2->id
235 $this->seeInDatabase('role_user', [
236 'user_id' => $user->id,
237 'role_id' => $existingRole->id
241 public function test_login_maps_roles_and_removes_old_roles_if_set()
243 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
244 $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
245 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
246 $this->mockUser->attachRole($existingRole);
249 'services.ldap.user_to_groups' => true,
250 'services.ldap.group_attribute' => 'memberOf',
251 'services.ldap.remove_from_groups' => true,
253 $this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
254 $this->mockLdap->shouldReceive('setVersion')->times(2);
255 $this->mockLdap->shouldReceive('setOption')->times(4);
256 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
257 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
258 ->andReturn(['count' => 1, 0 => [
259 'uid' => [$this->mockUser->name],
260 'cn' => [$this->mockUser->name],
261 'dn' => ['dc=test' . config('services.ldap.base_dn')],
262 'mail' => [$this->mockUser->email],
265 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
268 $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
269 $this->mockEscapes(4);
270 $this->mockExplodes(2);
272 $this->mockUserLogin()->seePageIs('/');
274 $user = User::where('email', $this->mockUser->email)->first();
275 $this->seeInDatabase('role_user', [
276 'user_id' => $user->id,
277 'role_id' => $roleToReceive->id
279 $this->dontSeeInDatabase('role_user', [
280 'user_id' => $user->id,
281 'role_id' => $existingRole->id
285 public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
287 $role = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
288 $this->asAdmin()->visit('/settings/roles/' . $role->id)
292 public function test_login_maps_roles_using_external_auth_ids_if_set()
294 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
295 $roleToNotReceive = factory(Role::class)->create(['name' => 'ldaptester-not-receive', 'display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
298 'services.ldap.user_to_groups' => true,
299 'services.ldap.group_attribute' => 'memberOf',
300 'services.ldap.remove_from_groups' => true,
302 $this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
303 $this->mockLdap->shouldReceive('setVersion')->times(2);
304 $this->mockLdap->shouldReceive('setOption')->times(4);
305 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
306 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
307 ->andReturn(['count' => 1, 0 => [
308 'uid' => [$this->mockUser->name],
309 'cn' => [$this->mockUser->name],
310 'dn' => ['dc=test' . config('services.ldap.base_dn')],
311 'mail' => [$this->mockUser->email],
314 0 => "cn=ex-auth-a,ou=groups,dc=example,dc=com",
317 $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
318 $this->mockEscapes(4);
319 $this->mockExplodes(2);
321 $this->mockUserLogin()->seePageIs('/');
323 $user = User::where('email', $this->mockUser->email)->first();
324 $this->seeInDatabase('role_user', [
325 'user_id' => $user->id,
326 'role_id' => $roleToReceive->id
328 $this->dontSeeInDatabase('role_user', [
329 'user_id' => $user->id,
330 'role_id' => $roleToNotReceive->id
334 public function test_login_group_mapping_does_not_conflict_with_default_role()
336 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
337 $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
338 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
340 setting()->put('registration-role', $roleToReceive->id);
343 'services.ldap.user_to_groups' => true,
344 'services.ldap.group_attribute' => 'memberOf',
345 'services.ldap.remove_from_groups' => true,
347 $this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
348 $this->mockLdap->shouldReceive('setVersion')->times(2);
349 $this->mockLdap->shouldReceive('setOption')->times(5);
350 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(5)
351 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
352 ->andReturn(['count' => 1, 0 => [
353 'uid' => [$this->mockUser->name],
354 'cn' => [$this->mockUser->name],
355 'dn' => ['dc=test' . config('services.ldap.base_dn')],
356 'mail' => [$this->mockUser->email],
359 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
360 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
363 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
364 $this->mockEscapes(5);
365 $this->mockExplodes(6);
367 $this->mockUserLogin()->seePageIs('/');
369 $user = User::where('email', $this->mockUser->email)->first();
370 $this->seeInDatabase('role_user', [
371 'user_id' => $user->id,
372 'role_id' => $roleToReceive->id
374 $this->seeInDatabase('role_user', [
375 'user_id' => $user->id,
376 'role_id' => $roleToReceive2->id
380 public function test_login_uses_specified_display_name_attribute()
383 'services.ldap.display_name_attribute' => 'displayName'
386 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
387 $this->mockLdap->shouldReceive('setVersion')->once();
388 $this->mockLdap->shouldReceive('setOption')->times(4);
389 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
390 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
391 ->andReturn(['count' => 1, 0 => [
392 'uid' => [$this->mockUser->name],
393 'cn' => [$this->mockUser->name],
394 'dn' => ['dc=test' . config('services.ldap.base_dn')],
395 'displayname' => 'displayNameAttribute'
397 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
398 $this->mockEscapes(4);
400 $this->mockUserLogin()
401 ->seePageIs('/login')->see('Please enter an email to use for this account.');
403 $this->type($this->mockUser->email, '#email')
406 ->see('displayNameAttribute')
407 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
410 public function test_login_uses_default_display_name_attribute_if_specified_not_present()
413 'services.ldap.display_name_attribute' => 'displayName'
416 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
417 $this->mockLdap->shouldReceive('setVersion')->once();
418 $this->mockLdap->shouldReceive('setOption')->times(4);
419 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
420 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
421 ->andReturn(['count' => 1, 0 => [
422 'uid' => [$this->mockUser->name],
423 'cn' => [$this->mockUser->name],
424 'dn' => ['dc=test' . config('services.ldap.base_dn')]
426 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
427 $this->mockEscapes(4);
429 $this->mockUserLogin()
430 ->seePageIs('/login')->see('Please enter an email to use for this account.');
432 $this->type($this->mockUser->email, '#email')
435 ->see($this->mockUser->name)
436 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => $this->mockUser->name]);
439 protected function checkLdapReceivesCorrectDetails($serverString, $expectedHost, $expectedPort)
442 'services.ldap.server' => $serverString
446 $this->mockLdap->shouldReceive('setVersion')->once();
447 $this->mockLdap->shouldReceive('setOption')->times(2);
448 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)->andReturn(['count' => 1, 0 => [
449 'uid' => [$this->mockUser->name],
450 'cn' => [$this->mockUser->name],
451 'dn' => ['dc=test' . config('services.ldap.base_dn')]
453 $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
454 $this->mockEscapes(2);
456 $this->mockLdap->shouldReceive('connect')->once()
457 ->with($expectedHost, $expectedPort)->andReturn($this->resourceId);
458 $this->mockUserLogin();
461 public function test_ldap_port_provided_on_host_if_host_is_full_uri()
463 $hostName = 'ldaps://bookstack:8080';
464 $this->checkLdapReceivesCorrectDetails($hostName, $hostName, 389);
467 public function test_ldap_port_parsed_from_server_if_host_is_not_full_uri()
469 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com:8080', 'ldap.bookstack.com', 8080);
472 public function test_default_ldap_port_used_if_not_in_server_string_and_not_uri()
474 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com', 'ldap.bookstack.com', 389);