Skip to content

Navigation Menu

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

Commit 80eb8a3

Browse filesBrowse files
committed
Add a normalization step for the user-identifier in firewalls
1 parent cad21a9 commit 80eb8a3
Copy full SHA for 80eb8a3

File tree

14 files changed

+297
-2
lines changed
Filter options

14 files changed

+297
-2
lines changed

‎src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ CHANGELOG
66

77
* Add `Security::isGrantedForUser()` to test user authorization without relying on the session. For example, users not currently logged in, or while processing a message from a message queue
88
* Add encryption support to `OidcTokenHandler` (JWE)
9+
* Add ability to fetch Ldap roles
910

1011
7.2
1112
---

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php
+2
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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Resources/config/security.php
+1
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@
268268
abstract_arg('filter'),
269269
abstract_arg('password_attribute'),
270270
abstract_arg('extra_fields (email etc)'),
271+
abstract_arg('role fetcher'),
271272
])
272273

273274
->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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginLdapTest.php
+48
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,45 @@ public function testKernelBoot()
2229

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

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

Copy file name to clipboardExpand all lines: src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/config.yml
+2-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ imports:
33
services:
44
Symfony\Component\Ldap\Ldap:
55
arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter']
6+
tags: [ 'ldap' ]
67

78
Symfony\Component\Ldap\Adapter\ExtLdap\Adapter:
89
arguments:
@@ -21,7 +22,7 @@ security:
2122
search_password: ''
2223
default_roles: ROLE_USER
2324
uid_key: uid
24-
extra_fields: ['email']
25+
#extra_fields: ['email']
2526

2627
firewalls:
2728
main:
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 }

‎src/Symfony/Component/Ldap/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Ldap/CHANGELOG.md
+5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Add ability to fetch roles
8+
49
7.2
510
---
611

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

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

Copy file name to clipboardExpand all lines: src/Symfony/Component/Ldap/Security/LdapUserProvider.php
+6-1
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
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+
}
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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.