first commit

This commit is contained in:
2025-06-17 11:53:18 +02:00
commit 9f0f7ba12b
8804 changed files with 1369176 additions and 0 deletions

View File

@ -0,0 +1,115 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Security;
use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Ldap\Exception\InvalidCredentialsException;
use Symfony\Component\Ldap\Exception\InvalidSearchCredentialsException;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\LogicException;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
/**
* Verifies password credentials using an LDAP service whenever the
* LdapBadge is attached to the Security passport.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class CheckLdapCredentialsListener implements EventSubscriberInterface
{
private ContainerInterface $ldapLocator;
public function __construct(ContainerInterface $ldapLocator)
{
$this->ldapLocator = $ldapLocator;
}
/**
* @return void
*/
public function onCheckPassport(CheckPassportEvent $event)
{
$passport = $event->getPassport();
if (!$passport->hasBadge(LdapBadge::class)) {
return;
}
/** @var LdapBadge $ldapBadge */
$ldapBadge = $passport->getBadge(LdapBadge::class);
if ($ldapBadge->isResolved()) {
return;
}
if (!$passport->hasBadge(PasswordCredentials::class)) {
throw new \LogicException(sprintf('LDAP authentication requires a passport containing password credentials, authenticator "%s" does not fulfill these requirements.', $event->getAuthenticator()::class));
}
/** @var PasswordCredentials $passwordCredentials */
$passwordCredentials = $passport->getBadge(PasswordCredentials::class);
if ($passwordCredentials->isResolved()) {
throw new \LogicException('LDAP authentication password verification cannot be completed because something else has already resolved the PasswordCredentials.');
}
if (!$this->ldapLocator->has($ldapBadge->getLdapServiceId())) {
throw new \LogicException(sprintf('Cannot check credentials using the "%s" ldap service, as such service is not found. Did you maybe forget to add the "ldap" service tag to this service?', $ldapBadge->getLdapServiceId()));
}
$presentedPassword = $passwordCredentials->getPassword();
if ('' === $presentedPassword) {
throw new BadCredentialsException('The presented password cannot be empty.');
}
$user = $passport->getUser();
/** @var LdapInterface $ldap */
$ldap = $this->ldapLocator->get($ldapBadge->getLdapServiceId());
try {
if ($ldapBadge->getQueryString()) {
if ('' !== $ldapBadge->getSearchDn() && '' !== $ldapBadge->getSearchPassword()) {
try {
$ldap->bind($ldapBadge->getSearchDn(), $ldapBadge->getSearchPassword());
} catch (InvalidCredentialsException) {
throw new InvalidSearchCredentialsException();
}
} else {
throw new LogicException('Using the "query_string" config without using a "search_dn" and a "search_password" is not supported.');
}
$identifier = $ldap->escape($user->getUserIdentifier(), '', LdapInterface::ESCAPE_FILTER);
$query = str_replace('{user_identifier}', $identifier, $ldapBadge->getQueryString());
$result = $ldap->query($ldapBadge->getDnString(), $query)->execute();
if (1 !== $result->count()) {
throw new BadCredentialsException('The presented user identifier is invalid.');
}
$dn = $result[0]->getDn();
} else {
$identifier = $ldap->escape($user->getUserIdentifier(), '', LdapInterface::ESCAPE_DN);
$dn = str_replace('{user_identifier}', $identifier, $ldapBadge->getDnString());
}
$ldap->bind($dn, $presentedPassword);
} catch (InvalidCredentialsException) {
throw new BadCredentialsException('The presented password is invalid.');
}
$passwordCredentials->markResolved();
$ldapBadge->markResolved();
}
public static function getSubscribedEvents(): array
{
return [CheckPassportEvent::class => ['onCheckPassport', 144]];
}
}

View File

@ -0,0 +1,108 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Http\EntryPoint\Exception\NotAnEntryPointException;
/**
* This class decorates internal authenticators to add the LDAP integration.
*
* In your own authenticators, it is recommended to directly use the
* LdapBadge in the authenticate() method. This class should only be
* used for Symfony or third party authenticators.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class LdapAuthenticator implements AuthenticationEntryPointInterface, InteractiveAuthenticatorInterface
{
private AuthenticatorInterface $authenticator;
private string $ldapServiceId;
private string $dnString;
private string $searchDn;
private string $searchPassword;
private string $queryString;
public function __construct(AuthenticatorInterface $authenticator, string $ldapServiceId, string $dnString = '{user_identifier}', string $searchDn = '', string $searchPassword = '', string $queryString = '')
{
$this->authenticator = $authenticator;
$this->ldapServiceId = $ldapServiceId;
$this->dnString = $dnString;
$this->searchDn = $searchDn;
$this->searchPassword = $searchPassword;
$this->queryString = $queryString;
}
public function supports(Request $request): ?bool
{
return $this->authenticator->supports($request);
}
public function authenticate(Request $request): Passport
{
$passport = $this->authenticator->authenticate($request);
$passport->addBadge(new LdapBadge($this->ldapServiceId, $this->dnString, $this->searchDn, $this->searchPassword, $this->queryString));
return $passport;
}
/**
* @internal
*/
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
{
throw new \BadMethodCallException(sprintf('The "%s()" method cannot be called.', __METHOD__));
}
public function createToken(Passport $passport, string $firewallName): TokenInterface
{
return $this->authenticator->createToken($passport, $firewallName);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return $this->authenticator->onAuthenticationFailure($request, $exception);
}
public function start(Request $request, AuthenticationException $authException = null): Response
{
if (!$this->authenticator instanceof AuthenticationEntryPointInterface) {
throw new NotAnEntryPointException(sprintf('Decorated authenticator "%s" does not implement interface "%s".', get_debug_type($this->authenticator), AuthenticationEntryPointInterface::class));
}
return $this->authenticator->start($request, $authException);
}
public function isInteractive(): bool
{
if ($this->authenticator instanceof InteractiveAuthenticatorInterface) {
return $this->authenticator->isInteractive();
}
return false;
}
}

View File

@ -0,0 +1,85 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Security;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
/**
* A badge indicating that the credentials should be checked using LDAP.
*
* This badge must be used together with PasswordCredentials.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*
* @final
*/
class LdapBadge implements BadgeInterface
{
private bool $resolved = false;
private string $ldapServiceId;
private string $dnString;
private string $searchDn;
private string $searchPassword;
private ?string $queryString;
public function __construct(string $ldapServiceId, string $dnString = '{user_identifier}', string $searchDn = '', string $searchPassword = '', string $queryString = null)
{
$this->ldapServiceId = $ldapServiceId;
$dnString = str_replace('{username}', '{user_identifier}', $dnString, $replaceCount);
if ($replaceCount > 0) {
trigger_deprecation('symfony/ldap', '6.2', 'Using "{username}" parameter in LDAP configuration is deprecated, consider using "{user_identifier}" instead.');
}
$this->dnString = $dnString;
$this->searchDn = $searchDn;
$this->searchPassword = $searchPassword;
$queryString = str_replace('{username}', '{user_identifier}', $queryString ?? '', $replaceCount);
if ($replaceCount > 0) {
trigger_deprecation('symfony/ldap', '6.2', 'Using "{username}" parameter in LDAP configuration is deprecated, consider using "{user_identifier}" instead.');
}
$this->queryString = $queryString;
}
public function getLdapServiceId(): string
{
return $this->ldapServiceId;
}
public function getDnString(): string
{
return $this->dnString;
}
public function getSearchDn(): string
{
return $this->searchDn;
}
public function getSearchPassword(): string
{
return $this->searchPassword;
}
public function getQueryString(): ?string
{
return $this->queryString;
}
public function markResolved(): void
{
$this->resolved = true;
}
public function isResolved(): bool
{
return $this->resolved;
}
}

View File

@ -0,0 +1,113 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Security;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @author Robin Chalas <robin.chalas@gmail.com>
*
* @final
*/
class LdapUser implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface
{
private Entry $entry;
private string $identifier;
private ?string $password;
private array $roles;
private array $extraFields;
public function __construct(Entry $entry, string $identifier, #[\SensitiveParameter] ?string $password, array $roles = [], array $extraFields = [])
{
if (!$identifier) {
throw new \InvalidArgumentException('The username cannot be empty.');
}
$this->entry = $entry;
$this->identifier = $identifier;
$this->password = $password;
$this->roles = $roles;
$this->extraFields = $extraFields;
}
public function getEntry(): Entry
{
return $this->entry;
}
public function getRoles(): array
{
return $this->roles;
}
public function getPassword(): ?string
{
return $this->password;
}
public function getSalt(): ?string
{
return null;
}
/**
* @internal for compatibility with Symfony 5.4
*/
public function getUsername(): string
{
return $this->getUserIdentifier();
}
public function getUserIdentifier(): string
{
return $this->identifier;
}
public function eraseCredentials(): void
{
$this->password = null;
}
public function getExtraFields(): array
{
return $this->extraFields;
}
public function setPassword(#[\SensitiveParameter] string $password): void
{
$this->password = $password;
}
public function isEqualTo(UserInterface $user): bool
{
if (!$user instanceof self) {
return false;
}
if ($this->getPassword() !== $user->getPassword()) {
return false;
}
if ($this->getSalt() !== $user->getSalt()) {
return false;
}
if ($this->getUserIdentifier() !== $user->getUserIdentifier()) {
return false;
}
return true;
}
}

View File

@ -0,0 +1,187 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Security;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Exception\ExceptionInterface;
use Symfony\Component\Ldap\Exception\InvalidCredentialsException;
use Symfony\Component\Ldap\Exception\InvalidSearchCredentialsException;
use Symfony\Component\Ldap\LdapInterface;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
/**
* LdapUserProvider is a simple user provider on top of LDAP.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
class LdapUserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
private LdapInterface $ldap;
private string $baseDn;
private ?string $searchDn;
private ?string $searchPassword;
private array $defaultRoles;
private ?string $uidKey;
private string $defaultSearch;
private ?string $passwordAttribute;
private array $extraFields;
public function __construct(LdapInterface $ldap, string $baseDn, string $searchDn = null, #[\SensitiveParameter] string $searchPassword = null, array $defaultRoles = [], string $uidKey = null, string $filter = null, string $passwordAttribute = null, array $extraFields = [])
{
$uidKey ??= 'sAMAccountName';
$filter ??= '({uid_key}={user_identifier})';
$this->ldap = $ldap;
$this->baseDn = $baseDn;
$this->searchDn = $searchDn;
$this->searchPassword = $searchPassword;
$this->defaultRoles = $defaultRoles;
$this->uidKey = $uidKey;
$this->defaultSearch = str_replace('{uid_key}', $uidKey, $filter);
$this->passwordAttribute = $passwordAttribute;
$this->extraFields = $extraFields;
}
/**
* @internal for compatibility with Symfony 5.4
*/
public function loadUserByUsername(string $username): UserInterface
{
return $this->loadUserByIdentifier($username);
}
public function loadUserByIdentifier(string $identifier): UserInterface
{
try {
$this->ldap->bind($this->searchDn, $this->searchPassword);
} catch (InvalidCredentialsException) {
throw new InvalidSearchCredentialsException();
}
$identifier = $this->ldap->escape($identifier, '', LdapInterface::ESCAPE_FILTER);
$query = str_replace('{username}', '{user_identifier}', $this->defaultSearch, $replaceCount);
if ($replaceCount > 0) {
trigger_deprecation('symfony/ldap', '6.2', 'Using "{username}" parameter in LDAP configuration is deprecated, consider using "{user_identifier}" instead.');
}
$query = str_replace('{user_identifier}', $identifier, $query);
$search = $this->ldap->query($this->baseDn, $query, ['filter' => 0 == \count($this->extraFields) ? '*' : $this->extraFields]);
$entries = $search->execute();
$count = \count($entries);
if (!$count) {
$e = new UserNotFoundException(sprintf('User "%s" not found.', $identifier));
$e->setUserIdentifier($identifier);
throw $e;
}
if ($count > 1) {
$e = new UserNotFoundException('More than one user found.');
$e->setUserIdentifier($identifier);
throw $e;
}
$entry = $entries[0];
try {
if (null !== $this->uidKey) {
$identifier = $this->getAttributeValue($entry, $this->uidKey);
}
} catch (InvalidArgumentException) {
}
return $this->loadUser($identifier, $entry);
}
public function refreshUser(UserInterface $user): UserInterface
{
if (!$user instanceof LdapUser) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
}
return new LdapUser($user->getEntry(), $user->getUserIdentifier(), $user->getPassword(), $user->getRoles(), $user->getExtraFields());
}
/**
* @final
*/
public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
{
if (!$user instanceof LdapUser) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user)));
}
if (null === $this->passwordAttribute) {
return;
}
try {
$user->getEntry()->setAttribute($this->passwordAttribute, [$newHashedPassword]);
$this->ldap->getEntryManager()->update($user->getEntry());
$user->setPassword($newHashedPassword);
} catch (ExceptionInterface) {
// ignore failed password upgrades
}
}
public function supportsClass(string $class): bool
{
return LdapUser::class === $class;
}
/**
* Loads a user from an LDAP entry.
*/
protected function loadUser(string $identifier, Entry $entry): UserInterface
{
$password = null;
$extraFields = [];
if (null !== $this->passwordAttribute) {
$password = $this->getAttributeValue($entry, $this->passwordAttribute);
}
foreach ($this->extraFields as $field) {
$extraFields[$field] = $this->getAttributeValue($entry, $field);
}
return new LdapUser($entry, $identifier, $password, $this->defaultRoles, $extraFields);
}
private function getAttributeValue(Entry $entry, string $attribute): mixed
{
if (!$entry->hasAttribute($attribute)) {
throw new InvalidArgumentException(sprintf('Missing attribute "%s" for user "%s".', $attribute, $entry->getDn()));
}
$values = $entry->getAttribute($attribute);
if (!\in_array($attribute, [$this->uidKey, $this->passwordAttribute])) {
return $values;
}
if (1 !== \count($values)) {
throw new InvalidArgumentException(sprintf('Attribute "%s" has multiple values.', $attribute));
}
return $values[0];
}
}