Skip to content

Commit bd83a8c

Browse files
committed
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.
1 parent 678abb4 commit bd83a8c

File tree

12 files changed

+296
-7
lines changed

12 files changed

+296
-7
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public function create(ContainerBuilder $container, string $id, array $config):
3737
->replaceArgument(6, $config['filter'])
3838
->replaceArgument(7, $config['password_attribute'])
3939
->replaceArgument(8, $config['extra_fields'])
40+
->replaceArgument(9, $config['role_fetcher'] ? new Reference($config['role_fetcher']) : null)
4041
;
4142
}
4243

@@ -63,6 +64,7 @@ public function addConfiguration(NodeDefinition $node): void
6364
->requiresAtLeastOneElement()
6465
->prototype('scalar')->end()
6566
->end()
67+
->scalarNode('role_fetcher')->defaultNull()->end()
6668
->scalarNode('uid_key')->defaultValue('sAMAccountName')->end()
6769
->scalarNode('filter')->defaultValue('({uid_key}={user_identifier})')->end()
6870
->scalarNode('password_attribute')->defaultNull()->end()

src/Symfony/Bundle/SecurityBundle/Resources/config/security.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@
256256
abstract_arg('filter'),
257257
abstract_arg('password_attribute'),
258258
abstract_arg('extra_fields (email etc)'),
259+
abstract_arg('role fetcher'),
259260
])
260261

261262
->set('security.user.provider.chain', ChainUserProvider::class)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLdapLoginBundle\Controller;
13+
14+
use Symfony\Component\HttpFoundation\JsonResponse;
15+
use Symfony\Component\Security\Core\User\UserInterface;
16+
17+
class TestController
18+
{
19+
public function loginCheckAction(UserInterface $user)
20+
{
21+
return new JsonResponse([
22+
'message' => sprintf('Welcome @%s!', $user->getUserIdentifier()),
23+
'roles' => $user->getRoles(),
24+
]);
25+
}
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLdapLoginBundle\Security\Ldap;
13+
14+
use Symfony\Component\Ldap\Entry;
15+
use Symfony\Component\Ldap\Security\RoleFetcherInterface;
16+
17+
class DummyRoleFetcher implements RoleFetcherInterface
18+
{
19+
public function fetchRoles(Entry $entry): array
20+
{
21+
if ($entry->getAttribute('uid') === ['spomky']) {
22+
return ['ROLE_SUPER_ADMIN', 'ROLE_USER'];
23+
}
24+
25+
return ['ROLE_LDAP_USER_42', 'ROLE_USER'];
26+
}
27+
}

src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginLdapTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111

1212
namespace Symfony\Bundle\SecurityBundle\Tests\Functional;
1313

14+
use Symfony\Component\HttpFoundation\JsonResponse;
1415
use Symfony\Component\HttpKernel\Kernel;
16+
use Symfony\Component\Ldap\Adapter\AdapterInterface;
17+
use Symfony\Component\Ldap\Adapter\CollectionInterface;
18+
use Symfony\Component\Ldap\Adapter\ConnectionInterface;
19+
use Symfony\Component\Ldap\Adapter\ExtLdap\Adapter;
20+
use Symfony\Component\Ldap\Adapter\QueryInterface;
21+
use Symfony\Component\Ldap\Entry;
1522

1623
class JsonLoginLdapTest extends AbstractWebTestCase
1724
{
@@ -22,4 +29,42 @@ public function testKernelBoot()
2229

2330
$this->assertInstanceOf(Kernel::class, $kernel);
2431
}
32+
33+
public function testDefaultJsonLdapLoginSuccess()
34+
{
35+
// Given
36+
$client = $this->createClient(['test_case' => 'JsonLoginLdap', 'root_config' => 'config.yml', 'debug' => true]);
37+
$container = $client->getContainer();
38+
$connectionMock = $this->createMock(ConnectionInterface::class);
39+
$collection = new class([new Entry('', ['uid' => ['spomky']])]) extends \ArrayObject implements CollectionInterface {
40+
public function toArray(): array
41+
{
42+
return $this->getArrayCopy();
43+
}
44+
};
45+
$queryMock = $this->createMock(QueryInterface::class);
46+
$queryMock
47+
->method('execute')
48+
->willReturn($collection)
49+
;
50+
$ldapAdapterMock = $this->createMock(AdapterInterface::class);
51+
$ldapAdapterMock
52+
->method('getConnection')
53+
->willReturn($connectionMock)
54+
;
55+
$ldapAdapterMock
56+
->method('createQuery')
57+
->willReturn($queryMock)
58+
;
59+
$container->set(Adapter::class, $ldapAdapterMock);
60+
61+
// When
62+
$client->request('POST', '/login', [], [], ['CONTENT_TYPE' => 'application/json'], '{"user": {"login": "spomky", "password": "foo"}}');
63+
$response = $client->getResponse();
64+
65+
// Then
66+
$this->assertInstanceOf(JsonResponse::class, $response);
67+
$this->assertSame(200, $response->getStatusCode());
68+
$this->assertSame(['message' => 'Welcome @spomky!', 'roles' => ['ROLE_SUPER_ADMIN', 'ROLE_USER']], json_decode($response->getContent(), true));
69+
}
2570
}

src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/config.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ imports:
33
services:
44
Symfony\Component\Ldap\Ldap:
55
arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
6+
tags: [ 'ldap' ]
7+
8+
test_role_fetcher:
9+
class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLdapLoginBundle\Security\Ldap\DummyRoleFetcher
610

711
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
812
arguments:
@@ -21,7 +25,8 @@ security:
2125
search_password: ''
2226
default_roles: ROLE_USER
2327
uid_key: uid
24-
extra_fields: ['email']
28+
#extra_fields: ['email']
29+
role_fetcher: 'test_role_fetcher'
2530

2631
firewalls:
2732
main:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
login_check:
2+
path: /login
3+
defaults: { _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLdapLoginBundle\Controller\TestController::loginCheckAction }
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Ldap\Security;
13+
14+
use Symfony\Component\Ldap\Entry;
15+
16+
final readonly class AssignDefaultRoles implements RoleFetcherInterface
17+
{
18+
/**
19+
* @param string[] $roles
20+
*/
21+
public function __construct(
22+
private array $roles
23+
) { }
24+
25+
/**
26+
* @return string[]
27+
*/
28+
public function fetchRoles(Entry $entry): array
29+
{
30+
return $this->roles;
31+
}
32+
}

src/Symfony/Component/Ldap/Security/LdapUserProvider.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class LdapUserProvider implements UserProviderInterface, PasswordUpgraderInterfa
3737
{
3838
private string $uidKey;
3939
private string $defaultSearch;
40+
private RoleFetcherInterface $roleFetcher;
4041

4142
public function __construct(
4243
private LdapInterface $ldap,
@@ -48,12 +49,14 @@ public function __construct(
4849
?string $filter = null,
4950
private ?string $passwordAttribute = null,
5051
private array $extraFields = [],
52+
?RoleFetcherInterface $roleFetcher = null
5153
) {
5254
$uidKey ??= 'sAMAccountName';
5355
$filter ??= '({uid_key}={user_identifier})';
5456

5557
$this->uidKey = $uidKey;
5658
$this->defaultSearch = str_replace('{uid_key}', $uidKey, $filter);
59+
$this->roleFetcher = $roleFetcher ?? new AssignDefaultRoles($defaultRoles);
5760
}
5861

5962
public function loadUserByIdentifier(string $identifier): UserInterface
@@ -147,7 +150,9 @@ protected function loadUser(string $identifier, Entry $entry): UserInterface
147150
$extraFields[$field] = $this->getAttributeValue($entry, $field);
148151
}
149152

150-
return new LdapUser($entry, $identifier, $password, $this->defaultRoles, $extraFields);
153+
$roles = $this->roleFetcher->fetchRoles($entry);
154+
155+
return new LdapUser($entry, $identifier, $password, $roles, $extraFields);
151156
}
152157

153158
private function getAttributeValue(Entry $entry, string $attribute): mixed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Ldap\Security;
13+
14+
use Symfony\Component\Ldap\Entry;
15+
16+
final readonly class MemberOfRoles implements RoleFetcherInterface
17+
{
18+
/**
19+
* @param array<string, string> $mapping
20+
*/
21+
public function __construct(
22+
private array $mapping,
23+
private string $attributeName = 'ismemberof',
24+
private string $groupNameRegex = '/^CN=(?P<group>[^,]+),ou.*$/i',
25+
) {
26+
}
27+
28+
/**
29+
* @return string[]
30+
*/
31+
public function fetchRoles(Entry $entry): array
32+
{
33+
if (!$entry->hasAttribute($this->attributeName)) {
34+
return [];
35+
}
36+
37+
$roles = [];
38+
foreach ($entry->getAttribute($this->attributeName) as $group) {
39+
$groupName = $this->getGroupName($group);
40+
if (\array_key_exists($groupName, $this->mapping)) {
41+
$roles[] = $this->mapping[$groupName];
42+
}
43+
}
44+
45+
return array_unique($roles);
46+
}
47+
48+
private function getGroupName(string $group): string
49+
{
50+
if (preg_match($this->groupNameRegex, $group, $matches)) {
51+
return $matches['group'];
52+
}
53+
54+
return $group;
55+
}
56+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Ldap\Security;
13+
14+
use Symfony\Component\Ldap\Entry;
15+
16+
/**
17+
* Fetches LDAP roles for a given entry.
18+
*/
19+
interface RoleFetcherInterface
20+
{
21+
/**
22+
* @return string[] The list of roles
23+
*/
24+
public function fetchRoles(Entry $entry): array;
25+
}

0 commit comments

Comments
 (0)