* * 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 * @author Charles Sarrazin * @author Robin Chalas * * @template-implements UserProviderInterface */ 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]; } }