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