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()
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.user_to_groups' => false,
28 'auth.providers.users.driver' => 'ldap',
30 $this->mockLdap = \Mockery::mock(Ldap::class);
31 $this->app[Ldap::class] = $this->mockLdap;
32 $this->mockUser = factory(User::class)->make();
35 protected function mockEscapes($times = 1)
37 $this->mockLdap->shouldReceive('escape')->times($times)->andReturnUsing(function($val) {
38 return ldap_escape($val);
42 protected function mockExplodes($times = 1)
44 $this->mockLdap->shouldReceive('explodeDn')->times($times)->andReturnUsing(function($dn, $withAttrib) {
45 return ldap_explode_dn($dn, $withAttrib);
49 protected function mockUserLogin()
51 return $this->visit('/login')
53 ->type($this->mockUser->name, '#username')
54 ->type($this->mockUser->password, '#password')
58 public function test_login()
60 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
61 $this->mockLdap->shouldReceive('setVersion')->once();
62 $this->mockLdap->shouldReceive('setOption')->times(4);
63 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
64 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
65 ->andReturn(['count' => 1, 0 => [
66 'uid' => [$this->mockUser->name],
67 'cn' => [$this->mockUser->name],
68 'dn' => ['dc=test' . config('services.ldap.base_dn')]
70 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
71 $this->mockEscapes(4);
73 $this->mockUserLogin()
74 ->seePageIs('/login')->see('Please enter an email to use for this account.');
76 $this->type($this->mockUser->email, '#email')
79 ->see($this->mockUser->name)
80 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name]);
83 public function test_login_works_when_no_uid_provided_by_ldap_server()
85 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
86 $this->mockLdap->shouldReceive('setVersion')->once();
87 $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
88 $this->mockLdap->shouldReceive('setOption')->times(2);
89 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
90 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
91 ->andReturn(['count' => 1, 0 => [
92 'cn' => [$this->mockUser->name],
94 'mail' => [$this->mockUser->email]
96 $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
97 $this->mockEscapes(2);
99 $this->mockUserLogin()
101 ->see($this->mockUser->name)
102 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
105 public function test_initial_incorrect_details()
107 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
108 $this->mockLdap->shouldReceive('setVersion')->once();
109 $this->mockLdap->shouldReceive('setOption')->times(2);
110 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
111 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
112 ->andReturn(['count' => 1, 0 => [
113 'uid' => [$this->mockUser->name],
114 'cn' => [$this->mockUser->name],
115 'dn' => ['dc=test' . config('services.ldap.base_dn')]
117 $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
118 $this->mockEscapes(2);
120 $this->mockUserLogin()
121 ->seePageIs('/login')->see('These credentials do not match our records.')
122 ->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
125 public function test_create_user_form()
127 $this->asAdmin()->visit('/settings/users/create')
128 ->dontSee('Password')
129 ->type($this->mockUser->name, '#name')
130 ->type($this->mockUser->email, '#email')
132 ->see('The external auth id field is required.')
133 ->type($this->mockUser->name, '#external_auth_id')
135 ->seePageIs('/settings/users')
136 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
139 public function test_user_edit_form()
141 $editUser = $this->getNormalUser();
142 $this->asAdmin()->visit('/settings/users/' . $editUser->id)
144 ->dontSee('Password')
145 ->type('test_auth_id', '#external_auth_id')
147 ->seePageIs('/settings/users')
148 ->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
151 public function test_registration_disabled()
153 $this->visit('/register')
154 ->seePageIs('/login');
157 public function test_non_admins_cannot_change_auth_id()
159 $testUser = $this->getNormalUser();
160 $this->actingAs($testUser)->visit('/settings/users/' . $testUser->id)
161 ->dontSee('External Authentication');
164 public function test_login_maps_roles_and_retains_existing_roles()
166 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
167 $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
168 $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
169 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
170 $this->mockUser->attachRole($existingRole);
173 'services.ldap.user_to_groups' => true,
174 'services.ldap.group_attribute' => 'memberOf',
175 'services.ldap.remove_from_groups' => false,
177 $this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
178 $this->mockLdap->shouldReceive('setVersion')->times(2);
179 $this->mockLdap->shouldReceive('setOption')->times(5);
180 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(5)
181 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
182 ->andReturn(['count' => 1, 0 => [
183 'uid' => [$this->mockUser->name],
184 'cn' => [$this->mockUser->name],
185 'dn' => ['dc=test' . config('services.ldap.base_dn')],
186 'mail' => [$this->mockUser->email],
189 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
190 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
193 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
194 $this->mockEscapes(5);
195 $this->mockExplodes(6);
197 $this->mockUserLogin()->seePageIs('/');
199 $user = User::where('email', $this->mockUser->email)->first();
200 $this->seeInDatabase('role_user', [
201 'user_id' => $user->id,
202 'role_id' => $roleToReceive->id
204 $this->seeInDatabase('role_user', [
205 'user_id' => $user->id,
206 'role_id' => $roleToReceive2->id
208 $this->seeInDatabase('role_user', [
209 'user_id' => $user->id,
210 'role_id' => $existingRole->id
214 public function test_login_maps_roles_and_removes_old_roles_if_set()
216 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
217 $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']);
218 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
219 $this->mockUser->attachRole($existingRole);
222 'services.ldap.user_to_groups' => true,
223 'services.ldap.group_attribute' => 'memberOf',
224 'services.ldap.remove_from_groups' => true,
226 $this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
227 $this->mockLdap->shouldReceive('setVersion')->times(2);
228 $this->mockLdap->shouldReceive('setOption')->times(4);
229 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
230 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
231 ->andReturn(['count' => 1, 0 => [
232 'uid' => [$this->mockUser->name],
233 'cn' => [$this->mockUser->name],
234 'dn' => ['dc=test' . config('services.ldap.base_dn')],
235 'mail' => [$this->mockUser->email],
238 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
241 $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
242 $this->mockEscapes(4);
243 $this->mockExplodes(2);
245 $this->mockUserLogin()->seePageIs('/');
247 $user = User::where('email', $this->mockUser->email)->first();
248 $this->seeInDatabase('role_user', [
249 'user_id' => $user->id,
250 'role_id' => $roleToReceive->id
252 $this->dontSeeInDatabase('role_user', [
253 'user_id' => $user->id,
254 'role_id' => $existingRole->id
258 public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
260 $role = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
261 $this->asAdmin()->visit('/settings/roles/' . $role->id)
265 public function test_login_maps_roles_using_external_auth_ids_if_set()
267 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
268 $roleToNotReceive = factory(Role::class)->create(['name' => 'ldaptester-not-receive', 'display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
271 'services.ldap.user_to_groups' => true,
272 'services.ldap.group_attribute' => 'memberOf',
273 'services.ldap.remove_from_groups' => true,
275 $this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
276 $this->mockLdap->shouldReceive('setVersion')->times(2);
277 $this->mockLdap->shouldReceive('setOption')->times(4);
278 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
279 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
280 ->andReturn(['count' => 1, 0 => [
281 'uid' => [$this->mockUser->name],
282 'cn' => [$this->mockUser->name],
283 'dn' => ['dc=test' . config('services.ldap.base_dn')],
284 'mail' => [$this->mockUser->email],
287 0 => "cn=ex-auth-a,ou=groups,dc=example,dc=com",
290 $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
291 $this->mockEscapes(4);
292 $this->mockExplodes(2);
294 $this->mockUserLogin()->seePageIs('/');
296 $user = User::where('email', $this->mockUser->email)->first();
297 $this->seeInDatabase('role_user', [
298 'user_id' => $user->id,
299 'role_id' => $roleToReceive->id
301 $this->dontSeeInDatabase('role_user', [
302 'user_id' => $user->id,
303 'role_id' => $roleToNotReceive->id
307 public function test_login_group_mapping_does_not_conflict_with_default_role()
309 $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']);
310 $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']);
311 $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
313 setting()->put('registration-role', $roleToReceive->id);
316 'services.ldap.user_to_groups' => true,
317 'services.ldap.group_attribute' => 'memberOf',
318 'services.ldap.remove_from_groups' => true,
320 $this->mockLdap->shouldReceive('connect')->times(2)->andReturn($this->resourceId);
321 $this->mockLdap->shouldReceive('setVersion')->times(2);
322 $this->mockLdap->shouldReceive('setOption')->times(5);
323 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(5)
324 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
325 ->andReturn(['count' => 1, 0 => [
326 'uid' => [$this->mockUser->name],
327 'cn' => [$this->mockUser->name],
328 'dn' => ['dc=test' . config('services.ldap.base_dn')],
329 'mail' => [$this->mockUser->email],
332 0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
333 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
336 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
337 $this->mockEscapes(5);
338 $this->mockExplodes(6);
340 $this->mockUserLogin()->seePageIs('/');
342 $user = User::where('email', $this->mockUser->email)->first();
343 $this->seeInDatabase('role_user', [
344 'user_id' => $user->id,
345 'role_id' => $roleToReceive->id
347 $this->seeInDatabase('role_user', [
348 'user_id' => $user->id,
349 'role_id' => $roleToReceive2->id
353 public function test_login_uses_specified_display_name_attribute()
356 'services.ldap.display_name_attribute' => 'displayName'
359 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
360 $this->mockLdap->shouldReceive('setVersion')->once();
361 $this->mockLdap->shouldReceive('setOption')->times(4);
362 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
363 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
364 ->andReturn(['count' => 1, 0 => [
365 'uid' => [$this->mockUser->name],
366 'cn' => [$this->mockUser->name],
367 'dn' => ['dc=test' . config('services.ldap.base_dn')],
368 'displayName' => 'displayNameAttribute'
370 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
371 $this->mockEscapes(4);
373 $this->mockUserLogin()
374 ->seePageIs('/login')->see('Please enter an email to use for this account.');
376 $this->type($this->mockUser->email, '#email')
379 ->see('displayNameAttribute')
380 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
383 public function test_login_uses_default_display_name_attribute_if_specified_not_present()
386 'services.ldap.display_name_attribute' => 'displayName'
389 $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId);
390 $this->mockLdap->shouldReceive('setVersion')->once();
391 $this->mockLdap->shouldReceive('setOption')->times(4);
392 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4)
393 ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
394 ->andReturn(['count' => 1, 0 => [
395 'uid' => [$this->mockUser->name],
396 'cn' => [$this->mockUser->name],
397 'dn' => ['dc=test' . config('services.ldap.base_dn')]
399 $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
400 $this->mockEscapes(4);
402 $this->mockUserLogin()
403 ->seePageIs('/login')->see('Please enter an email to use for this account.');
405 $this->type($this->mockUser->email, '#email')
408 ->see($this->mockUser->name)
409 ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => $this->mockUser->name]);
412 protected function checkLdapReceivesCorrectDetails($serverString, $expectedHost, $expectedPort)
415 'services.ldap.server' => $serverString
419 $this->mockLdap->shouldReceive('setVersion')->once();
420 $this->mockLdap->shouldReceive('setOption')->times(2);
421 $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)->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(3)->andReturn(true);
427 $this->mockEscapes(2);
429 $this->mockLdap->shouldReceive('connect')->once()
430 ->with($expectedHost, $expectedPort)->andReturn($this->resourceId);
431 $this->mockUserLogin();
434 public function test_ldap_port_provided_on_host_if_host_is_full_uri()
436 $hostName = 'ldaps://bookstack:8080';
437 $this->checkLdapReceivesCorrectDetails($hostName, $hostName, 389);
440 public function test_ldap_port_parsed_from_server_if_host_is_not_full_uri()
442 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com:8080', 'ldap.bookstack.com', 8080);
445 public function test_default_ldap_port_used_if_not_in_server_string_and_not_uri()
447 $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com', 'ldap.bookstack.com', 389);