Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[Ldap] Allow to use ldap in a chain provider #51231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 7.3
Choose a base branch
Loading
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions 23 src/Symfony/Component/Ldap/Attribute/WithLdapPassword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?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\Attribute;

/**
* Marker to allow or not using ldap credentials for a non-ldap user.
*
* @author Laurent VOULLEMIER <laurent.voullemier@gmail.com>
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class WithLdapPassword
{
public function __construct(public readonly bool $enabled = true) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@

use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Ldap\Attribute\WithLdapPassword;
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\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;

Expand Down Expand Up @@ -48,9 +50,6 @@ public function onCheckPassport(CheckPassportEvent $event)

/** @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));
Expand All @@ -72,6 +71,16 @@ public function onCheckPassport(CheckPassportEvent $event)
}

$user = $passport->getUser();
$nonLdapUserWithoutLdapPasswordAttribute = false;
if (!$user instanceof LdapUser) {
$reflectionClass = new \ReflectionClass($user);
$attr = $reflectionClass->getAttributes(WithLdapPassword::class);

$nonLdapUserWithoutLdapPasswordAttribute = count($attr) === 0;
if (!$nonLdapUserWithoutLdapPasswordAttribute && !$attr[0]->newInstance()->enabled) {
return;
}
}

/** @var LdapInterface $ldap */
$ldap = $this->ldapLocator->get($ldapBadge->getLdapServiceId());
Expand Down Expand Up @@ -104,8 +113,11 @@ public function onCheckPassport(CheckPassportEvent $event)
throw new BadCredentialsException('The presented password is invalid.');
}

if ($nonLdapUserWithoutLdapPasswordAttribute) {
trigger_deprecation('symfony/ldap', '6.4', 'Authenticate a user that is not an instance of %s is deprecated and won\'t be the default behavior anymore in 7.0. Use the %s attribute to keep this behavior.', LdapUser::class, WithLdapPassword::class);
}

$passwordCredentials->markResolved();
$ldapBadge->markResolved();
}

public static function getSubscribedEvents(): array
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public function supports(Request $request): ?bool
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));
$passport->addBadge(new LdapBadge($this->ldapServiceId, $this->dnString, $this->searchDn, $this->searchPassword, $this->queryString, true));

return $passport;
}
Expand Down
10 changes: 8 additions & 2 deletions 10 src/Symfony/Component/Ldap/Security/LdapBadge.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@
*/
class LdapBadge implements BadgeInterface
{
private bool $resolved = false;
private bool $resolved = true;
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)
public function __construct(string $ldapServiceId, string $dnString = '{user_identifier}', string $searchDn = '', string $searchPassword = '', string $queryString = null, bool $resolved = false)
{
$this->ldapServiceId = $ldapServiceId;
$dnString = str_replace('{username}', '{user_identifier}', $dnString, $replaceCount);
Expand All @@ -46,6 +46,10 @@ public function __construct(string $ldapServiceId, string $dnString = '{user_ide
trigger_deprecation('symfony/ldap', '6.2', 'Using "{username}" parameter in LDAP configuration is deprecated, consider using "{user_identifier}" instead.');
}
$this->queryString = $queryString;
$this->resolved = $resolved;
if (false === $this->resolved) {
trigger_deprecation('symfony/ldap', '6.4', 'Passing "false" as resolved initial value is deprecated, use "true" instead.');
}
}

public function getLdapServiceId(): string
Expand Down Expand Up @@ -75,6 +79,8 @@ public function getQueryString(): ?string

public function markResolved(): void
{
trigger_deprecation('symfony/ldap', '6.4', '%s is deprecated and will be removed in 7.0. %s is intended to bear LDAP information and doesn\'t need to be resolved anymore.', __METHOD__, __CLASS__);

$this->resolved = true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,23 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
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\Attribute\WithLdapPassword;
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\LdapBadge;
use Symfony\Component\Ldap\Security\LdapUser;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
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\Core\User\UserInterface;
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;
Expand All @@ -38,6 +42,7 @@

class CheckLdapCredentialsListenerTest extends TestCase
{
use ExpectDeprecationTrait;
private MockObject&LdapInterface $ldap;

protected function setUp(): void
Expand All @@ -46,25 +51,42 @@ protected function setUp(): void
}

/**
* @dataProvider provideShouldNotCheckPassport
* @group legacy
*
* @dataProvider provideShouldCheckPassport
*/
public function testShouldNotCheckPassport($authenticator, $passport)
public function testShouldCheckPassport(AuthenticatorInterface $authenticator, Passport $passport, bool $expectedBindCalled, bool $expectDeprecation)
{
$this->ldap->expects($this->never())->method('bind');
if ($expectDeprecation) {
$this->expectDeprecation('Since symfony/ldap 6.4: Authenticate a user that is not an instance of Symfony\Component\Ldap\Security\LdapUser is deprecated and won\'t be the default behavior anymore in 7.0. Use the Symfony\Component\Ldap\Attribute\WithLdapPassword attribute to keep this behavior.');
}

if ($expectedBindCalled) {
$this->ldap->expects($this->once())->method('bind');
} else {
$this->ldap->expects($this->never())->method('bind');
}

$listener = $this->createListener();
$listener->onCheckPassport(new CheckPassportEvent($authenticator, $passport));
}

public static function provideShouldNotCheckPassport()
public static function provideShouldCheckPassport()
{
// no LdapBadge
yield [new TestAuthenticator(), new Passport(new UserBadge('test'), new PasswordCredentials('s3cret'))];
yield 'no LdapBadge' => [new TestAuthenticator(), new Passport(new UserBadge('test'), new PasswordCredentials('s3cret')), false, false];

// ldap already resolved
$badge = new LdapBadge('app.ldap');
$badge->markResolved();
yield [new TestAuthenticator(), new Passport(new UserBadge('test'), new PasswordCredentials('s3cret'), [$badge])];
$ldapBadge = new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true);
$userBadge = new UserBadge('test');
$userBadge->setUserLoader(function () { return new InMemoryUser('test', 'pass', ['ROLE_USER']); });
yield 'non ldap user' => [new TestAuthenticator(), new Passport($userBadge, new PasswordCredentials('s3cret'), [$ldapBadge]), true, true];

$userBadge = new UserBadge('test');
$userBadge->setUserLoader(function () { return new UserWithLdapPasswordEnabled('test', ['ROLE_USER']); });
yield 'withLdapPassword enabled' => [new TestAuthenticator(), new Passport($userBadge, new PasswordCredentials('s3cret'), [$ldapBadge]), true, false];

$userBadge = new UserBadge('test');
$userBadge->setUserLoader(function () { return new UserWithLdapPasswordDisabled('test', ['ROLE_USER']); });
yield 'withLdapPassword disabled' => [new TestAuthenticator(), new Passport($userBadge, new PasswordCredentials('s3cret'), [$ldapBadge]), false, false];
}

public function testPasswordCredentialsAlreadyResolvedThrowsException()
Expand All @@ -74,7 +96,7 @@ public function testPasswordCredentialsAlreadyResolvedThrowsException()

$badge = new PasswordCredentials('s3cret');
$badge->markResolved();
$passport = new Passport(new UserBadge('test'), $badge, [new LdapBadge('app.ldap')]);
$passport = new Passport(new UserBadge('test'), $badge, [new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true)]);

$listener = $this->createListener();
$listener->onCheckPassport(new CheckPassportEvent(new TestAuthenticator(), $passport));
Expand All @@ -86,7 +108,7 @@ public function testInvalidLdapServiceId()
$this->expectExceptionMessage('Cannot check credentials using the "not_existing_ldap_service" ldap service, as such service is not found. Did you maybe forget to add the "ldap" service tag to this service?');

$listener = $this->createListener();
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('not_existing_ldap_service')));
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('not_existing_ldap_service', '{user_identifier}', '', '', null, true)));
}

/**
Expand All @@ -104,7 +126,10 @@ public function testWrongPassport($passport)
public static function provideWrongPassportData()
{
// no password credentials
yield [new SelfValidatingPassport(new UserBadge('test'), [new LdapBadge('app.ldap')])];
yield [new SelfValidatingPassport(
new UserBadge('test'),
[new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true)]
)];
}

public function testEmptyPasswordShouldThrowAnException()
Expand Down Expand Up @@ -171,6 +196,9 @@ public static function queryForDnProvider(): iterable
yield ['{user_identifier}', '{user_identifier}_test'];
}

/**
* @group legacy
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this becoming a legacy tests ? Why would we remove this test when removing deprecated features related to this PR ?

*/
public function testQueryForDn()
{
$collection = new class([new Entry('')]) extends \ArrayObject implements CollectionInterface {
Expand Down Expand Up @@ -198,7 +226,7 @@ public function toArray(): array
$this->ldap->expects($this->once())->method('query')->with('{user_identifier}', 'wouter_test')->willReturn($query);

$listener = $this->createListener();
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test')));
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test', true)));
}

public function testEmptyQueryResultShouldThrowAnException()
Expand Down Expand Up @@ -226,14 +254,21 @@ public function testEmptyQueryResultShouldThrowAnException()
$this->ldap->expects($this->once())->method('query')->willReturn($query);

$listener = $this->createListener();
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test')));
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{user_identifier}', 'elsa', 'test1234A$', '{user_identifier}_test', true)));
}

private function createEvent($password = 's3cr3t', $ldapBadge = null)
{
$ldapUser = new LdapUser(new Entry('cn=Wouter,dc=example,dc=com'), 'Wouter', null, ['ROLE_USER']);

/*return new CheckPassportEvent(
new TestAuthenticator(),
new Passport(new UserBadge('Wouter', fn () => $ldapUser), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true)])
);*/

return new CheckPassportEvent(
new TestAuthenticator(),
new Passport(new UserBadge('Wouter', fn () => new InMemoryUser('Wouter', null, ['ROLE_USER'])), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap')])
new Passport(new UserBadge('Wouter', fn () => new InMemoryUser('Wouter', null, ['ROLE_USER'])), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap', '{user_identifier}', '', '', null, true)])
);
}

Expand Down Expand Up @@ -278,3 +313,30 @@ public function createToken(Passport $passport, string $firewallName): TokenInte
}
}
}

class BaseUser implements UserInterface
{
public function __construct(private string $identifier, private array $roles)
{
}

public function getRoles(): array
{
return $this->roles;
}

public function eraseCredentials(): void
{
}

public function getUserIdentifier(): string
{
return $this->identifier;
}
}

#[WithLdapPassword]
class UserWithLdapPasswordEnabled extends BaseUser {}

#[WithLdapPassword(false)]
class UserWithLdapPasswordDisabled extends BaseUser {}
42 changes: 42 additions & 0 deletions 42 src/Symfony/Component/Ldap/Tests/Security/LdapBadgeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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 Security;

use PHPUnit\Framework\TestCase;
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Ldap\Security\LdapBadge;

final class LdapBadgeTest extends TestCase
{
use ExpectDeprecationTrait;

/**
* @group legacy
*/
public function testDeprecationOnResolvedInitialValue()
{
$this->expectDeprecation('Since symfony/ldap 6.4: Passing "false" as resolved initial value is deprecated, use "true" instead.');

new LdapBadge('foo');
}

/**
* @group legacy
*/
public function testDeprecationOnMarkAsResolved()
{
$this->expectDeprecation('Since symfony/ldap 6.4: Symfony\Component\Ldap\Security\LdapBadge::markResolved is deprecated and will be removed in 7.0. Symfony\Component\Ldap\Security\LdapBadge is intended to bear LDAP information and doesn\'t need to be resolved anymore.');

$sut = new LdapBadge('foo', '{user_identifier}', '', '', null, true);
$sut->markResolved();
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.