From 48013cfa9d394343162dae7da914112a6206b575 Mon Sep 17 00:00:00 2001 From: Wouter de Jong Date: Sat, 16 Nov 2024 15:49:06 +0100 Subject: [PATCH 1/8] Proofread UPGRADE guide --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4539de0..efdb472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ CHANGELOG --- * Add methods for `saslBind()` and `whoami()` to `ConnectionInterface` and `LdapInterface` - * Deprecate the `sizeLimit` option of `AbstractQuery` + * Deprecate the `sizeLimit` option of `AbstractQuery`, the option is unused 7.1 --- From 1d5d845495122d9899aedc478aac17aa0f3f65c1 Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Wed, 11 Dec 2024 14:08:35 +0100 Subject: [PATCH 2/8] chore: PHP CS Fixer fixes --- Adapter/ExtLdap/Connection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Adapter/ExtLdap/Connection.php b/Adapter/ExtLdap/Connection.php index 3f75715..9ec6d3a 100644 --- a/Adapter/ExtLdap/Connection.php +++ b/Adapter/ExtLdap/Connection.php @@ -71,7 +71,7 @@ public function bind(?string $dn = null, #[\SensitiveParameter] ?string $passwor if (false === @ldap_bind($this->connection, $dn, $password)) { $error = ldap_error($this->connection); - ldap_get_option($this->connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagnostic); + ldap_get_option($this->connection, \LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagnostic); throw match (ldap_errno($this->connection)) { self::LDAP_INVALID_CREDENTIALS => new InvalidCredentialsException($error), @@ -99,7 +99,7 @@ public function saslBind(?string $dn = null, #[\SensitiveParameter] ?string $pas if (false === @ldap_sasl_bind($this->connection, $dn, $password, $mech, $realm, $authcId, $authzId, $props)) { $error = ldap_error($this->connection); - ldap_get_option($this->connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagnostic); + ldap_get_option($this->connection, \LDAP_OPT_DIAGNOSTIC_MESSAGE, $diagnostic); throw match (ldap_errno($this->connection)) { self::LDAP_INVALID_CREDENTIALS => new InvalidCredentialsException($error), From 8e83a641b7f6419d3e008a8eda2c7b9a5a207dbb Mon Sep 17 00:00:00 2001 From: Dariusz Ruminski Date: Fri, 10 Jan 2025 15:17:09 +0100 Subject: [PATCH 3/8] chore: PHP CS Fixer fixes --- LdapInterface.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/LdapInterface.php b/LdapInterface.php index 8cfe8a4..3c211a9 100644 --- a/LdapInterface.php +++ b/LdapInterface.php @@ -38,12 +38,12 @@ public function bind(?string $dn = null, #[\SensitiveParameter] ?string $passwor * * @throws ConnectionException if dn / password could not be bound */ - // public function saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null): void; + // public function saslBind(?string $dn = null, #[\SensitiveParameter] ?string $password = null, ?string $mech = null, ?string $realm = null, ?string $authcId = null, ?string $authzId = null, ?string $props = null): void; /** * Returns authenticated and authorized (for SASL) DN. */ - // public function whoami(): string; + // public function whoami(): string; /** * Queries a ldap server for entries matching the given criteria. From 4b84f8701ca41f1300450cb40d49518183d73e76 Mon Sep 17 00:00:00 2001 From: Robin Chalas Date: Fri, 6 Dec 2024 11:20:22 +0100 Subject: [PATCH 4/8] [Security] Deprecate `UserInterface` & `TokenInterface`'s `eraseCredentials()` --- CHANGELOG.md | 6 +++ Security/EraseLdapUserCredentialsListener.php | 48 +++++++++++++++++ Security/LdapUser.php | 14 ++++- .../EraseLdapUserCredentialsListenerTest.php | 53 +++++++++++++++++++ composer.json | 1 + 5 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 Security/EraseLdapUserCredentialsListener.php create mode 100644 Tests/Security/EraseLdapUserCredentialsListenerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index efdb472..a14b98c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ CHANGELOG ========= +7.3 +--- + + * Deprecate `LdapUser::eraseCredentials()`, use `LdapUser::setPassword(null)` instead + * Add `EraseLdapUserCredentialsListener` + 7.2 --- diff --git a/Security/EraseLdapUserCredentialsListener.php b/Security/EraseLdapUserCredentialsListener.php new file mode 100644 index 0000000..d10ba40 --- /dev/null +++ b/Security/EraseLdapUserCredentialsListener.php @@ -0,0 +1,48 @@ + + * + * 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\Event\AuthenticationSuccessEvent; +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\AuthenticationTokenCreatedEvent; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; + +/** + * Erases credentials from LdapUser instances upon successful authentication. + * + * @author Robin Chalas + */ +class EraseLdapUserCredentialsListener implements EventSubscriberInterface +{ + public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void + { + $user = $event->getAuthenticationToken()->getUser(); + + if (!$user instanceof LdapUser) { + return; + } + + $user->setPassword(null); + } + + public static function getSubscribedEvents(): array + { + return [AuthenticationSuccessEvent::class => ['onAuthenticationSuccess', 256]]; + } +} diff --git a/Security/LdapUser.php b/Security/LdapUser.php index a28320f..d930cb7 100644 --- a/Security/LdapUser.php +++ b/Security/LdapUser.php @@ -62,6 +62,8 @@ public function getUserIdentifier(): string public function eraseCredentials(): void { + trigger_deprecation('symfony/security-core', '7.3', sprintf('The "%s()" method is deprecated and will be removed in 8.0, call "setPassword(null)" instead.', __METHOD__)); + $this->password = null; } @@ -70,7 +72,7 @@ public function getExtraFields(): array return $this->extraFields; } - public function setPassword(#[\SensitiveParameter] string $password): void + public function setPassword(#[\SensitiveParameter] ?string $password): void { $this->password = $password; } @@ -95,4 +97,14 @@ public function isEqualTo(UserInterface $user): bool return true; } + + public function __serialize(): array + { + return [$this->entry, $this->identifier, null, $this->roles, $this->extraFields]; + } + + public function __unserialize(array $data): void + { + [$this->entry, $this->identifier, $this->password, $this->roles, $this->extraFields] = $data; + } } diff --git a/Tests/Security/EraseLdapUserCredentialsListenerTest.php b/Tests/Security/EraseLdapUserCredentialsListenerTest.php new file mode 100644 index 0000000..6d132c7 --- /dev/null +++ b/Tests/Security/EraseLdapUserCredentialsListenerTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Tests\Security; + +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Ldap\Adapter\CollectionInterface; +use Symfony\Component\Ldap\Adapter\QueryInterface; +use Symfony\Component\Ldap\Entry; +use Symfony\Component\Ldap\Exception\InvalidCredentialsException; +use Symfony\Component\Ldap\LdapInterface; +use Symfony\Component\Ldap\Security\CheckLdapCredentialsListener; +use Symfony\Component\Ldap\Security\EraseLdapUserCredentialsListener; +use Symfony\Component\Ldap\Security\LdapBadge; +use Symfony\Component\Ldap\Security\LdapUser; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\BadCredentialsException; +use Symfony\Component\Security\Core\User\InMemoryUser; +use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; +use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; +use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; +use Symfony\Component\Security\Http\Authenticator\Passport\Passport; +use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; +use Symfony\Component\Security\Http\Event\CheckPassportEvent; +use Symfony\Contracts\Service\ServiceLocatorTrait; + +class EraseLdapUserCredentialsListenerTest extends TestCase +{ + public function testPasswordIsErasedOnAuthenticationSuccess() + { + $user = new LdapUser(new Entry(''), 'chalasr', 'password'); + $listener = new EraseLdapUserCredentialsListener(); + + $listener->onAuthenticationSuccess(new AuthenticationSuccessEvent(new UsernamePasswordToken($user, 'main'))); + + $this->assertSame(null, $user->getPassword()); + } +} diff --git a/composer.json b/composer.json index 5ed2995..2867afa 100644 --- a/composer.json +++ b/composer.json @@ -18,6 +18,7 @@ "require": { "php": ">=8.2", "ext-ldap": "*", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/options-resolver": "^6.4|^7.0" }, "require-dev": { From 52dad31dd3525e74110659e0766924e588da6850 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 3 Feb 2025 18:35:30 +0100 Subject: [PATCH 5/8] [Security] Improve BC-layer to deprecate eraseCredentials methods --- CHANGELOG.md | 3 +- Security/EraseLdapUserCredentialsListener.php | 48 ----------------- Security/LdapUser.php | 16 +++--- .../EraseLdapUserCredentialsListenerTest.php | 53 ------------------- composer.json | 1 - 5 files changed, 11 insertions(+), 110 deletions(-) delete mode 100644 Security/EraseLdapUserCredentialsListener.php delete mode 100644 Tests/Security/EraseLdapUserCredentialsListenerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a14b98c..ff9cedd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,7 @@ CHANGELOG 7.3 --- - * Deprecate `LdapUser::eraseCredentials()`, use `LdapUser::setPassword(null)` instead - * Add `EraseLdapUserCredentialsListener` + * Deprecate `LdapUser::eraseCredentials()` in favor of `__serialize()` 7.2 --- diff --git a/Security/EraseLdapUserCredentialsListener.php b/Security/EraseLdapUserCredentialsListener.php deleted file mode 100644 index d10ba40..0000000 --- a/Security/EraseLdapUserCredentialsListener.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * 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\Event\AuthenticationSuccessEvent; -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\AuthenticationTokenCreatedEvent; -use Symfony\Component\Security\Http\Event\CheckPassportEvent; - -/** - * Erases credentials from LdapUser instances upon successful authentication. - * - * @author Robin Chalas - */ -class EraseLdapUserCredentialsListener implements EventSubscriberInterface -{ - public function onAuthenticationSuccess(AuthenticationSuccessEvent $event): void - { - $user = $event->getAuthenticationToken()->getUser(); - - if (!$user instanceof LdapUser) { - return; - } - - $user->setPassword(null); - } - - public static function getSubscribedEvents(): array - { - return [AuthenticationSuccessEvent::class => ['onAuthenticationSuccess', 256]]; - } -} diff --git a/Security/LdapUser.php b/Security/LdapUser.php index d930cb7..ef73b82 100644 --- a/Security/LdapUser.php +++ b/Security/LdapUser.php @@ -60,9 +60,15 @@ public function getUserIdentifier(): string return $this->identifier; } + /** + * @deprecated since Symfony 7.3 + */ + #[\Deprecated(since: 'symfony/ldap 7.3')] public function eraseCredentials(): void { - trigger_deprecation('symfony/security-core', '7.3', sprintf('The "%s()" method is deprecated and will be removed in 8.0, call "setPassword(null)" instead.', __METHOD__)); + if (\PHP_VERSION_ID < 80400) { + @trigger_error(\sprintf('Method %s::eraseCredentials() is deprecated since symfony/ldap 7.3', self::class), \E_USER_DEPRECATED); + } $this->password = null; } @@ -100,11 +106,9 @@ public function isEqualTo(UserInterface $user): bool public function __serialize(): array { - return [$this->entry, $this->identifier, null, $this->roles, $this->extraFields]; - } + $data = (array) $this; + unset($data[\sprintf("\0%s\0password", self::class)]); - public function __unserialize(array $data): void - { - [$this->entry, $this->identifier, $this->password, $this->roles, $this->extraFields] = $data; + return $data; } } diff --git a/Tests/Security/EraseLdapUserCredentialsListenerTest.php b/Tests/Security/EraseLdapUserCredentialsListenerTest.php deleted file mode 100644 index 6d132c7..0000000 --- a/Tests/Security/EraseLdapUserCredentialsListenerTest.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Ldap\Tests\Security; - -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; -use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Ldap\Adapter\CollectionInterface; -use Symfony\Component\Ldap\Adapter\QueryInterface; -use Symfony\Component\Ldap\Entry; -use Symfony\Component\Ldap\Exception\InvalidCredentialsException; -use Symfony\Component\Ldap\LdapInterface; -use Symfony\Component\Ldap\Security\CheckLdapCredentialsListener; -use Symfony\Component\Ldap\Security\EraseLdapUserCredentialsListener; -use Symfony\Component\Ldap\Security\LdapBadge; -use Symfony\Component\Ldap\Security\LdapUser; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; -use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; -use Symfony\Component\Security\Core\Exception\AuthenticationException; -use Symfony\Component\Security\Core\Exception\BadCredentialsException; -use Symfony\Component\Security\Core\User\InMemoryUser; -use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface; -use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; -use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; -use Symfony\Component\Security\Http\Authenticator\Passport\Passport; -use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; -use Symfony\Component\Security\Http\Event\CheckPassportEvent; -use Symfony\Contracts\Service\ServiceLocatorTrait; - -class EraseLdapUserCredentialsListenerTest extends TestCase -{ - public function testPasswordIsErasedOnAuthenticationSuccess() - { - $user = new LdapUser(new Entry(''), 'chalasr', 'password'); - $listener = new EraseLdapUserCredentialsListener(); - - $listener->onAuthenticationSuccess(new AuthenticationSuccessEvent(new UsernamePasswordToken($user, 'main'))); - - $this->assertSame(null, $user->getPassword()); - } -} diff --git a/composer.json b/composer.json index 2867afa..5ed2995 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,6 @@ "require": { "php": ">=8.2", "ext-ldap": "*", - "symfony/deprecation-contracts": "^2.5|^3", "symfony/options-resolver": "^6.4|^7.0" }, "require-dev": { From 265a5dde2c2cb67fe7ae0c13da545bf58ddbe03b Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Fri, 7 Feb 2025 21:05:10 +0100 Subject: [PATCH 6/8] [Security] Ability to add roles in form_login_ldap by ldap group This update allows LDAP to fetch roles for a given user entry by using the new RoleFetcherInterface. The LdapUserProvider class has been adjusted to use this new functionality. --- CHANGELOG.md | 2 + Security/AssignDefaultRoles.php | 33 +++++++++++++ Security/LdapUserProvider.php | 8 +++- Security/MemberOfRoles.php | 56 ++++++++++++++++++++++ Security/RoleFetcherInterface.php | 25 ++++++++++ Tests/Security/LdapUserProviderTest.php | 62 +++++++++++++++++++++++++ 6 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 Security/AssignDefaultRoles.php create mode 100644 Security/MemberOfRoles.php create mode 100644 Security/RoleFetcherInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ff9cedd..9f1bb9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ CHANGELOG --- * Deprecate `LdapUser::eraseCredentials()` in favor of `__serialize()` + * Add `RoleFetcherInterface` to allow roles fetching at user loading + * Add ability to fetch LDAP roles 7.2 --- diff --git a/Security/AssignDefaultRoles.php b/Security/AssignDefaultRoles.php new file mode 100644 index 0000000..351b52d --- /dev/null +++ b/Security/AssignDefaultRoles.php @@ -0,0 +1,33 @@ + + * + * 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; + +final readonly class AssignDefaultRoles implements RoleFetcherInterface +{ + /** + * @param string[] $roles + */ + public function __construct( + private array $roles, + ) { + } + + /** + * @return string[] + */ + public function fetchRoles(Entry $entry): array + { + return $this->roles; + } +} diff --git a/Security/LdapUserProvider.php b/Security/LdapUserProvider.php index 211f2ac..5c0f478 100644 --- a/Security/LdapUserProvider.php +++ b/Security/LdapUserProvider.php @@ -37,13 +37,14 @@ class LdapUserProvider implements UserProviderInterface, PasswordUpgraderInterfa { private string $uidKey; private string $defaultSearch; + private RoleFetcherInterface $roleFetcher; public function __construct( private LdapInterface $ldap, private string $baseDn, private ?string $searchDn = null, #[\SensitiveParameter] private ?string $searchPassword = null, - private array $defaultRoles = [], + array|RoleFetcherInterface $defaultRoles = [], ?string $uidKey = null, ?string $filter = null, private ?string $passwordAttribute = null, @@ -54,6 +55,7 @@ public function __construct( $this->uidKey = $uidKey; $this->defaultSearch = str_replace('{uid_key}', $uidKey, $filter); + $this->roleFetcher = \is_array($defaultRoles) ? new AssignDefaultRoles($defaultRoles) : $defaultRoles; } public function loadUserByIdentifier(string $identifier): UserInterface @@ -147,7 +149,9 @@ protected function loadUser(string $identifier, Entry $entry): UserInterface $extraFields[$field] = $this->getAttributeValue($entry, $field); } - return new LdapUser($entry, $identifier, $password, $this->defaultRoles, $extraFields); + $roles = $this->roleFetcher->fetchRoles($entry); + + return new LdapUser($entry, $identifier, $password, $roles, $extraFields); } private function getAttributeValue(Entry $entry, string $attribute): mixed diff --git a/Security/MemberOfRoles.php b/Security/MemberOfRoles.php new file mode 100644 index 0000000..166274a --- /dev/null +++ b/Security/MemberOfRoles.php @@ -0,0 +1,56 @@ + + * + * 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; + +final readonly class MemberOfRoles implements RoleFetcherInterface +{ + /** + * @param array $mapping + */ + public function __construct( + private array $mapping, + private string $attributeName = 'ismemberof', + private string $groupNameRegex = '/^CN=(?P[^,]+),ou.*$/i', + ) { + } + + /** + * @return string[] + */ + public function fetchRoles(Entry $entry): array + { + if (!$entry->hasAttribute($this->attributeName)) { + return []; + } + + $roles = []; + foreach ($entry->getAttribute($this->attributeName) as $group) { + $groupName = $this->getGroupName($group); + if (\array_key_exists($groupName, $this->mapping)) { + $roles[] = $this->mapping[$groupName]; + } + } + + return array_unique($roles); + } + + private function getGroupName(string $group): string + { + if (preg_match($this->groupNameRegex, $group, $matches)) { + return $matches['group']; + } + + return $group; + } +} diff --git a/Security/RoleFetcherInterface.php b/Security/RoleFetcherInterface.php new file mode 100644 index 0000000..29bf3b9 --- /dev/null +++ b/Security/RoleFetcherInterface.php @@ -0,0 +1,25 @@ + + * + * 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; + +/** + * Fetches LDAP roles for a given entry. + */ +interface RoleFetcherInterface +{ + /** + * @return string[] The list of roles + */ + public function fetchRoles(Entry $entry): array; +} diff --git a/Tests/Security/LdapUserProviderTest.php b/Tests/Security/LdapUserProviderTest.php index 9095535..0e35c32 100644 --- a/Tests/Security/LdapUserProviderTest.php +++ b/Tests/Security/LdapUserProviderTest.php @@ -19,6 +19,8 @@ use Symfony\Component\Ldap\LdapInterface; use Symfony\Component\Ldap\Security\LdapUser; use Symfony\Component\Ldap\Security\LdapUserProvider; +use Symfony\Component\Ldap\Security\MemberOfRoles; +use Symfony\Component\Ldap\Security\RoleFetcherInterface; use Symfony\Component\Security\Core\Exception\InvalidArgumentException; use Symfony\Component\Security\Core\Exception\UserNotFoundException; @@ -388,4 +390,64 @@ public function testRefreshUserShouldReturnUserWithSameProperties() $this->assertEquals($user, $provider->refreshUser($user)); } + + public function testLoadUserWithCorrectRoles() + { + // Given + $result = $this->createMock(CollectionInterface::class); + $query = $this->createMock(QueryInterface::class); + $query + ->method('execute') + ->willReturn($result) + ; + $ldap = $this->createMock(LdapInterface::class); + $result + ->method('offsetGet') + ->with(0) + ->willReturn(new Entry('foo', ['sAMAccountName' => ['foo']])) + ; + $result + ->method('count') + ->willReturn(1) + ; + $ldap + ->method('escape') + ->willReturn('foo') + ; + $ldap + ->method('query') + ->willReturn($query) + ; + $roleFetcher = $this->createMock(RoleFetcherInterface::class); + $roleFetcher + ->method('fetchRoles') + ->willReturn(['ROLE_FOO', 'ROLE_BAR']) + ; + + $provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com', defaultRoles: $roleFetcher); + + // When + $user = $provider->loadUserByIdentifier('foo'); + + // Then + $this->assertInstanceOf(LdapUser::class, $user); + $this->assertSame(['ROLE_FOO', 'ROLE_BAR'], $user->getRoles()); + } + + public function testMemberOfRoleFetch() + { + // Given + $roleFetcher = new MemberOfRoles( + ['Staff' => 'ROLE_STAFF', 'Admin' => 'ROLE_ADMIN'], + 'memberOf' + ); + + $entry = new Entry('uid=elliot.alderson,ou=staff,ou=people,dc=example,dc=com', ['memberOf' => ['cn=Staff,ou=Groups,dc=example,dc=com', 'cn=Admin,ou=Groups,dc=example,dc=com']]); + + // When + $roles = $roleFetcher->fetchRoles($entry); + + // Then + $this->assertSame(['ROLE_STAFF', 'ROLE_ADMIN'], $roles); + } } From 140dd47af79cc16332028ad969429dd13ac54235 Mon Sep 17 00:00:00 2001 From: Yonel Ceruto Date: Sat, 25 Jan 2025 22:08:40 -0500 Subject: [PATCH 7/8] Add setOptions method --- Adapter/ExtLdap/Connection.php | 2 +- composer.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Adapter/ExtLdap/Connection.php b/Adapter/ExtLdap/Connection.php index 9ec6d3a..d3dd908 100644 --- a/Adapter/ExtLdap/Connection.php +++ b/Adapter/ExtLdap/Connection.php @@ -170,7 +170,7 @@ protected function configureOptions(OptionsResolver $resolver): void $resolver->setAllowedTypes('debug', 'bool'); $resolver->setDefault('referrals', false); $resolver->setAllowedTypes('referrals', 'bool'); - $resolver->setDefault('options', function (OptionsResolver $options, Options $parent) { + $resolver->setOptions('options', function (OptionsResolver $options, Options $parent) { $options->setDefined(array_map('strtolower', array_keys((new \ReflectionClass(ConnectionOptions::class))->getConstants()))); if (true === $parent['debug']) { diff --git a/composer.json b/composer.json index 5ed2995..535ad18 100644 --- a/composer.json +++ b/composer.json @@ -18,14 +18,13 @@ "require": { "php": ">=8.2", "ext-ldap": "*", - "symfony/options-resolver": "^6.4|^7.0" + "symfony/options-resolver": "^7.3" }, "require-dev": { "symfony/security-core": "^6.4|^7.0", "symfony/security-http": "^6.4|^7.0" }, "conflict": { - "symfony/options-resolver": "<6.4", "symfony/security-core": "<6.4" }, "autoload": { From ee23db6241e4ddaec316220e3a1d5884262e27d0 Mon Sep 17 00:00:00 2001 From: matlec Date: Mon, 2 Jun 2025 10:17:40 +0200 Subject: [PATCH 8/8] [Ldap] Fix `LdapUser::isEqualTo` --- Security/LdapUser.php | 4 ++-- Tests/Security/LdapUserTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 Tests/Security/LdapUserTest.php diff --git a/Security/LdapUser.php b/Security/LdapUser.php index ef73b82..020fcb5 100644 --- a/Security/LdapUser.php +++ b/Security/LdapUser.php @@ -47,7 +47,7 @@ public function getRoles(): array public function getPassword(): ?string { - return $this->password; + return $this->password ?? null; } public function getSalt(): ?string @@ -89,7 +89,7 @@ public function isEqualTo(UserInterface $user): bool return false; } - if ($this->getPassword() !== $user->getPassword()) { + if (($this->getPassword() ?? $user->getPassword()) !== $user->getPassword()) { return false; } diff --git a/Tests/Security/LdapUserTest.php b/Tests/Security/LdapUserTest.php new file mode 100644 index 0000000..0a696bc --- /dev/null +++ b/Tests/Security/LdapUserTest.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Tests\Security; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Ldap\Entry; +use Symfony\Component\Ldap\Security\LdapUser; + +class LdapUserTest extends TestCase +{ + public function testIsEqualToWorksOnUnserializedUser() + { + $user = new LdapUser(new Entry('uid=jonhdoe,ou=MyBusiness,dc=symfony,dc=com', []), 'jonhdoe', 'p455w0rd'); + $unserializedUser = unserialize(serialize($user)); + + $this->assertTrue($unserializedUser->isEqualTo($user)); + } +}