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