mirror of
https://github.com/projectsend/projectsend.git
synced 2026-10-08 22:31:06 +00:00
Merge pull request #1810 from veenone/feat/ldap-signin-by-username
Feat/ldap signin by username
This commit is contained in:
@@ -25,7 +25,7 @@ beforeEach(function () {
|
||||
});
|
||||
|
||||
/**
|
||||
* @param array<string, array{password: string, name?: string, dn?: string}> $entries
|
||||
* @param array<string, array{password: string, name?: string, dn?: string, username?: string}> $entries
|
||||
*/
|
||||
function fakeDirectory(array $entries = []): FakeLdapDirectory
|
||||
{
|
||||
@@ -35,7 +35,7 @@ function fakeDirectory(array $entries = []): FakeLdapDirectory
|
||||
return $fake;
|
||||
}
|
||||
|
||||
function enableLdap(bool $autoProvision = false, bool $autoApprove = false): LdapSettings
|
||||
function enableLdap(bool $autoProvision = false, bool $autoApprove = false, ?string $usernameAttribute = null): LdapSettings
|
||||
{
|
||||
$settings = LdapSettings::current();
|
||||
$settings->forceFill([
|
||||
@@ -44,6 +44,7 @@ function enableLdap(bool $autoProvision = false, bool $autoApprove = false): Lda
|
||||
'base_dn' => 'dc=example,dc=test',
|
||||
'auto_provision' => $autoProvision,
|
||||
'auto_approve' => $autoApprove,
|
||||
'username_attribute' => $usernameAttribute,
|
||||
])->save();
|
||||
|
||||
return $settings;
|
||||
@@ -512,3 +513,113 @@ test('a local account still confirms against its own hash, with LDAP on', functi
|
||||
->assertRedirect()
|
||||
->assertSessionHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Signing in with a directory username
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
test('a client signs in with their directory username', function () {
|
||||
enableLdap(usernameAttribute: 'uid');
|
||||
$fake = fakeDirectory(['someone@example.test' => ['password' => 'directory-pass', 'username' => 'someone']]);
|
||||
$client = User::factory()->client()->create(['email' => 'someone@example.test']);
|
||||
|
||||
$this->post('/login', ['email' => 'someone', 'password' => 'directory-pass'])->assertRedirect();
|
||||
|
||||
$this->assertAuthenticatedAs($client);
|
||||
expect($fake->lookedUpUsernames)->toBe(['someone'])
|
||||
->and($fake->attemptedEmails)->toBe(['someone@example.test']);
|
||||
});
|
||||
|
||||
test('a username signs in a client the directory has not met yet, when auto-provisioning is on', function () {
|
||||
enableLdap(autoProvision: true, autoApprove: true, usernameAttribute: 'uid');
|
||||
fakeDirectory(['newcomer@example.test' => ['password' => 'directory-pass', 'username' => 'newcomer', 'name' => 'New Comer']]);
|
||||
|
||||
$this->post('/login', ['email' => 'newcomer', 'password' => 'directory-pass'])->assertRedirect();
|
||||
|
||||
$user = User::query()->where('email', 'newcomer@example.test')->sole();
|
||||
expect($user->isClient())->toBeTrue()
|
||||
->and($user->auth_source)->toBe(AuthSource::Ldap);
|
||||
$this->assertAuthenticatedAs($user);
|
||||
});
|
||||
|
||||
test('a username never signs in a staff account, even with its own password', function () {
|
||||
enableLdap(usernameAttribute: 'uid');
|
||||
fakeDirectory(['admin@example.test' => ['password' => 'directory-pass', 'username' => 'admin']]);
|
||||
User::factory()->create(['email' => 'admin@example.test']);
|
||||
|
||||
$this->post('/login', ['email' => 'admin', 'password' => 'password'])->assertSessionHasErrors('email');
|
||||
$this->post('/login', ['email' => 'admin', 'password' => 'directory-pass'])->assertSessionHasErrors('email');
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
test('a wrong password with a valid username is refused', function () {
|
||||
enableLdap(usernameAttribute: 'uid');
|
||||
fakeDirectory(['someone@example.test' => ['password' => 'directory-pass', 'username' => 'someone']]);
|
||||
User::factory()->client()->create(['email' => 'someone@example.test']);
|
||||
|
||||
$this->post('/login', ['email' => 'someone', 'password' => 'wrong'])
|
||||
->assertSessionHasErrors(['email' => __('auth.failed')]);
|
||||
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
test('an unknown username gets the same failure as a wrong password, and counts toward the limit', function () {
|
||||
enableLdap(usernameAttribute: 'uid');
|
||||
fakeDirectory();
|
||||
|
||||
foreach (range(1, 5) as $_) {
|
||||
$this->post('/login', ['email' => 'nobody', 'password' => 'whatever'])
|
||||
->assertSessionHasErrors(['email' => __('auth.failed')]);
|
||||
}
|
||||
|
||||
$this->post('/login', ['email' => 'nobody', 'password' => 'whatever'])
|
||||
->assertSessionHasErrors('email');
|
||||
expect(session('errors')->first('email'))->not->toBe(__('auth.failed'));
|
||||
});
|
||||
|
||||
test('a username is only accepted once a username attribute is set', function () {
|
||||
enableLdap();
|
||||
$fake = fakeDirectory(['someone@example.test' => ['password' => 'directory-pass', 'username' => 'someone']]);
|
||||
User::factory()->client()->create(['email' => 'someone@example.test']);
|
||||
|
||||
$this->post('/login', ['email' => 'someone', 'password' => 'directory-pass'])->assertSessionHasErrors('email');
|
||||
|
||||
expect($fake->calls)->toBe(0);
|
||||
$this->assertGuest();
|
||||
});
|
||||
|
||||
test('an email address still signs in by email with username sign-in on', function () {
|
||||
enableLdap(usernameAttribute: 'uid');
|
||||
$fake = fakeDirectory(['someone@example.test' => ['password' => 'directory-pass', 'username' => 'someone']]);
|
||||
$client = User::factory()->client()->create(['email' => 'someone@example.test']);
|
||||
|
||||
$this->post('/login', ['email' => 'someone@example.test', 'password' => 'directory-pass'])->assertRedirect();
|
||||
|
||||
$this->assertAuthenticatedAs($client);
|
||||
expect($fake->lookedUpUsernames)->toBe([]);
|
||||
});
|
||||
|
||||
test('the login page offers username sign-in only when it is configured', function () {
|
||||
$this->get('/login')->assertInertia(fn ($page) => $page->where('usernameSignIn', false));
|
||||
|
||||
enableLdap(usernameAttribute: 'uid');
|
||||
|
||||
$this->get('/login')->assertInertia(fn ($page) => $page->where('usernameSignIn', true));
|
||||
});
|
||||
|
||||
// Decided by the same rule that admitted the address, or an account whose
|
||||
// address the `email` rule accepts and filter_var does not (a dotless
|
||||
// domain, say) would be taken for a username and locked out.
|
||||
test('an address the email rule accepts is still an address with username sign-in on', function () {
|
||||
enableLdap(usernameAttribute: 'uid');
|
||||
$fake = fakeDirectory();
|
||||
$client = User::factory()->client()->create(['email' => 'someone@localhost']);
|
||||
|
||||
$this->post('/login', ['email' => 'someone@localhost', 'password' => 'password'])->assertRedirect();
|
||||
|
||||
$this->assertAuthenticatedAs($client);
|
||||
expect($fake->lookedUpUsernames)->toBe([]);
|
||||
});
|
||||
|
||||
@@ -190,3 +190,16 @@ test('settings are only usable once they are complete', function () {
|
||||
$settings->forceFill(['active' => true, 'host' => 'h', 'base_dn' => 'b'])->save();
|
||||
expect($settings->refresh()->usable())->toBe(extension_loaded('ldap'));
|
||||
});
|
||||
|
||||
test('the username attribute is optional, saved, and cleared when left empty', function () {
|
||||
expect(LdapSettings::current()->username_attribute)->toBeNull();
|
||||
|
||||
$this->actingAs($this->admin)->patch('/system/settings/ldap', ldapPayload(['username_attribute' => 'uid']))->assertRedirect();
|
||||
expect(LdapSettings::current()->username_attribute)->toBe('uid');
|
||||
|
||||
$this->actingAs($this->admin)->get('/system/settings/ldap')
|
||||
->assertInertia(fn (AssertableInertia $page) => $page->where('ldap.username_attribute', 'uid'));
|
||||
|
||||
$this->actingAs($this->admin)->patch('/system/settings/ldap', ldapPayload(['username_attribute' => '']))->assertRedirect();
|
||||
expect(LdapSettings::current()->username_attribute)->toBeNull();
|
||||
});
|
||||
|
||||
@@ -25,8 +25,11 @@ class FakeLdapDirectory implements LdapDirectory
|
||||
/** @var list<string> */
|
||||
public array $attemptedEmails = [];
|
||||
|
||||
/** @var list<string> */
|
||||
public array $lookedUpUsernames = [];
|
||||
|
||||
/**
|
||||
* @param array<string, array{password: string, name?: string, dn?: string}> $entries keyed by email
|
||||
* @param array<string, array{password: string, name?: string, dn?: string, username?: string}> $entries keyed by email
|
||||
*/
|
||||
public function __construct(private readonly array $entries = []) {}
|
||||
|
||||
@@ -48,6 +51,19 @@ class FakeLdapDirectory implements LdapDirectory
|
||||
);
|
||||
}
|
||||
|
||||
public function emailForUsername(string $username): ?string
|
||||
{
|
||||
$this->calls++;
|
||||
$this->lookedUpUsernames[] = $username;
|
||||
|
||||
$matches = array_keys(array_filter(
|
||||
$this->entries,
|
||||
fn (array $entry): bool => ($entry['username'] ?? null) === $username,
|
||||
));
|
||||
|
||||
return count($matches) === 1 ? $matches[0] : null;
|
||||
}
|
||||
|
||||
public function probe(?string $email = null, ?string $password = null): LdapProbeResult
|
||||
{
|
||||
return LdapProbeResult::ok(LdapProbeResult::STAGE_SERVICE_BIND, 'Fake directory reachable.');
|
||||
|
||||
Reference in New Issue
Block a user