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