+ if (!defined('LDAP_OPT_REFERRALS')) {
+ define('LDAP_OPT_REFERRALS', 1);
+ }
+ config()->set([
+ 'auth.method' => 'ldap',
+ 'auth.defaults.guard' => 'ldap',
+ 'services.ldap.base_dn' => 'dc=ldap,dc=local',
+ 'services.ldap.email_attribute' => 'mail',
+ 'services.ldap.display_name_attribute' => 'cn',
+ 'services.ldap.id_attribute' => 'uid',
+ 'services.ldap.user_to_groups' => false,
+ 'services.ldap.version' => '3',
+ 'services.ldap.user_filter' => '(&(uid={user}))',
+ 'services.ldap.follow_referrals' => false,
+ 'services.ldap.tls_insecure' => false,
+ 'services.ldap.tls_ca_cert' => false,
+ 'services.ldap.thumbnail_attribute' => null,
+ ]);
+ $this->mockLdap = $this->mock(Ldap::class);
+ $this->mockUser = User::factory()->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) {
+ return ldap_escape($val);
+ });
+ }
+
+ protected function mockExplodes($times = 1)
+ {
+ $this->mockLdap->shouldReceive('explodeDn')->times($times)->andReturnUsing(function ($dn, $withAttrib) {
+ return ldap_explode_dn($dn, $withAttrib);
+ });
+ }
+
+ protected function mockUserLogin(?string $email = null): TestResponse
+ {
+ return $this->post('/login', [
+ 'username' => $this->mockUser->name,
+ 'password' => $this->mockUser->password,
+ ] + ($email ? ['email' => $email] : []));
+ }
+
+ /**
+ * 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, int $groups = 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);
+ $this->mockGroupLookups($groups);
+ }
+
+ protected function mockGroupLookups(int $times = 1): void
+ {
+ $this->mockLdap->shouldReceive('read')->times($times)->andReturn(['count' => 0]);
+ $this->mockLdap->shouldReceive('getEntries')->times($times)->andReturn(['count' => 0]);
+ }
+
+ public function test_login()
+ {
+ $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'),
+ ]]);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/login');
+ $resp = $this->followRedirects($resp);
+ $resp->assertSee('Please enter an email to use for this account.');
+ $resp->assertSee($this->mockUser->name);
+
+ $resp = $this->followingRedirects()->mockUserLogin($this->mockUser->email);
+ $this->withHtml($resp)->assertElementExists('#home-default');
+ $resp->assertSee($this->mockUser->name);
+ $this->assertDatabaseHas('users', [
+ 'email' => $this->mockUser->email,
+ 'email_confirmed' => false,
+ 'external_auth_id' => $this->mockUser->name,
+ ]);
+ }
+
+ public function test_email_domain_restriction_active_on_new_ldap_login()
+ {
+ $this->setSettings([
+ 'registration-restrict' => 'testing.com',
+ ]);
+
+ $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'),
+ ]]);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/login');
+ $this->followRedirects($resp)->assertSee('Please enter an email to use for this account.');
+
+ $resp = $this->mockUserLogin($email);
+ $resp->assertRedirect('/login');
+ $this->followRedirects($resp)->assertSee('That email domain does not have access to this application');
+
+ $this->assertDatabaseMissing('users', ['email' => $email]);
+ }
+
+ public function test_login_works_when_no_uid_provided_by_ldap_server()
+ {
+ $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
+
+ $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 => [
+ 'cn' => [$this->mockUser->name],
+ 'dn' => $ldapDn,
+ 'mail' => [$this->mockUser->email],
+ ]]);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/');
+ $this->followRedirects($resp)->assertSee($this->mockUser->name);
+ $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => $ldapDn]);
+ }
+
+ public function test_a_custom_uid_attribute_can_be_specified_and_is_used_properly()
+ {
+ config()->set(['services.ldap.id_attribute' => 'my_custom_id']);
+
+ $this->commonLdapMocks(1, 1, 1, 2, 1);
+ $ldapDn = 'cn=test-user,dc=test' . config('services.ldap.base_dn');
+ $this->mockLdap->shouldReceive('searchAndGetEntries')->times(1)
+ ->with($this->resourceId, config('services.ldap.base_dn'), \Mockery::type('string'), \Mockery::type('array'))
+ ->andReturn(['count' => 1, 0 => [
+ 'cn' => [$this->mockUser->name],
+ 'dn' => $ldapDn,
+ 'my_custom_id' => ['cooluser456'],
+ 'mail' => [$this->mockUser->email],
+ ]]);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/');
+ $this->followRedirects($resp)->assertSee($this->mockUser->name);
+ $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'email_confirmed' => false, 'external_auth_id' => 'cooluser456']);
+ }
+
+ public function test_user_filter_default_placeholder_format()
+ {
+ config()->set('services.ldap.user_filter', '(&(uid={user}))');
+ $this->mockUser->name = 'barryldapuser';
+ $expectedFilter = '(&(uid=\62\61\72\72\79\6c\64\61\70\75\73\65\72))';
+
+ $this->commonLdapMocks(1, 1, 1, 1, 1);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')
+ ->once()
+ ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \Mockery::type('array'))
+ ->andReturn(['count' => 0, 0 => []]);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/login');
+ }
+
+ public function test_user_filter_old_placeholder_format()
+ {
+ config()->set('services.ldap.user_filter', '(&(username=${user}))');
+ $this->mockUser->name = 'barryldapuser';
+ $expectedFilter = '(&(username=\62\61\72\72\79\6c\64\61\70\75\73\65\72))';
+
+ $this->commonLdapMocks(1, 1, 1, 1, 1);
+ $this->mockLdap->shouldReceive('searchAndGetEntries')
+ ->once()
+ ->with($this->resourceId, config('services.ldap.base_dn'), $expectedFilter, \Mockery::type('array'))
+ ->andReturn(['count' => 0, 0 => []]);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/login');
+ }
+
+ public function test_initial_incorrect_credentials()
+ {
+ $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 => [
+ 'uid' => [$this->mockUser->name],
+ 'cn' => [$this->mockUser->name],
+ 'dn' => 'dc=test' . config('services.ldap.base_dn'),
+ ]]);
+ $this->mockLdap->shouldReceive('bind')->times(2)->andReturn(true, false);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/login');
+ $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
+ $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
+ }
+
+ public function test_login_not_found_username()
+ {
+ $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]);
+
+ $resp = $this->mockUserLogin();
+ $resp->assertRedirect('/login');
+ $this->followRedirects($resp)->assertSee('These credentials do not match our records.');
+ $this->assertDatabaseMissing('users', ['external_auth_id' => $this->mockUser->name]);
+ }
+
+ public function test_create_user_form()
+ {
+ $userForm = $this->asAdmin()->get('/settings/users/create');
+ $userForm->assertDontSee('Password');
+
+ $save = $this->post('/settings/users/create', [
+ 'name' => $this->mockUser->name,
+ 'email' => $this->mockUser->email,
+ ]);
+ $save->assertSessionHasErrors(['external_auth_id' => 'The external auth id field is required.']);
+
+ $save = $this->post('/settings/users/create', [
+ 'name' => $this->mockUser->name,
+ 'email' => $this->mockUser->email,
+ 'external_auth_id' => $this->mockUser->name,
+ ]);
+ $save->assertRedirect('/settings/users');
+ $this->assertDatabaseHas('users', ['email' => $this->mockUser->email, 'external_auth_id' => $this->mockUser->name, 'email_confirmed' => true]);
+ }
+
+ public function test_user_edit_form()
+ {
+ $editUser = $this->users->viewer();
+ $editPage = $this->asAdmin()->get("/settings/users/{$editUser->id}");
+ $editPage->assertSee('Edit User');
+ $editPage->assertDontSee('Password');
+
+ $update = $this->put("/settings/users/{$editUser->id}", [
+ 'name' => $editUser->name,
+ 'email' => $editUser->email,
+ 'external_auth_id' => 'test_auth_id',
+ ]);
+ $update->assertRedirect('/settings/users');
+ $this->assertDatabaseHas('users', ['email' => $editUser->email, 'external_auth_id' => 'test_auth_id']);