Skip to content

Commit 265a5dd

Browse files
committed
[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.
1 parent 52dad31 commit 265a5dd

File tree

6 files changed

+184
-2
lines changed

6 files changed

+184
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ CHANGELOG
55
---
66

77
* Deprecate `LdapUser::eraseCredentials()` in favor of `__serialize()`
8+
* Add `RoleFetcherInterface` to allow roles fetching at user loading
9+
* Add ability to fetch LDAP roles
810

911
7.2
1012
---

Security/AssignDefaultRoles.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
/**
27+
* @return string[]
28+
*/
29+
public function fetchRoles(Entry $entry): array
30+
{
31+
return $this->roles;
32+
}
33+
}

Security/LdapUserProvider.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@ 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,
4344
private string $baseDn,
4445
private ?string $searchDn = null,
4546
#[\SensitiveParameter] private ?string $searchPassword = null,
46-
private array $defaultRoles = [],
47+
array|RoleFetcherInterface $defaultRoles = [],
4748
?string $uidKey = null,
4849
?string $filter = null,
4950
private ?string $passwordAttribute = null,
@@ -54,6 +55,7 @@ public function __construct(
5455

5556
$this->uidKey = $uidKey;
5657
$this->defaultSearch = str_replace('{uid_key}', $uidKey, $filter);
58+
$this->roleFetcher = \is_array($defaultRoles) ? new AssignDefaultRoles($defaultRoles) : $defaultRoles;
5759
}
5860

5961
public function loadUserByIdentifier(string $identifier): UserInterface
@@ -147,7 +149,9 @@ protected function loadUser(string $identifier, Entry $entry): UserInterface
147149
$extraFields[$field] = $this->getAttributeValue($entry, $field);
148150
}
149151

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

153157
private function getAttributeValue(Entry $entry, string $attribute): mixed

Security/MemberOfRoles.php

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+
}

Security/RoleFetcherInterface.php

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+
}

Tests/Security/LdapUserProviderTest.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
use Symfony\Component\Ldap\LdapInterface;
2020
use Symfony\Component\Ldap\Security\LdapUser;
2121
use Symfony\Component\Ldap\Security\LdapUserProvider;
22+
use Symfony\Component\Ldap\Security\MemberOfRoles;
23+
use Symfony\Component\Ldap\Security\RoleFetcherInterface;
2224
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
2325
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
2426

@@ -388,4 +390,64 @@ public function testRefreshUserShouldReturnUserWithSameProperties()
388390

389391
$this->assertEquals($user, $provider->refreshUser($user));
390392
}
393+
394+
public function testLoadUserWithCorrectRoles()
395+
{
396+
// Given
397+
$result = $this->createMock(CollectionInterface::class);
398+
$query = $this->createMock(QueryInterface::class);
399+
$query
400+
->method('execute')
401+
->willReturn($result)
402+
;
403+
$ldap = $this->createMock(LdapInterface::class);
404+
$result
405+
->method('offsetGet')
406+
->with(0)
407+
->willReturn(new Entry('foo', ['sAMAccountName' => ['foo']]))
408+
;
409+
$result
410+
->method('count')
411+
->willReturn(1)
412+
;
413+
$ldap
414+
->method('escape')
415+
->willReturn('foo')
416+
;
417+
$ldap
418+
->method('query')
419+
->willReturn($query)
420+
;
421+
$roleFetcher = $this->createMock(RoleFetcherInterface::class);
422+
$roleFetcher
423+
->method('fetchRoles')
424+
->willReturn(['ROLE_FOO', 'ROLE_BAR'])
425+
;
426+
427+
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com', defaultRoles: $roleFetcher);
428+
429+
// When
430+
$user = $provider->loadUserByIdentifier('foo');
431+
432+
// Then
433+
$this->assertInstanceOf(LdapUser::class, $user);
434+
$this->assertSame(['ROLE_FOO', 'ROLE_BAR'], $user->getRoles());
435+
}
436+
437+
public function testMemberOfRoleFetch()
438+
{
439+
// Given
440+
$roleFetcher = new MemberOfRoles(
441+
['Staff' => 'ROLE_STAFF', 'Admin' => 'ROLE_ADMIN'],
442+
'memberOf'
443+
);
444+
445+
$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']]);
446+
447+
// When
448+
$roles = $roleFetcher->fetchRoles($entry);
449+
450+
// Then
451+
$this->assertSame(['ROLE_STAFF', 'ROLE_ADMIN'], $roles);
452+
}
391453
}

0 commit comments

Comments
 (0)