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