X-Git-Url: http://source.bookstackapp.com/bookstack/blobdiff_plain/da1cea06ca5db56a9635bf8bb01da2516d601620..refs/pull/2700/head:/tests/Auth/LdapTest.php diff --git a/tests/Auth/LdapTest.php b/tests/Auth/LdapTest.php index de86b9fb6..840dfd630 100644 --- a/tests/Auth/LdapTest.php +++ b/tests/Auth/LdapTest.php @@ -4,6 +4,7 @@ use BookStack\Auth\Access\LdapService; use BookStack\Auth\Role; use BookStack\Auth\Access\Ldap; use BookStack\Auth\User; +use BookStack\Exceptions\LdapException; use Mockery\MockInterface; use Tests\BrowserKitTest; @@ -40,6 +41,14 @@ class LdapTest extends BrowserKitTest $this->mockUser = factory(User::class)->make(); } + protected function runFailedAuthLogin() + { + $this->commonLdapMocks(1, 1, 1, 1, 1); + $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) + ->andReturn(['count' => 0]); + $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']); + } + protected function mockEscapes($times = 1) { $this->mockLdap->shouldReceive('escape')->times($times)->andReturnUsing(function($val) { @@ -63,11 +72,22 @@ class LdapTest extends BrowserKitTest ->press('Log In'); } + /** + * Set LDAP method mocks for things we commonly call without altering. + */ + protected function commonLdapMocks(int $connects = 1, int $versions = 1, int $options = 2, int $binds = 4, int $escapes = 2, int $explodes = 0) + { + $this->mockLdap->shouldReceive('connect')->times($connects)->andReturn($this->resourceId); + $this->mockLdap->shouldReceive('setVersion')->times($versions); + $this->mockLdap->shouldReceive('setOption')->times($options); + $this->mockLdap->shouldReceive('bind')->times($binds)->andReturn(true); + $this->mockEscapes($escapes); + $this->mockExplodes($explodes); + } + public function test_login() { - $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->once(); - $this->mockLdap->shouldReceive('setOption')->times(2); + $this->commonLdapMocks(1, 1, 2, 4, 2); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -75,8 +95,6 @@ class LdapTest extends BrowserKitTest 'cn' => [$this->mockUser->name], 'dn' => ['dc=test' . config('services.ldap.base_dn')] ]]); - $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true); - $this->mockEscapes(2); $this->mockUserLogin() ->seePageIs('/login')->see('Please enter an email to use for this account.'); @@ -94,9 +112,7 @@ class LdapTest extends BrowserKitTest 'registration-restrict' => 'testing.com' ]); - $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->once(); - $this->mockLdap->shouldReceive('setOption')->times(2); + $this->commonLdapMocks(1, 1, 2, 4, 2); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -104,8 +120,6 @@ class LdapTest extends BrowserKitTest 'cn' => [$this->mockUser->name], 'dn' => ['dc=test' . config('services.ldap.base_dn')] ]]); - $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true); - $this->mockEscapes(2); $this->mockUserLogin() ->seePageIs('/login') @@ -122,10 +136,9 @@ class LdapTest extends BrowserKitTest public function test_login_works_when_no_uid_provided_by_ldap_server() { - $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->once(); $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn'); - $this->mockLdap->shouldReceive('setOption')->times(1); + + $this->commonLdapMocks(1, 1, 1, 2, 1); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -133,8 +146,6 @@ class LdapTest extends BrowserKitTest 'dn' => $ldapDn, 'mail' => [$this->mockUser->email] ]]); - $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true); - $this->mockEscapes(1); $this->mockUserLogin() ->seePageIs('/') @@ -145,10 +156,9 @@ class LdapTest extends BrowserKitTest public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly() { config()->set(['services.ldap.id_attribute' => 'my_custom_id']); - $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->once(); + + $this->commonLdapMocks(1, 1, 1, 2, 1); $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn'); - $this->mockLdap->shouldReceive('setOption')->times(1); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -159,9 +169,6 @@ class LdapTest extends BrowserKitTest ]]); - $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true); - $this->mockEscapes(1); - $this->mockUserLogin() ->seePageIs('/') ->see($this->mockUser->name) @@ -170,9 +177,7 @@ class LdapTest extends BrowserKitTest public function test_initial_incorrect_credentials() { - $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->once(); - $this->mockLdap->shouldReceive('setOption')->times(1); + $this->commonLdapMocks(1, 1, 1, 0, 1); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -181,7 +186,6 @@ class LdapTest extends BrowserKitTest 'dn' => ['dc=test' . config('services.ldap.base_dn')] ]]); $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false); - $this->mockEscapes(1); $this->mockUserLogin() ->seePageIs('/login')->see('These credentials do not match our records.') @@ -190,14 +194,10 @@ class LdapTest extends BrowserKitTest public function test_login_not_found_username() { - $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->once(); - $this->mockLdap->shouldReceive('setOption')->times(1); + $this->commonLdapMocks(1, 1, 1, 1, 1); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 0]); - $this->mockLdap->shouldReceive('bind')->times(1)->andReturn(true, false); - $this->mockEscapes(1); $this->mockUserLogin() ->seePageIs('/login')->see('These credentials do not match our records.') @@ -246,9 +246,9 @@ class LdapTest extends BrowserKitTest public function test_login_maps_roles_and_retains_existing_roles() { - $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']); - $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']); - $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']); + $roleToReceive = factory(Role::class)->create(['display_name' => 'LdapTester']); + $roleToReceive2 = factory(Role::class)->create(['display_name' => 'LdapTester Second']); + $existingRole = factory(Role::class)->create(['display_name' => 'ldaptester-existing']); $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save(); $this->mockUser->attachRole($existingRole); @@ -257,9 +257,8 @@ class LdapTest extends BrowserKitTest 'services.ldap.group_attribute' => 'memberOf', 'services.ldap.remove_from_groups' => false, ]); - $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->times(1); - $this->mockLdap->shouldReceive('setOption')->times(4); + + $this->commonLdapMocks(1, 1, 4, 5, 4, 6); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -273,9 +272,6 @@ class LdapTest extends BrowserKitTest 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com", ] ]]); - $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true); - $this->mockEscapes(4); - $this->mockExplodes(6); $this->mockUserLogin()->seePageIs('/'); @@ -296,8 +292,8 @@ class LdapTest extends BrowserKitTest public function test_login_maps_roles_and_removes_old_roles_if_set() { - $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']); - $existingRole = factory(Role::class)->create(['name' => 'ldaptester-existing']); + $roleToReceive = factory(Role::class)->create(['display_name' => 'LdapTester']); + $existingRole = factory(Role::class)->create(['display_name' => 'ldaptester-existing']); $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save(); $this->mockUser->attachRole($existingRole); @@ -306,9 +302,8 @@ class LdapTest extends BrowserKitTest 'services.ldap.group_attribute' => 'memberOf', 'services.ldap.remove_from_groups' => true, ]); - $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->times(1); - $this->mockLdap->shouldReceive('setOption')->times(3); + + $this->commonLdapMocks(1, 1, 3, 4, 3, 2); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -321,9 +316,6 @@ class LdapTest extends BrowserKitTest 0 => "cn=ldaptester,ou=groups,dc=example,dc=com", ] ]]); - $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true); - $this->mockEscapes(3); - $this->mockExplodes(2); $this->mockUserLogin()->seePageIs('/'); @@ -340,24 +332,23 @@ class LdapTest extends BrowserKitTest public function test_external_auth_id_visible_in_roles_page_when_ldap_active() { - $role = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']); + $role = factory(Role::class)->create(['display_name' => 'ldaptester', 'external_auth_id' => 'ex-auth-a, test-second-param']); $this->asAdmin()->visit('/settings/roles/' . $role->id) ->see('ex-auth-a'); } public function test_login_maps_roles_using_external_auth_ids_if_set() { - $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']); - $roleToNotReceive = factory(Role::class)->create(['name' => 'ldaptester-not-receive', 'display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']); + $roleToReceive = factory(Role::class)->create(['display_name' => 'ldaptester', 'external_auth_id' => 'test-second-param, ex-auth-a']); + $roleToNotReceive = factory(Role::class)->create(['display_name' => 'ex-auth-a', 'external_auth_id' => 'test-second-param']); app('config')->set([ 'services.ldap.user_to_groups' => true, 'services.ldap.group_attribute' => 'memberOf', 'services.ldap.remove_from_groups' => true, ]); - $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->times(1); - $this->mockLdap->shouldReceive('setOption')->times(3); + + $this->commonLdapMocks(1, 1, 3, 4, 3, 2); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(3) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -370,9 +361,6 @@ class LdapTest extends BrowserKitTest 0 => "cn=ex-auth-a,ou=groups,dc=example,dc=com", ] ]]); - $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true); - $this->mockEscapes(3); - $this->mockExplodes(2); $this->mockUserLogin()->seePageIs('/'); @@ -389,8 +377,8 @@ class LdapTest extends BrowserKitTest public function test_login_group_mapping_does_not_conflict_with_default_role() { - $roleToReceive = factory(Role::class)->create(['name' => 'ldaptester', 'display_name' => 'LdapTester']); - $roleToReceive2 = factory(Role::class)->create(['name' => 'ldaptester-second', 'display_name' => 'LdapTester Second']); + $roleToReceive = factory(Role::class)->create(['display_name' => 'LdapTester']); + $roleToReceive2 = factory(Role::class)->create(['display_name' => 'LdapTester Second']); $this->mockUser->forceFill(['external_auth_id' => $this->mockUser->name])->save(); setting()->put('registration-role', $roleToReceive->id); @@ -400,9 +388,8 @@ class LdapTest extends BrowserKitTest 'services.ldap.group_attribute' => 'memberOf', 'services.ldap.remove_from_groups' => true, ]); - $this->mockLdap->shouldReceive('connect')->times(1)->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->times(1); - $this->mockLdap->shouldReceive('setOption')->times(4); + + $this->commonLdapMocks(1, 1, 4, 5, 4, 6); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(4) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -416,9 +403,6 @@ class LdapTest extends BrowserKitTest 1 => "cn=ldaptester-second,ou=groups,dc=example,dc=com", ] ]]); - $this->mockLdap->shouldReceive('bind')->times(5)->andReturn(true); - $this->mockEscapes(4); - $this->mockExplodes(6); $this->mockUserLogin()->seePageIs('/'); @@ -439,9 +423,7 @@ class LdapTest extends BrowserKitTest 'services.ldap.display_name_attribute' => 'displayName' ]); - $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->once(); - $this->mockLdap->shouldReceive('setOption')->times(2); + $this->commonLdapMocks(1, 1, 2, 4, 2); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -450,8 +432,6 @@ class LdapTest extends BrowserKitTest 'dn' => ['dc=test' . config('services.ldap.base_dn')], 'displayname' => 'displayNameAttribute' ]]); - $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true); - $this->mockEscapes(2); $this->mockUserLogin() ->seePageIs('/login')->see('Please enter an email to use for this account.'); @@ -469,9 +449,7 @@ class LdapTest extends BrowserKitTest 'services.ldap.display_name_attribute' => 'displayName' ]); - $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->once(); - $this->mockLdap->shouldReceive('setOption')->times(2); + $this->commonLdapMocks(1, 1, 2, 4, 2); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -479,8 +457,6 @@ class LdapTest extends BrowserKitTest 'cn' => [$this->mockUser->name], 'dn' => ['dc=test' . config('services.ldap.base_dn')] ]]); - $this->mockLdap->shouldReceive('bind')->times(4)->andReturn(true); - $this->mockEscapes(2); $this->mockUserLogin() ->seePageIs('/login')->see('Please enter an email to use for this account.'); @@ -499,15 +475,12 @@ class LdapTest extends BrowserKitTest ]); // Standard mocks - $this->mockLdap->shouldReceive('setVersion')->once(); - $this->mockLdap->shouldReceive('setOption')->times(1); + $this->commonLdapMocks(0, 1, 1, 2, 1); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)->andReturn(['count' => 1, 0 => [ 'uid' => [$this->mockUser->name], 'cn' => [$this->mockUser->name], 'dn' => ['dc=test' . config('services.ldap.base_dn')] ]]); - $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true); - $this->mockEscapes(1); $this->mockLdap->shouldReceive('connect')->once() ->with($expectedHost, $expectedPort)->andReturn($this->resourceId); @@ -567,9 +540,7 @@ class LdapTest extends BrowserKitTest { config()->set(['services.ldap.dump_user_details' => true]); - $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->once(); - $this->mockLdap->shouldReceive('setOption')->times(1); + $this->commonLdapMocks(1, 1, 1, 1, 1); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) ->andReturn(['count' => 1, 0 => [ @@ -577,8 +548,6 @@ class LdapTest extends BrowserKitTest 'cn' => [$this->mockUser->name], 'dn' => ['dc=test' . config('services.ldap.base_dn')] ]]); - $this->mockLdap->shouldReceive('bind')->times(1)->andReturn(true); - $this->mockEscapes(1); $this->post('/login', [ 'username' => $this->mockUser->name, @@ -590,14 +559,27 @@ class LdapTest extends BrowserKitTest ]); } + public function test_start_tls_called_if_option_set() + { + config()->set(['services.ldap.start_tls' => true]); + $this->mockLdap->shouldReceive('startTls')->once()->andReturn(true); + $this->runFailedAuthLogin(); + } + + public function test_connection_fails_if_tls_fails() + { + config()->set(['services.ldap.start_tls' => true]); + $this->mockLdap->shouldReceive('startTls')->once()->andReturn(false); + $this->commonLdapMocks(1, 1, 0, 0, 0); + $this->post('/login', ['username' => 'timmyjenkins', 'password' => 'cattreedog']); + $this->assertResponseStatus(500); + } + public function test_ldap_attributes_can_be_binary_decoded_if_marked() { config()->set(['services.ldap.id_attribute' => 'BIN;uid']); $ldapService = app()->make(LdapService::class); - - $this->mockLdap->shouldReceive('connect')->once()->andReturn($this->resourceId); - $this->mockLdap->shouldReceive('setVersion')->once(); - $this->mockLdap->shouldReceive('setOption')->times(1); + $this->commonLdapMocks(1, 1, 1, 1, 1); $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1) ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), ['cn', 'dn', 'uid', 'mail', 'cn']) ->andReturn(['count' => 1, 0 => [ @@ -605,10 +587,85 @@ class LdapTest extends BrowserKitTest 'cn' => [$this->mockUser->name], 'dn' => ['dc=test' . config('services.ldap.base_dn')] ]]); - $this->mockLdap->shouldReceive('bind')->times(1)->andReturn(true); - $this->mockEscapes(1); $details = $ldapService->getUserDetails('test'); $this->assertEquals('fff8f7', $details['uid']); } + + public function test_new_ldap_user_login_with_already_used_email_address_shows_error_message_to_user() + { + $this->commonLdapMocks(1, 1, 2, 4, 2); + $this->mockLdap->shouldReceive('searchAndGetEntries')->times(2) + ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array')) + ->andReturn(['count' => 1, 0 => [ + 'uid' => [$this->mockUser->name], + 'cn' => [$this->mockUser->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], + 'mail' => 'tester@example.com', + ]], ['count' => 1, 0 => [ + 'uid' => ['Barry'], + 'cn' => ['Scott'], + 'dn' => ['dc=bscott' . config('services.ldap.base_dn')], + 'mail' => 'tester@example.com', + ]]); + + // First user login + $this->mockUserLogin()->seePageIs('/'); + + // Second user login + auth()->logout(); + $this->post('/login', ['username' => 'bscott', 'password' => 'pass'])->followRedirects(); + + $this->see('A user with the email tester@example.com already exists but with different credentials'); + } + + public function test_login_with_email_confirmation_required_maps_groups_but_shows_confirmation_screen() + { + $roleToReceive = factory(Role::class)->create(['display_name' => 'LdapTester']); + $user = factory(User::class)->make(); + setting()->put('registration-confirmation', 'true'); + + app('config')->set([ + 'services.ldap.user_to_groups' => true, + 'services.ldap.group_attribute' => 'memberOf', + 'services.ldap.remove_from_groups' => true, + ]); + + $this->commonLdapMocks(1, 1, 3, 4, 3, 2); + $this->mockLdap->shouldReceive('searchAndGetEntries') + ->times(3) + ->andReturn(['count' => 1, 0 => [ + 'uid' => [$user->name], + 'cn' => [$user->name], + 'dn' => ['dc=test' . config('services.ldap.base_dn')], + 'mail' => [$user->email], + 'memberof' => [ + 'count' => 1, + 0 => "cn=ldaptester,ou=groups,dc=example,dc=com", + ] + ]]); + + $this->mockUserLogin()->seePageIs('/register/confirm'); + $this->seeInDatabase('users', [ + 'email' => $user->email, + 'email_confirmed' => false, + ]); + + $user = User::query()->where('email', '=', $user->email)->first(); + $this->seeInDatabase('role_user', [ + 'user_id' => $user->id, + 'role_id' => $roleToReceive->id + ]); + + $homePage = $this->get('/'); + $homePage->assertRedirectedTo('/register/confirm/awaiting'); + } + + public function test_failed_logins_are_logged_when_message_configured() + { + $log = $this->withTestLogger(); + config()->set(['logging.failed_login.message' => 'Failed login for %u']); + $this->runFailedAuthLogin(); + $this->assertTrue($log->hasWarningThatContains('Failed login for timmyjenkins')); + } }