]> BookStack Code Mirror - bookstack/blob - tests/Auth/LdapTest.php
Add Perl syntax higlighting to code editor
[bookstack] / tests / Auth / LdapTest.php
1 <?php namespace Tests;
2 use BookStack\Auth\Role;
3 use BookStack\Auth\Access\Ldap;
4 use BookStack\Auth\User;
5 use Mockery\MockInterface;
6
7 class LdapTest extends BrowserKitTest
8 {
9
10     /**
11      * @var MockInterface
12      */
13     protected $mockLdap;
14
15     protected $mockUser;
16     protected $resourceId = 'resource-test';
17
18     public function setUp(): void
19     {
20         parent::setUp();
21         if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
22         app('config')->set([
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',
30         ]);
31         $this->mockLdap = \Mockery::mock(Ldap::class);
32         $this->app[Ldap::class] = $this->mockLdap;
33         $this->mockUser = factory(User::class)->make();
34     }
35
36     protected function mockEscapes($times = 1)
37     {
38         $this->mockLdap->shouldReceive('escape')->times($times)->andReturnUsing(function($val) {
39             return ldap_escape($val);
40         });
41     }
42
43     protected function mockExplodes($times = 1)
44     {
45         $this->mockLdap->shouldReceive('explodeDn')->times($times)->andReturnUsing(function($dn, $withAttrib) {
46             return ldap_explode_dn($dn, $withAttrib);
47         });
48     }
49
50     protected function mockUserLogin()
51     {
52         return $this->visit('/login')
53             ->see('Username')
54             ->type($this->mockUser->name, '#username')
55             ->type($this->mockUser->password, '#password')
56             ->press('Log In');
57     }
58
59     public function test_login()
60     {
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')]
70             ]]);
71         $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
72         $this->mockEscapes(4);
73
74         $this->mockUserLogin()
75             ->seePageIs('/login')->see('Please enter an email to use for this account.');
76
77         $this->type($this->mockUser->email, '#email')
78             ->press('Log In')
79             ->seePageIs('/')
80             ->see($this->mockUser->name)
81             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name]);
82     }
83
84     public function test_login_works_when_no_uid_provided_by_ldap_server()
85     {
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],
94                 'dn' => $ldapDn,
95                 'mail' => [$this->mockUser->email]
96             ]]);
97         $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
98         $this->mockEscapes(2);
99
100         $this->mockUserLogin()
101             ->seePageIs('/')
102             ->see($this->mockUser->name)
103             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
104     }
105
106     public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()
107     {
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],
117                 'dn' => $ldapDn,
118                 'my_custom_id' => ['cooluser456'],
119                 'mail' => [$this->mockUser->email]
120             ]]);
121
122
123         $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
124         $this->mockEscapes(2);
125
126         $this->mockUserLogin()
127             ->seePageIs('/')
128             ->see($this->mockUser->name)
129             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);
130     }
131
132     public function test_initial_incorrect_details()
133     {
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')]
143             ]]);
144         $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true, true, false);
145         $this->mockEscapes(2);
146
147         $this->mockUserLogin()
148             ->seePageIs('/login')->see('These credentials do not match our records.')
149             ->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
150     }
151
152     public function test_create_user_form()
153     {
154         $this->asAdmin()->visit('/settings/users/create')
155             ->dontSee('Password')
156             ->type($this->mockUser->name, '#name')
157             ->type($this->mockUser->email, '#email')
158             ->press('Save')
159             ->see('The external auth id field is required.')
160             ->type($this->mockUser->name, '#external_auth_id')
161             ->press('Save')
162             ->seePageIs('/settings/users')
163             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
164     }
165
166     public function test_user_edit_form()
167     {
168         $editUser = $this->getNormalUser();
169         $this->asAdmin()->visit('/settings/users/' . $editUser->id)
170             ->see('Edit User')
171             ->dontSee('Password')
172             ->type('test_auth_id', '#external_auth_id')
173             ->press('Save')
174             ->seePageIs('/settings/users')
175             ->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
176     }
177
178     public function test_registration_disabled()
179     {
180         $this->visit('/register')
181             ->seePageIs('/login');
182     }
183
184     public function test_non_admins_cannot_change_auth_id()
185     {
186         $testUser = $this->getNormalUser();
187         $this->actingAs($testUser)->visit('/settings/users/' . $testUser->id)
188             ->dontSee('External Authentication');
189     }
190
191     public function test_login_maps_roles_and_retains_existing_roles()
192     {
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);
198
199         app('config')->set([
200             'services.ldap.user_to_groups' => true,
201             'services.ldap.group_attribute' => 'memberOf',
202             'services.ldap.remove_from_groups' => false,
203         ]);
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],
214                 'memberof' => [
215                     'count' => 2,
216                     0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
217                     1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
218                 ]
219             ]]);
220         $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
221         $this->mockEscapes(5);
222         $this->mockExplodes(6);
223
224         $this->mockUserLogin()->seePageIs('/');
225
226         $user = User::where('email', $this->mockUser->email)->first();
227         $this->seeInDatabase('role_user', [
228             'user_id' => $user->id,
229             'role_id' => $roleToReceive->id
230         ]);
231         $this->seeInDatabase('role_user', [
232             'user_id' => $user->id,
233             'role_id' => $roleToReceive2->id
234         ]);
235         $this->seeInDatabase('role_user', [
236             'user_id' => $user->id,
237             'role_id' => $existingRole->id
238         ]);
239     }
240
241     public function test_login_maps_roles_and_removes_old_roles_if_set()
242     {
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);
247
248         app('config')->set([
249             'services.ldap.user_to_groups' => true,
250             'services.ldap.group_attribute' => 'memberOf',
251             'services.ldap.remove_from_groups' => true,
252         ]);
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],
263                 'memberof' => [
264                     'count' => 1,
265                     0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
266                 ]
267             ]]);
268         $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
269         $this->mockEscapes(4);
270         $this->mockExplodes(2);
271
272         $this->mockUserLogin()->seePageIs('/');
273
274         $user = User::where('email', $this->mockUser->email)->first();
275         $this->seeInDatabase('role_user', [
276             'user_id' => $user->id,
277             'role_id' => $roleToReceive->id
278         ]);
279         $this->dontSeeInDatabase('role_user', [
280             'user_id' => $user->id,
281             'role_id' => $existingRole->id
282         ]);
283     }
284
285     public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
286     {
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)
289             ->see('ex-auth-a');
290     }
291
292     public function test_login_maps_roles_using_external_auth_ids_if_set()
293     {
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']);
296
297         app('config')->set([
298             'services.ldap.user_to_groups' => true,
299             'services.ldap.group_attribute' => 'memberOf',
300             'services.ldap.remove_from_groups' => true,
301         ]);
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],
312                 'memberof' => [
313                     'count' => 1,
314                     0 => "cn=ex-auth-a,ou=groups,dc=example,dc=com",
315                 ]
316             ]]);
317         $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
318         $this->mockEscapes(4);
319         $this->mockExplodes(2);
320
321         $this->mockUserLogin()->seePageIs('/');
322
323         $user = User::where('email', $this->mockUser->email)->first();
324         $this->seeInDatabase('role_user', [
325             'user_id' => $user->id,
326             'role_id' => $roleToReceive->id
327         ]);
328         $this->dontSeeInDatabase('role_user', [
329             'user_id' => $user->id,
330             'role_id' => $roleToNotReceive->id
331         ]);
332     }
333
334     public function test_login_group_mapping_does_not_conflict_with_default_role()
335     {
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();
339
340         setting()->put('registration-role', $roleToReceive->id);
341
342         app('config')->set([
343             'services.ldap.user_to_groups' => true,
344             'services.ldap.group_attribute' => 'memberOf',
345             'services.ldap.remove_from_groups' => true,
346         ]);
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],
357                 'memberof' => [
358                     'count' => 2,
359                     0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
360                     1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
361                 ]
362             ]]);
363         $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
364         $this->mockEscapes(5);
365         $this->mockExplodes(6);
366
367         $this->mockUserLogin()->seePageIs('/');
368
369         $user = User::where('email', $this->mockUser->email)->first();
370         $this->seeInDatabase('role_user', [
371             'user_id' => $user->id,
372             'role_id' => $roleToReceive->id
373         ]);
374         $this->seeInDatabase('role_user', [
375             'user_id' => $user->id,
376             'role_id' => $roleToReceive2->id
377         ]);
378     }
379
380     public function test_login_uses_specified_display_name_attribute()
381     {
382         app('config')->set([
383             'services.ldap.display_name_attribute' => 'displayName'
384         ]);
385
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'
396             ]]);
397         $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
398         $this->mockEscapes(4);
399
400         $this->mockUserLogin()
401             ->seePageIs('/login')->see('Please enter an email to use for this account.');
402
403         $this->type($this->mockUser->email, '#email')
404             ->press('Log In')
405             ->seePageIs('/')
406             ->see('displayNameAttribute')
407             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
408     }
409
410     public function test_login_uses_default_display_name_attribute_if_specified_not_present()
411     {
412         app('config')->set([
413             'services.ldap.display_name_attribute' => 'displayName'
414         ]);
415
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')]
425             ]]);
426         $this->mockLdap->shouldReceive('bind')->times(6)->andReturn(true);
427         $this->mockEscapes(4);
428
429         $this->mockUserLogin()
430             ->seePageIs('/login')->see('Please enter an email to use for this account.');
431
432         $this->type($this->mockUser->email, '#email')
433             ->press('Log In')
434             ->seePageIs('/')
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]);
437     }
438
439     protected function checkLdapReceivesCorrectDetails($serverString, $expectedHost, $expectedPort)
440     {
441         app('config')->set([
442             'services.ldap.server' => $serverString
443         ]);
444
445         // Standard mocks
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')]
452         ]]);
453         $this->mockLdap->shouldReceive('bind')->times(3)->andReturn(true);
454         $this->mockEscapes(2);
455
456         $this->mockLdap->shouldReceive('connect')->once()
457             ->with($expectedHost, $expectedPort)->andReturn($this->resourceId);
458         $this->mockUserLogin();
459     }
460
461     public function test_ldap_port_provided_on_host_if_host_is_full_uri()
462     {
463         $hostName = 'ldaps://bookstack:8080';
464         $this->checkLdapReceivesCorrectDetails($hostName, $hostName, 389);
465     }
466
467     public function test_ldap_port_parsed_from_server_if_host_is_not_full_uri()
468     {
469         $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com:8080', 'ldap.bookstack.com', 8080);
470     }
471
472     public function test_default_ldap_port_used_if_not_in_server_string_and_not_uri()
473     {
474         $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com', 'ldap.bookstack.com', 389);
475     }
476 }