]> BookStack Code Mirror - bookstack/blob - tests/Auth/LdapTest.php
Updated translator & dependency attribution before release v25.05.1
[bookstack] / tests / Auth / LdapTest.php
1 <?php
2
3 namespace Tests\Auth;
4
5 use BookStack\Access\Ldap;
6 use BookStack\Access\LdapService;
7 use BookStack\Exceptions\LdapException;
8 use BookStack\Users\Models\Role;
9 use BookStack\Users\Models\User;
10 use Illuminate\Testing\TestResponse;
11 use Mockery\MockInterface;
12 use Tests\TestCase;
13
14 class LdapTest extends TestCase
15 {
16     protected MockInterface $mockLdap;
17
18     protected User $mockUser;
19     protected string $resourceId = 'resource-test';
20
21     protected function setUp(): void
22     {
23         parent::setUp();
24         if (!defined('LDAP_OPT_REFERRALS')) {
25             define('LDAP_OPT_REFERRALS', 1);
26         }
27         config()->set([
28             'auth.method'                          => 'ldap',
29             'auth.defaults.guard'                  => 'ldap',
30             'services.ldap.base_dn'                => 'dc=ldap,dc=local',
31             'services.ldap.email_attribute'        => 'mail',
32             'services.ldap.display_name_attribute' => 'cn',
33             'services.ldap.id_attribute'           => 'uid',
34             'services.ldap.user_to_groups'         => false,
35             'services.ldap.version'                => '3',
36             'services.ldap.user_filter'            => '(&(uid={user}))',
37             'services.ldap.follow_referrals'       => false,
38             'services.ldap.tls_insecure'           => false,
39             'services.ldap.tls_ca_cert'            => false,
40             'services.ldap.thumbnail_attribute'    => null,
41         ]);
42         $this->mockLdap = $this->mock(Ldap::class);
43         $this->mockUser = User::factory()->make();
44     }
45
46     protected function runFailedAuthLogin()
47     {
48         $this->commonLdapMocks(1, 1, 1, 1, 1);
49         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
50             ->andReturn(['count' => 0]);
51         $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);
52     }
53
54     protected function mockEscapes($times = 1)
55     {
56         $this->mockLdap->shouldReceive('escape')->times($times)->andReturnUsing(function ($val) {
57             return ldap_escape($val);
58         });
59     }
60
61     protected function mockExplodes($times = 1)
62     {
63         $this->mockLdap->shouldReceive('explodeDn')->times($times)->andReturnUsing(function ($dn, $withAttrib) {
64             return ldap_explode_dn($dn, $withAttrib);
65         });
66     }
67
68     protected function mockUserLogin(?string $email = null): TestResponse
69     {
70         return $this->post('/login', [
71             'username' => $this->mockUser->name,
72             'password' => $this->mockUser->password,
73         ] + ($email ? ['email' => $email] : []));
74     }
75
76     /**
77      * Set LDAP method mocks for things we commonly call without altering.
78      */
79     protected function commonLdapMocks(int $connects = 1, int $versions = 1, int $options = 2, int $binds = 4, int $escapes = 2, int $explodes = 0, int $groups = 0)
80     {
81         $this->mockLdap->shouldReceive('connect')->times($connects)->andReturn($this->resourceId);
82         $this->mockLdap->shouldReceive('setVersion')->times($versions);
83         $this->mockLdap->shouldReceive('setOption')->times($options);
84         $this->mockLdap->shouldReceive('bind')->times($binds)->andReturn(true);
85         $this->mockEscapes($escapes);
86         $this->mockExplodes($explodes);
87         $this->mockGroupLookups($groups);
88     }
89
90     protected function mockGroupLookups(int $times = 1): void
91     {
92         $this->mockLdap->shouldReceive('read')->times($times)->andReturn(['count' => 0]);
93         $this->mockLdap->shouldReceive('getEntries')->times($times)->andReturn(['count' => 0]);
94     }
95
96     public function test_login()
97     {
98         $this->commonLdapMocks(1, 1, 2, 4, 2);
99         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
100             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
101             ->andReturn(['count' => 1, 0 => [
102                 'uid' => [$this->mockUser->name],
103                 'cn'  => [$this->mockUser->name],
104                 'dn'  => 'dc=test' . config('services.ldap.base_dn'),
105             ]]);
106
107         $resp = $this->mockUserLogin();
108         $resp->assertRedirect('/login');
109         $resp = $this->followRedirects($resp);
110         $resp->assertSee('Please enter an email to use for this account.');
111         $resp->assertSee($this->mockUser->name);
112
113         $resp = $this->followingRedirects()->mockUserLogin($this->mockUser->email);
114         $this->withHtml($resp)->assertElementExists('#home-default');
115         $resp->assertSee($this->mockUser->name);
116         $this->assertDatabaseHas('users', [
117             'email'            => $this->mockUser->email,
118             'email_confirmed'  => false,
119             'external_auth_id' => $this->mockUser->name,
120         ]);
121     }
122
123     public function test_email_domain_restriction_active_on_new_ldap_login()
124     {
125         $this->setSettings([
126             'registration-restrict' => 'testing.com',
127         ]);
128
129         $this->commonLdapMocks(1, 1, 2, 4, 2);
130         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
131             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
132             ->andReturn(['count' => 1, 0 => [
133                 'uid' => [$this->mockUser->name],
134                 'cn'  => [$this->mockUser->name],
135                 'dn'  => 'dc=test' . config('services.ldap.base_dn'),
136             ]]);
137
138         $resp = $this->mockUserLogin();
139         $resp->assertRedirect('/login');
140         $this->followRedirects($resp)->assertSee('Please enter an email to use for this account.');
141
142         $email = 'tester@invaliddomain.com';
143         $resp = $this->mockUserLogin($email);
144         $resp->assertRedirect('/login');
145         $this->followRedirects($resp)->assertSee('That email domain does not have access to this application');
146
147         $this->assertDatabaseMissing('users', ['email' => $email]);
148     }
149
150     public function test_login_works_when_no_uid_provided_by_ldap_server()
151     {
152         $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
153
154         $this->commonLdapMocks(1, 1, 1, 2, 1);
155         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
156             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
157             ->andReturn(['count' => 1, 0 => [
158                 'cn'   => [$this->mockUser->name],
159                 'dn'   => $ldapDn,
160                 'mail' => [$this->mockUser->email],
161             ]]);
162
163         $resp = $this->mockUserLogin();
164         $resp->assertRedirect('/');
165         $this->followRedirects($resp)->assertSee($this->mockUser->name);
166         $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
167     }
168
169     public function test_login_works_when_ldap_server_does_not_provide_a_cn_value()
170     {
171         $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
172
173         $this->commonLdapMocks(1, 1, 1, 2, 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                 'dn'   => $ldapDn,
178                 'mail' => [$this->mockUser->email],
179             ]]);
180
181         $resp = $this->mockUserLogin();
182         $resp->assertRedirect('/');
183         $this->assertDatabaseHas('users', [
184             'name' => 'test-user',
185             'email' => $this->mockUser->email,
186         ]);
187     }
188
189     public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()
190     {
191         config()->set(['services.ldap.id_attribute' => 'my_custom_id']);
192
193         $this->commonLdapMocks(1, 1, 1, 2, 1);
194         $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
195         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
196             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
197             ->andReturn(['count' => 1, 0 => [
198                 'cn'           => [$this->mockUser->name],
199                 'dn'           => $ldapDn,
200                 'my_custom_id' => ['cooluser456'],
201                 'mail'         => [$this->mockUser->email],
202             ]]);
203
204         $resp = $this->mockUserLogin();
205         $resp->assertRedirect('/');
206         $this->followRedirects($resp)->assertSee($this->mockUser->name);
207         $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);
208     }
209
210     public function test_user_filter_default_placeholder_format()
211     {
212         config()->set('services.ldap.user_filter', '(&(uid={user}))');
213         $this->mockUser->name = 'barryldapuser';
214         $expectedFilter = '(&(uid=\62\61\72\72\79\6c\64\61\70\75\73\65\72))';
215
216         $this->commonLdapMocks(1, 1, 1, 1, 1);
217         $this->mockLdap->shouldReceive('searchAndGetEntries')
218             ->once()
219             ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \Mockery::type('array'))
220             ->andReturn(['count' => 0, 0 => []]);
221
222         $resp = $this->mockUserLogin();
223         $resp->assertRedirect('/login');
224     }
225
226     public function test_user_filter_old_placeholder_format()
227     {
228         config()->set('services.ldap.user_filter', '(&(username=${user}))');
229         $this->mockUser->name = 'barryldapuser';
230         $expectedFilter = '(&(username=\62\61\72\72\79\6c\64\61\70\75\73\65\72))';
231
232         $this->commonLdapMocks(1, 1, 1, 1, 1);
233         $this->mockLdap->shouldReceive('searchAndGetEntries')
234             ->once()
235             ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \Mockery::type('array'))
236             ->andReturn(['count' => 0, 0 => []]);
237
238         $resp = $this->mockUserLogin();
239         $resp->assertRedirect('/login');
240     }
241
242     public function test_initial_incorrect_credentials()
243     {
244         $this->commonLdapMocks(1, 1, 1, 0, 1);
245         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
246             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
247             ->andReturn(['count' => 1, 0 => [
248                 'uid' => [$this->mockUser->name],
249                 'cn'  => [$this->mockUser->name],
250                 'dn'  => 'dc=test' . config('services.ldap.base_dn'),
251             ]]);
252         $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false);
253
254         $resp = $this->mockUserLogin();
255         $resp->assertRedirect('/login');
256         $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
257         $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
258     }
259
260     public function test_login_not_found_username()
261     {
262         $this->commonLdapMocks(1, 1, 1, 1, 1);
263         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
264             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
265             ->andReturn(['count' => 0]);
266
267         $resp = $this->mockUserLogin();
268         $resp->assertRedirect('/login');
269         $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
270         $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
271     }
272
273     public function test_create_user_form()
274     {
275         $userForm = $this->asAdmin()->get('/settings/users/create');
276         $userForm->assertDontSee('Password');
277
278         $save = $this->post('/settings/users/create', [
279             'name'  => $this->mockUser->name,
280             'email' => $this->mockUser->email,
281         ]);
282         $save->assertSessionHasErrors(['external_auth_id' => 'The external auth id field is required.']);
283
284         $save = $this->post('/settings/users/create', [
285             'name'             => $this->mockUser->name,
286             'email'            => $this->mockUser->email,
287             'external_auth_id' => $this->mockUser->name,
288         ]);
289         $save->assertRedirect('/settings/users');
290         $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
291     }
292
293     public function test_user_edit_form()
294     {
295         $editUser = $this->users->viewer();
296         $editPage = $this->asAdmin()->get("/settings/users/{$editUser->id}");
297         $editPage->assertSee('Edit User');
298         $editPage->assertDontSee('Password');
299
300         $update = $this->put("/settings/users/{$editUser->id}", [
301             'name'             => $editUser->name,
302             'email'            => $editUser->email,
303             'external_auth_id' => 'test_auth_id',
304         ]);
305         $update->assertRedirect('/settings/users');
306         $this->assertDatabaseHas('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);
307     }
308
309     public function test_registration_disabled()
310     {
311         $resp = $this->followingRedirects()->get('/register');
312         $this->withHtml($resp)->assertElementContains('#content', 'Log In');
313     }
314
315     public function test_non_admins_cannot_change_auth_id()
316     {
317         $testUser = $this->users->viewer();
318         $this->actingAs($testUser)
319             ->get('/settings/users/' . $testUser->id)
320             ->assertDontSee('External Authentication');
321     }
322
323     public function test_login_maps_roles_and_retains_existing_roles()
324     {
325         $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
326         $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);
327         $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);
328         $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
329         $this->mockUser->attachRole($existingRole);
330
331         app('config')->set([
332             'services.ldap.user_to_groups'     => true,
333             'services.ldap.group_attribute'    => 'memberOf',
334             'services.ldap.remove_from_groups' => false,
335         ]);
336
337         $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 2);
338         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
339             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
340             ->andReturn(['count' => 1, 0 => [
341                 'uid'      => [$this->mockUser->name],
342                 'cn'       => [$this->mockUser->name],
343                 'dn'       => 'dc=test' . config('services.ldap.base_dn'),
344                 'mail'     => [$this->mockUser->email],
345                 'memberof' => [
346                     'count' => 2,
347                     0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',
348                     1       => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
349                 ],
350             ]]);
351
352         $this->mockUserLogin()->assertRedirect('/');
353
354         $user = User::where('email', $this->mockUser->email)->first();
355         $this->assertDatabaseHas('role_user', [
356             'user_id' => $user->id,
357             'role_id' => $roleToReceive->id,
358         ]);
359         $this->assertDatabaseHas('role_user', [
360             'user_id' => $user->id,
361             'role_id' => $roleToReceive2->id,
362         ]);
363         $this->assertDatabaseHas('role_user', [
364             'user_id' => $user->id,
365             'role_id' => $existingRole->id,
366         ]);
367     }
368
369     public function test_login_maps_roles_and_removes_old_roles_if_set()
370     {
371         $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
372         $existingRole = Role::factory()->create(['display_name' => 'ldaptester-existing']);
373         $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
374         $this->mockUser->attachRole($existingRole);
375
376         app('config')->set([
377             'services.ldap.user_to_groups'     => true,
378             'services.ldap.group_attribute'    => 'memberOf',
379             'services.ldap.remove_from_groups' => true,
380         ]);
381
382         $this->commonLdapMocks(1, 1, 3, 4, 2, 1, 1);
383         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
384             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
385             ->andReturn(['count' => 1, 0 => [
386                 'uid'      => [$this->mockUser->name],
387                 'cn'       => [$this->mockUser->name],
388                 'dn'       => 'dc=test' . config('services.ldap.base_dn'),
389                 'mail'     => [$this->mockUser->email],
390                 'memberof' => [
391                     'count' => 1,
392                     0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',
393                 ],
394             ]]);
395
396         $this->mockUserLogin()->assertRedirect('/');
397
398         $user = User::query()->where('email', $this->mockUser->email)->first();
399         $this->assertDatabaseHas('role_user', [
400             'user_id' => $user->id,
401             'role_id' => $roleToReceive->id,
402         ]);
403         $this->assertDatabaseMissing('role_user', [
404             'user_id' => $user->id,
405             'role_id' => $existingRole->id,
406         ]);
407     }
408
409     public function test_dump_user_groups_shows_group_related_details_as_json()
410     {
411         app('config')->set([
412             'services.ldap.user_to_groups'     => true,
413             'services.ldap.group_attribute'    => 'memberOf',
414             'services.ldap.remove_from_groups' => true,
415             'services.ldap.dump_user_groups'   => true,
416         ]);
417
418         $userResp = ['count' => 1, 0 => [
419             'uid'      => [$this->mockUser->name],
420             'cn'       => [$this->mockUser->name],
421             'dn'       => 'dc=test,' . config('services.ldap.base_dn'),
422             'mail'     => [$this->mockUser->email],
423         ]];
424         $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 0);
425         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
426             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
427             ->andReturn($userResp, ['count' => 1,
428                 0 => [
429                     'dn' => 'dc=test,' . config('services.ldap.base_dn'),
430                     'memberof' => [
431                         'count' => 1,
432                         0 => 'cn=ldaptester,ou=groups,dc=example,dc=com',
433                     ],
434                 ],
435             ]);
436
437         $this->mockLdap->shouldReceive('read')->times(2);
438         $this->mockLdap->shouldReceive('getEntries')->times(2)
439             ->andReturn([
440                 'count' => 1,
441                 0 => [
442                     'dn'        => 'cn=ldaptester,ou=groups,dc=example,dc=com',
443                     'memberof'  => [
444                         'count' => 1,
445                         0       => 'cn=monsters,ou=groups,dc=example,dc=com',
446                     ],
447                 ],
448             ], ['count' => 0]);
449
450         $resp = $this->mockUserLogin();
451         $resp->assertJson([
452             'details_from_ldap' => [
453                 'dn'       => 'dc=test,' . config('services.ldap.base_dn'),
454                 'memberof' => [
455                     0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',
456                     'count' => 1,
457                 ],
458             ],
459             'parsed_direct_user_groups' => [
460                 'cn=ldaptester,ou=groups,dc=example,dc=com',
461             ],
462             'parsed_recursive_user_groups' => [
463                 'cn=ldaptester,ou=groups,dc=example,dc=com',
464                 'cn=monsters,ou=groups,dc=example,dc=com',
465             ],
466             'parsed_resulting_group_names' => [
467                 'ldaptester',
468                 'monsters',
469             ],
470         ]);
471     }
472
473     public function test_recursive_group_search_queries_via_full_dn()
474     {
475         app('config')->set([
476             'services.ldap.user_to_groups'     => true,
477             'services.ldap.group_attribute'    => 'memberOf',
478         ]);
479
480         $userResp = ['count' => 1, 0 => [
481             'uid'      => [$this->mockUser->name],
482             'cn'       => [$this->mockUser->name],
483             'dn'       => 'dc=test,' . config('services.ldap.base_dn'),
484             'mail'     => [$this->mockUser->email],
485         ]];
486         $groupResp = ['count' => 1,
487                       0 => [
488                           'dn'       => 'dc=test,' . config('services.ldap.base_dn'),
489                           'memberof' => [
490                               'count' => 1,
491                               0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',
492                           ],
493                       ],
494         ];
495
496         $this->commonLdapMocks(1, 1, 3, 4, 2, 1);
497
498         $escapedName = ldap_escape($this->mockUser->name);
499         $this->mockLdap->shouldReceive('searchAndGetEntries')->twice()
500             ->with($this->resourceId, config('services.ldap.base_dn'), "(&(uid={$escapedName}))", \Mockery::type('array'))
501             ->andReturn($userResp, $groupResp);
502
503         $this->mockLdap->shouldReceive('read')->times(1)
504             ->with($this->resourceId, 'cn=ldaptester,ou=groups,dc=example,dc=com', '(objectClass=*)', ['memberof'])
505             ->andReturn(['count' => 0]);
506         $this->mockLdap->shouldReceive('getEntries')->times(1)
507             ->with($this->resourceId, ['count' => 0])
508             ->andReturn(['count' => 0]);
509
510         $resp = $this->mockUserLogin();
511         $resp->assertRedirect('/');
512     }
513
514     public function test_external_auth_id_visible_in_roles_page_when_ldap_active()
515     {
516         $role = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']);
517         $this->asAdmin()->get('/settings/roles/' . $role->id)
518             ->assertSee('ex-auth-a');
519     }
520
521     public function test_login_maps_roles_using_external_auth_ids_if_set()
522     {
523         $roleToReceive = Role::factory()->create(['display_name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']);
524         $roleToNotReceive = Role::factory()->create(['display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']);
525
526         app('config')->set([
527             'services.ldap.user_to_groups'     => true,
528             'services.ldap.group_attribute'    => 'memberOf',
529             'services.ldap.remove_from_groups' => true,
530         ]);
531
532         $this->commonLdapMocks(1, 1, 3, 4, 2, 1, 1);
533         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
534             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
535             ->andReturn(['count' => 1, 0 => [
536                 'uid'      => [$this->mockUser->name],
537                 'cn'       => [$this->mockUser->name],
538                 'dn'       => 'dc=test' . config('services.ldap.base_dn'),
539                 'mail'     => [$this->mockUser->email],
540                 'memberof' => [
541                     'count' => 1,
542                     0       => 'cn=ex-auth-a,ou=groups,dc=example,dc=com',
543                 ],
544             ]]);
545
546         $this->mockUserLogin()->assertRedirect('/');
547
548         $user = User::query()->where('email', $this->mockUser->email)->first();
549         $this->assertDatabaseHas('role_user', [
550             'user_id' => $user->id,
551             'role_id' => $roleToReceive->id,
552         ]);
553         $this->assertDatabaseMissing('role_user', [
554             'user_id' => $user->id,
555             'role_id' => $roleToNotReceive->id,
556         ]);
557     }
558
559     public function test_login_group_mapping_does_not_conflict_with_default_role()
560     {
561         $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
562         $roleToReceive2 = Role::factory()->create(['display_name' => 'LdapTester Second']);
563         $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save();
564
565         setting()->put('registration-role', $roleToReceive->id);
566
567         app('config')->set([
568             'services.ldap.user_to_groups'     => true,
569             'services.ldap.group_attribute'    => 'memberOf',
570             'services.ldap.remove_from_groups' => true,
571         ]);
572
573         $this->commonLdapMocks(1, 1, 4, 5, 2, 2, 2);
574         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
575             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
576             ->andReturn(['count' => 1, 0 => [
577                 'uid'      => [$this->mockUser->name],
578                 'cn'       => [$this->mockUser->name],
579                 'dn'       => 'dc=test' . config('services.ldap.base_dn'),
580                 'mail'     => [$this->mockUser->email],
581                 'memberof' => [
582                     'count' => 2,
583                     0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',
584                     1       => 'cn=ldaptester-second,ou=groups,dc=example,dc=com',
585                 ],
586             ]]);
587
588         $this->mockUserLogin()->assertRedirect('/');
589
590         $user = User::query()->where('email', $this->mockUser->email)->first();
591         $this->assertDatabaseHas('role_user', [
592             'user_id' => $user->id,
593             'role_id' => $roleToReceive->id,
594         ]);
595         $this->assertDatabaseHas('role_user', [
596             'user_id' => $user->id,
597             'role_id' => $roleToReceive2->id,
598         ]);
599     }
600
601     public function test_login_uses_specified_display_name_attribute()
602     {
603         app('config')->set([
604             'services.ldap.display_name_attribute' => 'displayName',
605         ]);
606
607         $this->commonLdapMocks(1, 1, 2, 4, 2);
608         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
609             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
610             ->andReturn(['count' => 1, 0 => [
611                 'uid'         => [$this->mockUser->name],
612                 'cn'          => [$this->mockUser->name],
613                 'dn'          => 'dc=test' . config('services.ldap.base_dn'),
614                 'displayname' => 'displayNameAttribute',
615             ]]);
616
617         $this->mockUserLogin()->assertRedirect('/login');
618         $this->get('/login')->assertSee('Please enter an email to use for this account.');
619
620         $resp = $this->mockUserLogin($this->mockUser->email);
621         $resp->assertRedirect('/');
622         $this->get('/')->assertSee('displayNameAttribute');
623         $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $this->mockUser->name, 'name' => 'displayNameAttribute']);
624     }
625
626     public function test_login_uses_multiple_display_properties_if_defined()
627     {
628         app('config')->set([
629             'services.ldap.display_name_attribute' => 'firstname|middlename|noname|lastname',
630         ]);
631
632         $this->commonLdapMocks(1, 1, 1, 2, 1);
633         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
634             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
635             ->andReturn(['count' => 1, 0 => [
636                 'uid'         => [$this->mockUser->name],
637                 'cn'          => [$this->mockUser->name],
638                 'dn'          => 'dc=test' . config('services.ldap.base_dn'),
639                 'firstname' => ['Barry'],
640                 'middlename' => ['Elliott'],
641                 'lastname' => ['Chuckle'],
642                 'mail'     => [$this->mockUser->email],
643             ]]);
644
645         $this->mockUserLogin();
646
647         $this->assertDatabaseHas('users', [
648             'email' => $this->mockUser->email,
649             'name' => 'Barry Elliott Chuckle',
650         ]);
651     }
652
653     public function test_login_uses_default_display_name_attribute_if_specified_not_present()
654     {
655         app('config')->set([
656             'services.ldap.display_name_attribute' => 'displayName',
657         ]);
658
659         $this->commonLdapMocks(1, 1, 2, 4, 2);
660         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
661             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
662             ->andReturn(['count' => 1, 0 => [
663                 'uid' => [$this->mockUser->name],
664                 'cn'  => [$this->mockUser->name],
665                 'dn'  => 'dc=test' . config('services.ldap.base_dn'),
666             ]]);
667
668         $this->mockUserLogin()->assertRedirect('/login');
669         $this->get('/login')->assertSee('Please enter an email to use for this account.');
670
671         $resp = $this->mockUserLogin($this->mockUser->email);
672         $resp->assertRedirect('/');
673         $this->get('/')->assertSee($this->mockUser->name);
674         $this->assertDatabaseHas('users', [
675             'email'            => $this->mockUser->email,
676             'email_confirmed'  => false,
677             'external_auth_id' => $this->mockUser->name,
678             'name'             => $this->mockUser->name,
679         ]);
680     }
681
682     protected function checkLdapReceivesCorrectDetails($serverString, $expectedHostString): void
683     {
684         app('config')->set(['services.ldap.server' => $serverString]);
685
686         $this->mockLdap->shouldReceive('connect')
687             ->once()
688             ->with($expectedHostString)
689             ->andReturn(false);
690
691         $this->mockUserLogin();
692     }
693
694     public function test_ldap_receives_correct_connect_host_from_config()
695     {
696         $expectedResultByInput = [
697             'ldaps://bookstack:8080' => 'ldaps://bookstack:8080',
698             'ldap.bookstack.com:8080' => 'ldap://ldap.bookstack.com:8080',
699             'ldap.bookstack.com' => 'ldap://ldap.bookstack.com',
700             'ldaps://ldap.bookstack.com' => 'ldaps://ldap.bookstack.com',
701             'ldaps://ldap.bookstack.com ldap://a.b.com' => 'ldaps://ldap.bookstack.com ldap://a.b.com',
702         ];
703
704         foreach ($expectedResultByInput as $input => $expectedResult) {
705             $this->checkLdapReceivesCorrectDetails($input, $expectedResult);
706             $this->refreshApplication();
707             $this->setUp();
708         }
709     }
710
711     public function test_forgot_password_routes_inaccessible()
712     {
713         $resp = $this->get('/password/email');
714         $this->assertPermissionError($resp);
715
716         $resp = $this->post('/password/email');
717         $this->assertPermissionError($resp);
718
719         $resp = $this->get('/password/reset/abc123');
720         $this->assertPermissionError($resp);
721
722         $resp = $this->post('/password/reset');
723         $this->assertPermissionError($resp);
724     }
725
726     public function test_user_invite_routes_inaccessible()
727     {
728         $resp = $this->get('/register/invite/abc123');
729         $this->assertPermissionError($resp);
730
731         $resp = $this->post('/register/invite/abc123');
732         $this->assertPermissionError($resp);
733     }
734
735     public function test_user_register_routes_inaccessible()
736     {
737         $resp = $this->get('/register');
738         $this->assertPermissionError($resp);
739
740         $resp = $this->post('/register');
741         $this->assertPermissionError($resp);
742     }
743
744     public function test_dump_user_details_option_works()
745     {
746         config()->set(['services.ldap.dump_user_details' => true, 'services.ldap.thumbnail_attribute' => 'jpegphoto']);
747
748         $this->commonLdapMocks(1, 1, 1, 1, 1);
749         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
750             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
751             ->andReturn(['count' => 1, 0 => [
752                 'uid' => [$this->mockUser->name],
753                 'cn'  => [$this->mockUser->name],
754                 // Test dumping binary data for avatar responses
755                 'jpegphoto' => base64_decode('/9j/4AAQSkZJRg=='),
756                 'dn'        => 'dc=test' . config('services.ldap.base_dn'),
757             ]]);
758
759         $resp = $this->post('/login', [
760             'username' => $this->mockUser->name,
761             'password' => $this->mockUser->password,
762         ]);
763         $resp->assertJsonStructure([
764             'details_from_ldap'        => [],
765             'details_bookstack_parsed' => [],
766         ]);
767     }
768
769     public function test_start_tls_called_if_option_set()
770     {
771         config()->set(['services.ldap.start_tls' => true]);
772         $this->mockLdap->shouldReceive('startTls')->once()->andReturn(true);
773         $this->runFailedAuthLogin();
774     }
775
776     public function test_connection_fails_if_tls_fails()
777     {
778         config()->set(['services.ldap.start_tls' => true]);
779         $this->mockLdap->shouldReceive('startTls')->once()->andReturn(false);
780         $this->commonLdapMocks(1, 1, 0, 0, 0);
781         $resp = $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']);
782         $resp->assertStatus(500);
783     }
784
785     public function test_ldap_attributes_can_be_binary_decoded_if_marked()
786     {
787         config()->set(['services.ldap.id_attribute' => 'BIN;uid']);
788         $ldapService = app()->make(LdapService::class);
789         $this->commonLdapMocks(1, 1, 1, 1, 1);
790         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
791             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), ['cn', 'dn', 'uid', 'mail', 'cn'])
792             ->andReturn(['count' => 1, 0 => [
793                 'uid' => [hex2bin('FFF8F7')],
794                 'cn'  => [$this->mockUser->name],
795                 'dn'  => 'dc=test' . config('services.ldap.base_dn'),
796             ]]);
797
798         $details = $ldapService->getUserDetails('test');
799         $this->assertEquals('fff8f7', $details['uid']);
800     }
801
802     public function test_new_ldap_user_login_with_already_used_email_address_shows_error_message_to_user()
803     {
804         $this->commonLdapMocks(1, 1, 2, 4, 2);
805         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2)
806             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
807             ->andReturn(['count' => 1, 0 => [
808                 'uid'  => [$this->mockUser->name],
809                 'cn'   => [$this->mockUser->name],
810                 'dn'   => 'dc=test' . config('services.ldap.base_dn'),
811                 'mail' => 'tester@example.com',
812             ]], ['count' => 1, 0 => [
813                 'uid'  => ['Barry'],
814                 'cn'   => ['Scott'],
815                 'dn'   => 'dc=bscott' . config('services.ldap.base_dn'),
816                 'mail' => 'tester@example.com',
817             ]]);
818
819         // First user login
820         $this->mockUserLogin()->assertRedirect('/');
821
822         // Second user login
823         auth()->logout();
824         $resp = $this->followingRedirects()->post('/login', ['username' => 'bscott', 'password' => 'pass']);
825         $resp->assertSee('A user with the email tester@example.com already exists but with different credentials');
826     }
827
828     public function test_login_with_email_confirmation_required_maps_groups_but_shows_confirmation_screen()
829     {
830         $roleToReceive = Role::factory()->create(['display_name' => 'LdapTester']);
831         $user = User::factory()->make();
832         setting()->put('registration-confirmation', 'true');
833
834         app('config')->set([
835             'services.ldap.user_to_groups'     => true,
836             'services.ldap.group_attribute'    => 'memberOf',
837             'services.ldap.remove_from_groups' => true,
838         ]);
839
840         $this->commonLdapMocks(1, 1, 6, 8, 4, 2, 2);
841         $this->mockLdap->shouldReceive('searchAndGetEntries')
842             ->times(4)
843             ->andReturn(['count' => 1, 0 => [
844                 'uid'      => [$user->name],
845                 'cn'       => [$user->name],
846                 'dn'       => 'dc=test' . config('services.ldap.base_dn'),
847                 'mail'     => [$user->email],
848                 'memberof' => [
849                     'count' => 1,
850                     0       => 'cn=ldaptester,ou=groups,dc=example,dc=com',
851                 ],
852             ]]);
853
854         $login = $this->followingRedirects()->mockUserLogin();
855         $login->assertSee('Thanks for registering!');
856         $this->assertDatabaseHas('users', [
857             'email'           => $user->email,
858             'email_confirmed' => false,
859         ]);
860
861         $user = User::query()->where('email', '=', $user->email)->first();
862         $this->assertDatabaseHas('role_user', [
863             'user_id' => $user->id,
864             'role_id' => $roleToReceive->id,
865         ]);
866
867         $this->assertNull(auth()->user());
868
869         $homePage = $this->get('/');
870         $homePage->assertRedirect('/login');
871
872         $login = $this->followingRedirects()->mockUserLogin();
873         $login->assertSee('Email Address Not Confirmed');
874     }
875
876     public function test_failed_logins_are_logged_when_message_configured()
877     {
878         $log = $this->withTestLogger();
879         config()->set(['logging.failed_login.message' => 'Failed login for %u']);
880         $this->runFailedAuthLogin();
881         $this->assertTrue($log->hasWarningThatContains('Failed login for timmyjenkins'));
882     }
883
884     public function test_thumbnail_attribute_used_as_user_avatar_if_configured()
885     {
886         config()->set(['services.ldap.thumbnail_attribute' => 'jpegPhoto']);
887
888         $this->commonLdapMocks(1, 1, 1, 2, 1);
889         $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
890         $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
891             ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
892             ->andReturn(['count' => 1, 0 => [
893                 'cn'        => [$this->mockUser->name],
894                 'dn'        => $ldapDn,
895                 'jpegphoto' => [base64_decode('/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8Q
896 EBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=')],
897                 'mail' => [$this->mockUser->email],
898             ]]);
899
900         $this->mockUserLogin()
901             ->assertRedirect('/');
902
903         $user = User::query()->where('email', '=', $this->mockUser->email)->first();
904         $this->assertNotNull($user->avatar);
905         $this->assertEquals('8c90748342f19b195b9c6b4eff742ded', md5_file(public_path($user->avatar->path)));
906     }
907
908     public function test_tls_ca_cert_option_throws_if_set_to_invalid_location()
909     {
910         $path = 'non_found_' . time();
911         config()->set(['services.ldap.tls_ca_cert' => $path]);
912
913         $this->commonLdapMocks(0, 0, 0, 0, 0);
914
915         $this->assertThrows(function () {
916             $this->withoutExceptionHandling()->mockUserLogin();
917         }, LdapException::class, "Provided path [{$path}] for LDAP TLS CA certs could not be resolved to an existing location");
918     }
919
920     public function test_tls_ca_cert_option_used_if_set_to_a_folder()
921     {
922         $path = $this->files->testFilePath('');
923         config()->set(['services.ldap.tls_ca_cert' => $path]);
924
925         $this->mockLdap->shouldReceive('setOption')->once()->with(null, LDAP_OPT_X_TLS_CACERTDIR, rtrim($path, '/'))->andReturn(true);
926         $this->runFailedAuthLogin();
927     }
928
929     public function test_tls_ca_cert_option_used_if_set_to_a_file()
930     {
931         $path = $this->files->testFilePath('test-file.txt');
932         config()->set(['services.ldap.tls_ca_cert' => $path]);
933
934         $this->mockLdap->shouldReceive('setOption')->once()->with(null, LDAP_OPT_X_TLS_CACERTFILE, $path)->andReturn(true);
935         $this->runFailedAuthLogin();
936     }
937 }
Morty Proxy This is a proxified and sanitized view of the page, visit original site.