mirror of
https://github.com/projectsend/projectsend.git
synced 2026-10-08 14:21:15 +00:00
9c43f9cb9a
LDAP sign-in only took an email address. The LDAP settings now have an optional username attribute (cn, uid, sAMAccountName and so on). Once it is set, the login field also takes a username. The service account looks the username up, and the login carries on with the address the directory holds for it, through the same checks, single user bind, provisioning and rate limiting as an email login. Whether the input is an address is decided by the same email rule that accepted every stored address, so an address such as someone@localhost is never taken for a username. The username goes through the query builder, so it is escaped, and it has to match exactly one entry. The directory is client-only, so a username never signs in a staff account. With the attribute left empty, nothing changes. This ports feat/ldap_signin_by_username, which was written against v1 and has no history in common with this codebase.
72 lines
2.2 KiB
PHP
72 lines
2.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Support;
|
|
|
|
use App\Modules\Identity\Ldap\LdapDirectory;
|
|
use App\Modules\Identity\Ldap\LdapIdentity;
|
|
use App\Modules\Identity\Ldap\LdapProbeResult;
|
|
|
|
/**
|
|
* A directory that lives in an array.
|
|
*
|
|
* Swapped in with `$this->swap(LdapDirectory::class, ...)`, this exercises
|
|
* everything above the wire — which account types may authenticate,
|
|
* provisioning, the two-factor hand-off, rate limiting — with no server
|
|
* anywhere. It also counts calls, so a test can assert the directory was
|
|
* *not* consulted, which is how the "staff never reach LDAP" and "a valid
|
|
* local password costs no directory traffic" properties are proven.
|
|
*/
|
|
class FakeLdapDirectory implements LdapDirectory
|
|
{
|
|
public int $calls = 0;
|
|
|
|
/** @var list<string> */
|
|
public array $attemptedEmails = [];
|
|
|
|
/** @var list<string> */
|
|
public array $lookedUpUsernames = [];
|
|
|
|
/**
|
|
* @param array<string, array{password: string, name?: string, dn?: string, username?: string}> $entries keyed by email
|
|
*/
|
|
public function __construct(private readonly array $entries = []) {}
|
|
|
|
public function authenticate(string $email, string $password): ?LdapIdentity
|
|
{
|
|
$this->calls++;
|
|
$this->attemptedEmails[] = $email;
|
|
|
|
$entry = $this->entries[$email] ?? null;
|
|
|
|
if ($entry === null || $entry['password'] !== $password) {
|
|
return null;
|
|
}
|
|
|
|
return new LdapIdentity(
|
|
dn: $entry['dn'] ?? "uid={$email},ou=people,dc=example,dc=test",
|
|
email: $email,
|
|
name: $entry['name'] ?? 'Directory Person',
|
|
);
|
|
}
|
|
|
|
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.');
|
|
}
|
|
}
|