]> BookStack Code Mirror - bookstack/blob - tests/Auth/LdapTest.php
Added support for Fortran language
[bookstack] / tests / Auth / LdapTest.php
1 <?php namespace Tests;
2
3 use BookStack\Auth\Role;
4 use BookStack\Auth\Access\Ldap;
5 use BookStack\Auth\User;
6 use Mockery\MockInterface;
7
8 class LdapTest extends BrowserKitTest
9 {
10
11     /**
12      * @var MockInterface
13      */
14     protected $mockLdap;
15
16     protected $mockUser;
17     protected $resourceId = 'resource-test';
18
19     public function setUp(): void
20     {
21         parent::setUp();
22         if (!defined('LDAP_OPT_REFERRALS')) define('LDAP_OPT_REFERRALS', 1);
23         app('config')->set([
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,
35         ]);
36         $this->mockLdap = \Mockery::mock(Ldap::class);
37         $this->app[Ldap::class] = $this->mockLdap;
38         $this->mockUser = factory(User::class)->make();
39     }
40
41     protected function mockEscapes($times = 1)
42     {
43         $this->mockLdap->shouldReceive('escape')->times($times)->andReturnUsing(function($val) {
44             return ldap_escape($val);
45         });
46     }
47
48     protected function mockExplodes($times = 1)
49     {
50         $this->mockLdap->shouldReceive('explodeDn')->times($times)->andReturnUsing(function($dn, $withAttrib) {
51             return ldap_explode_dn($dn, $withAttrib);
52         });
53     }
54
55     protected function mockUserLogin()
56     {
57         return $this->visit('/login')
58             ->see('Username')
59             ->type($this->mockUser->name, '#username')
60             ->type($this->mockUser->password, '#password')
61             ->press('Log In');
62     }
63
64     public function test_login()
65     {
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')]
75             ]]);
76         $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
77         $this->mockEscapes(2);
78
79         $this->mockUserLogin()
80             ->seePageIs('/login')->see('Please enter an email to use for this account.');
81
82         $this->type($this->mockUser->email, '#email')
83             ->press('Log In')
84             ->seePageIs('/')
85             ->see($this->mockUser->name)
86             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name]);
87     }
88
89     public function test_email_domain_restriction_active_on_new_ldap_login()
90     {
91         $this->setSettings([
92             'registration-restrict' => 'testing.com'
93         ]);
94
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')]
104             ]]);
105         $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
106         $this->mockEscapes(2);
107
108         $this->mockUserLogin()
109             ->seePageIs('/login')
110             ->see('Please enter an email to use for this account.');
111
112         $email = '[email protected]';
113
114         $this->type($email, '#email')
115             ->press('Log In')
116             ->seePageIs('/login')
117             ->see('That email domain does not have access to this application')
118             ->dontSeeInDatabase('users', ['email' => $email]);
119     }
120
121     public function test_login_works_when_no_uid_provided_by_ldap_server()
122     {
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],
131                 'dn' => $ldapDn,
132                 'mail' => [$this->mockUser->email]
133             ]]);
134         $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true);
135         $this->mockEscapes(1);
136
137         $this->mockUserLogin()
138             ->seePageIs('/')
139             ->see($this->mockUser->name)
140             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
141     }
142
143     public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()
144     {
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],
154                 'dn' => $ldapDn,
155                 'my_custom_id' => ['cooluser456'],
156                 'mail' => [$this->mockUser->email]
157             ]]);
158
159
160         $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true);
161         $this->mockEscapes(1);
162
163         $this->mockUserLogin()
164             ->seePageIs('/')
165             ->see($this->mockUser->name)
166             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);
167     }
168
169     public function test_initial_incorrect_details()
170     {
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')]
180             ]]);
181         $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false);
182         $this->mockEscapes(1);
183
184         $this->mockUserLogin()
185             ->seePageIs('/login')->see('These credentials do not match our records.')
186             ->dontSeeInDatabase('users', ['external_auth_id' => $this->mockUser->name]);
187     }
188
189     public function test_create_user_form()
190     {
191         $this->asAdmin()->visit('/settings/users/create')
192             ->dontSee('Password')
193             ->type($this->mockUser->name, '#name')
194             ->type($this->mockUser->email, '#email')
195             ->press('Save')
196             ->see('The external auth id field is required.')
197             ->type($this->mockUser->name, '#external_auth_id')
198             ->press('Save')
199             ->seePageIs('/settings/users')
200             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
201     }
202
203     public function test_user_edit_form()
204     {
205         $editUser = $this->getNormalUser();
206         $this->asAdmin()->visit('/settings/users/' . $editUser->id)
207             ->see('Edit User')
208             ->dontSee('Password')
209             ->type('test_auth_id', '#external_auth_id')
210             ->press('Save')
211             ->seePageIs('/settings/users')
212             ->seeInDatabase('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
213     }
214
215     public function test_registration_disabled()
216     {
217         $this->visit('/register')
218             ->seePageIs('/login');
219     }
220
221     public function test_non_admins_cannot_change_auth_id()
222     {
223         $testUser = $this->getNormalUser();
224         $this->actingAs($testUser)->visit('/settings/users/' . $testUser->id)
225             ->dontSee('External Authentication');
226     }
227
228     public function test_login_maps_roles_and_retains_existing_roles()
229     {
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);
235
236         app('config')->set([
237             'services.ldap.user_to_groups' => true,
238             'services.ldap.group_attribute' => 'memberOf',
239             'services.ldap.remove_from_groups' => false,
240         ]);
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],
251                 'memberof' => [
252                     'count' => 2,
253                     0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
254                     1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
255                 ]
256             ]]);
257         $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
258         $this->mockEscapes(4);
259         $this->mockExplodes(6);
260
261         $this->mockUserLogin()->seePageIs('/');
262
263         $user = User::where('email', $this->mockUser->email)->first();
264         $this->seeInDatabase('role_user', [
265             'user_id' => $user->id,
266             'role_id' => $roleToReceive->id
267         ]);
268         $this->seeInDatabase('role_user', [
269             'user_id' => $user->id,
270             'role_id' => $roleToReceive2->id
271         ]);
272         $this->seeInDatabase('role_user', [
273             'user_id' => $user->id,
274             'role_id' => $existingRole->id
275         ]);
276     }
277
278     public function test_login_maps_roles_and_removes_old_roles_if_set()
279     {
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);
284
285         app('config')->set([
286             'services.ldap.user_to_groups' => true,
287             'services.ldap.group_attribute' => 'memberOf',
288             'services.ldap.remove_from_groups' => true,
289         ]);
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],
300                 'memberof' => [
301                     'count' => 1,
302                     0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
303                 ]
304             ]]);
305         $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
306         $this->mockEscapes(3);
307         $this->mockExplodes(2);
308
309         $this->mockUserLogin()->seePageIs('/');
310
311         $user = User::where('email', $this->mockUser->email)->first();
312         $this->seeInDatabase('role_user', [
313             'user_id' => $user->id,
314             'role_id' => $roleToReceive->id
315         ]);
316         $this->dontSeeInDatabase('role_user', [
317             'user_id' => $user->id,
318             'role_id' => $existingRole->id
319         ]);
320     }
321
322     public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
323     {
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)
326             ->see('ex-auth-a');
327     }
328
329     public function test_login_maps_roles_using_external_auth_ids_if_set()
330     {
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']);
333
334         app('config')->set([
335             'services.ldap.user_to_groups' => true,
336             'services.ldap.group_attribute' => 'memberOf',
337             'services.ldap.remove_from_groups' => true,
338         ]);
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],
349                 'memberof' => [
350                     'count' => 1,
351                     0 => "cn=ex-auth-a,ou=groups,dc=example,dc=com",
352                 ]
353             ]]);
354         $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
355         $this->mockEscapes(3);
356         $this->mockExplodes(2);
357
358         $this->mockUserLogin()->seePageIs('/');
359
360         $user = User::where('email', $this->mockUser->email)->first();
361         $this->seeInDatabase('role_user', [
362             'user_id' => $user->id,
363             'role_id' => $roleToReceive->id
364         ]);
365         $this->dontSeeInDatabase('role_user', [
366             'user_id' => $user->id,
367             'role_id' => $roleToNotReceive->id
368         ]);
369     }
370
371     public function test_login_group_mapping_does_not_conflict_with_default_role()
372     {
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();
376
377         setting()->put('registration-role', $roleToReceive->id);
378
379         app('config')->set([
380             'services.ldap.user_to_groups' => true,
381             'services.ldap.group_attribute' => 'memberOf',
382             'services.ldap.remove_from_groups' => true,
383         ]);
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],
394                 'memberof' => [
395                     'count' => 2,
396                     0 => "cn=ldaptester,ou=groups,dc=example,dc=com",
397                     1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com",
398                 ]
399             ]]);
400         $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true);
401         $this->mockEscapes(4);
402         $this->mockExplodes(6);
403
404         $this->mockUserLogin()->seePageIs('/');
405
406         $user = User::where('email', $this->mockUser->email)->first();
407         $this->seeInDatabase('role_user', [
408             'user_id' => $user->id,
409             'role_id' => $roleToReceive->id
410         ]);
411         $this->seeInDatabase('role_user', [
412             'user_id' => $user->id,
413             'role_id' => $roleToReceive2->id
414         ]);
415     }
416
417     public function test_login_uses_specified_display_name_attribute()
418     {
419         app('config')->set([
420             'services.ldap.display_name_attribute' => 'displayName'
421         ]);
422
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'
433             ]]);
434         $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
435         $this->mockEscapes(2);
436
437         $this->mockUserLogin()
438             ->seePageIs('/login')->see('Please enter an email to use for this account.');
439
440         $this->type($this->mockUser->email, '#email')
441             ->press('Log In')
442             ->seePageIs('/')
443             ->see('displayNameAttribute')
444             ->seeInDatabase('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
445     }
446
447     public function test_login_uses_default_display_name_attribute_if_specified_not_present()
448     {
449         app('config')->set([
450             'services.ldap.display_name_attribute' => 'displayName'
451         ]);
452
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')]
462             ]]);
463         $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true);
464         $this->mockEscapes(2);
465
466         $this->mockUserLogin()
467             ->seePageIs('/login')->see('Please enter an email to use for this account.');
468
469         $this->type($this->mockUser->email, '#email')
470             ->press('Log In')
471             ->seePageIs('/')
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]);
474     }
475
476     protected function checkLdapReceivesCorrectDetails($serverString, $expectedHost, $expectedPort)
477     {
478         app('config')->set([
479             'services.ldap.server' => $serverString
480         ]);
481
482         // Standard mocks
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')]
489         ]]);
490         $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true);
491         $this->mockEscapes(1);
492
493         $this->mockLdap->shouldReceive('connect')->once()
494             ->with($expectedHost, $expectedPort)->andReturn($this->resourceId);
495         $this->mockUserLogin();
496     }
497
498     public function test_ldap_port_provided_on_host_if_host_is_full_uri()
499     {
500         $hostName = 'ldaps://bookstack:8080';
501         $this->checkLdapReceivesCorrectDetails($hostName, $hostName, 389);
502     }
503
504     public function test_ldap_port_parsed_from_server_if_host_is_not_full_uri()
505     {
506         $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com:8080', 'ldap.bookstack.com', 8080);
507     }
508
509     public function test_default_ldap_port_used_if_not_in_server_string_and_not_uri()
510     {
511         $this->checkLdapReceivesCorrectDetails('ldap.bookstack.com', 'ldap.bookstack.com', 389);
512     }
513
514     public function test_forgot_password_routes_inaccessible()
515     {
516         $resp = $this->get('/password/email');
517         $this->assertPermissionError($resp);
518
519         $resp = $this->post('/password/email');
520         $this->assertPermissionError($resp);
521
522         $resp = $this->get('/password/reset/abc123');
523         $this->assertPermissionError($resp);
524
525         $resp = $this->post('/password/reset');
526         $this->assertPermissionError($resp);
527     }
528
529     public function test_user_invite_routes_inaccessible()
530     {
531         $resp = $this->get('/register/invite/abc123');
532         $this->assertPermissionError($resp);
533
534         $resp = $this->post('/register/invite/abc123');
535         $this->assertPermissionError($resp);
536     }
537
538     public function test_user_register_routes_inaccessible()
539     {
540         $resp = $this->get('/register');
541         $this->assertPermissionError($resp);
542
543         $resp = $this->post('/register');
544         $this->assertPermissionError($resp);
545     }
546 }