mirror of
https://github.com/projectsend/projectsend.git
synced 2026-10-12 15:58:58 +00:00
Merge pull request #1810 from veenone/feat/ldap-signin-by-username
Feat/ldap signin by username
This commit is contained in:
@@ -56,6 +56,7 @@ class LdapSettingsController extends Controller
|
||||
'user_filter' => $ldap->user_filter,
|
||||
'email_attribute' => $ldap->email_attribute,
|
||||
'name_attribute' => $ldap->name_attribute,
|
||||
'username_attribute' => $ldap->username_attribute,
|
||||
'auto_provision' => $ldap->auto_provision,
|
||||
'auto_approve' => $ldap->auto_approve,
|
||||
],
|
||||
@@ -93,6 +94,7 @@ class LdapSettingsController extends Controller
|
||||
'user_filter' => ['nullable', 'string', 'max:255'],
|
||||
'email_attribute' => ['required', 'string', 'max:64'],
|
||||
'name_attribute' => ['required', 'string', 'max:64'],
|
||||
'username_attribute' => ['nullable', 'string', 'max:64'],
|
||||
'auto_provision' => ['required', 'boolean'],
|
||||
'auto_approve' => ['required', 'boolean'],
|
||||
]);
|
||||
@@ -110,6 +112,7 @@ class LdapSettingsController extends Controller
|
||||
'user_filter' => $validated['user_filter'] ?? null,
|
||||
'email_attribute' => $validated['email_attribute'],
|
||||
'name_attribute' => $validated['name_attribute'],
|
||||
'username_attribute' => $validated['username_attribute'] ?? null,
|
||||
'auto_provision' => (bool) $validated['auto_provision'],
|
||||
'auto_approve' => (bool) $validated['auto_approve'],
|
||||
]);
|
||||
|
||||
@@ -72,6 +72,20 @@ class LdapAuthenticator
|
||||
return $identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* The address a directory username belongs to, so a login typed as a
|
||||
* username can carry on exactly as if the address had been typed.
|
||||
* Null whenever username sign-in is off or the breaker is open.
|
||||
*/
|
||||
public function emailForUsername(string $username): ?string
|
||||
{
|
||||
if (! LdapSettings::current()->allowsUsernameSignIn() || $this->breakerOpen()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->directory->emailForUsername($username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Record what the directory told us about an account that already
|
||||
* exists, so an administrator can see which entry it corresponds to.
|
||||
@@ -104,13 +118,18 @@ class LdapAuthenticator
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Cache::get(self::BREAKER_KEY) === true) {
|
||||
if ($this->breakerOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->enabled();
|
||||
}
|
||||
|
||||
private function breakerOpen(): bool
|
||||
{
|
||||
return Cache::get(self::BREAKER_KEY) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this account's password lives in the directory rather than
|
||||
* here — in which case the local hash is not consulted at all.
|
||||
|
||||
@@ -25,6 +25,14 @@ interface LdapDirectory
|
||||
*/
|
||||
public function authenticate(string $email, string $password): ?LdapIdentity;
|
||||
|
||||
/**
|
||||
* The address of the one entry whose username attribute matches, found
|
||||
* with the service account. No bind as the person, so nothing is
|
||||
* verified here: the caller still signs in by that address, through
|
||||
* authenticate(). Null for no match, more than one, or any failure.
|
||||
*/
|
||||
public function emailForUsername(string $username): ?string;
|
||||
|
||||
/**
|
||||
* Exercise the configuration and report which stage failed, for the
|
||||
* settings screen's test button. This is the one place that is allowed
|
||||
|
||||
@@ -42,7 +42,7 @@ class LdapRecordDirectory implements LdapDirectory
|
||||
$connection = LdapConnectionFactory::make($settings);
|
||||
$connection->connect();
|
||||
|
||||
$entry = $this->findEntry($connection, $settings, $email);
|
||||
$entry = $this->findEntry($connection, $settings, $settings->email_attribute, $email);
|
||||
|
||||
if ($entry === null) {
|
||||
return null;
|
||||
@@ -66,6 +66,28 @@ class LdapRecordDirectory implements LdapDirectory
|
||||
}
|
||||
}
|
||||
|
||||
public function emailForUsername(string $username): ?string
|
||||
{
|
||||
$settings = LdapSettings::current();
|
||||
|
||||
if (! $settings->allowsUsernameSignIn()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$connection = LdapConnectionFactory::make($settings);
|
||||
$connection->connect();
|
||||
|
||||
$entry = $this->findEntry($connection, $settings, (string) $settings->username_attribute, $username);
|
||||
|
||||
return $entry === null ? null : $this->attribute($entry, $settings->email_attribute);
|
||||
} catch (Throwable $e) {
|
||||
Log::warning('LDAP username lookup could not be completed.', ['exception' => $e::class]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function probe(?string $email = null, ?string $password = null): LdapProbeResult
|
||||
{
|
||||
$settings = LdapSettings::current();
|
||||
@@ -95,7 +117,7 @@ class LdapRecordDirectory implements LdapDirectory
|
||||
}
|
||||
|
||||
try {
|
||||
$entry = $this->findEntry($connection, $settings, $email);
|
||||
$entry = $this->findEntry($connection, $settings, $settings->email_attribute, $email);
|
||||
} catch (Throwable $e) {
|
||||
return LdapProbeResult::failed(
|
||||
LdapProbeResult::STAGE_SEARCH,
|
||||
@@ -131,21 +153,22 @@ class LdapRecordDirectory implements LdapDirectory
|
||||
}
|
||||
|
||||
/**
|
||||
* The one entry matching this address, or null.
|
||||
* The one entry whose attribute holds this value (an address, or a
|
||||
* username), or null.
|
||||
*
|
||||
* Two results is a misconfiguration — two objects sharing an address —
|
||||
* and choosing one of them is how you sign the wrong person in, so it
|
||||
* fails closed.
|
||||
* Two results is a misconfiguration — two objects sharing an address or
|
||||
* a username — and choosing one of them is how you sign the wrong person
|
||||
* in, so it fails closed.
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
private function findEntry(Connection $connection, LdapSettings $settings, string $email): ?array
|
||||
private function findEntry(Connection $connection, LdapSettings $settings, string $attribute, string $value): ?array
|
||||
{
|
||||
$query = $connection->query()
|
||||
->in($settings->base_dn)
|
||||
// The email goes through the builder, which escapes it. It is
|
||||
// never concatenated into a filter string.
|
||||
->whereEquals($settings->email_attribute, $email);
|
||||
// What the visitor typed goes through the builder, which
|
||||
// escapes it. It is never concatenated into a filter string.
|
||||
->whereEquals($attribute, $value);
|
||||
|
||||
if (is_string($settings->user_filter) && $settings->user_filter !== '') {
|
||||
// Admin-supplied, never visitor-supplied.
|
||||
|
||||
@@ -25,6 +25,7 @@ use Illuminate\Database\Eloquent\Model;
|
||||
* @property string|null $user_filter
|
||||
* @property string $email_attribute
|
||||
* @property string $name_attribute
|
||||
* @property string|null $username_attribute
|
||||
* @property bool $auto_provision
|
||||
* @property bool $auto_approve
|
||||
*/
|
||||
@@ -80,4 +81,15 @@ class LdapSettings extends Model
|
||||
&& is_string($this->host) && $this->host !== ''
|
||||
&& is_string($this->base_dn) && $this->base_dn !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether people may sign in with a directory username as well as an
|
||||
* address: only once an administrator has named the attribute that
|
||||
* holds it.
|
||||
*/
|
||||
public function allowsUsernameSignIn(): bool
|
||||
{
|
||||
return $this->usable()
|
||||
&& is_string($this->username_attribute) && $this->username_attribute !== '';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user