feature #14602 [2.8] [Ldap] Added support for LDAP (New Component + integration in the Security Component). (csarrazi, lyrixx)

This PR was merged into the 2.8 branch.

Discussion
----------

[2.8] [Ldap] Added support for LDAP (New Component + integration in the Security Component).

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | not yet
| Fixed tickets | -
| License       | MIT
| Doc PR        | not yet

Current state:

- [x] Implement logic
- [x] Post-review tuning and stabilization
- [x] Fix tests

This PR is a follow-up to #5189, which was in a stand-still for a few years now. It tries to fix the remaining issues which were mentioned in the discussion.

There are still a few issues with the PR, as it is. For example, it introduces two new firewall factories, whereas the base factories (`form_login` and `http_basic`) could simply introduce new configuration options.

Also, for a user to use an LDAP server as an authentication provider, he first needs to define a service which should be an instance of `Symfony\Component\Security\Ldap\Ldap`.

For example:

```yml
services:
    my_ldap:
        class: Symfony\Component\Security\Ldap\Ldap
        arguments: [ "ldap.mydomain.tld" ]
```

Then, in `security.yml`, this service can be used in both the user provider and the firewalls:

```yml
security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext

    role_hierarchy:
        ROLE_ADMIN:       ROLE_USER
        ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        ldap_users:
            ldap:
                service: my_ldap
                base_dn: dc=MyDomain,dc=tld
                search_dn: CN=My User,OU=Users,DC=MyDomain,DC=tld
                search_password: p455w0rd
                filter: (sAMAccountName={username})
                default_roles: ROLE_USER

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false
        demo_login:
            pattern:  ^/login$
            security: false
        api:
            provider: ldap_users
            stateless: true
            pattern:    ^/api
            http_basic_ldap:
                service: my_ldap
                dn_string: "{username}@MYDOMAIN"
        demo_secured_area:
            provider: ldap_users
            pattern:    ^/
            logout:
                path:   logout
                target: login
            form_login_ldap:
                service: my_ldap
                dn_string: CN={username},OU=Users,DC=MyDomain,DC=tld
                check_path: login_check
                login_path: login
```

Commits
-------

60b9f2e Implemented LDAP authentication and LDAP user provider
1c964b9 Introducing the LDAP component
This commit is contained in:
Fabien Potencier 2015-09-28 13:32:19 +02:00
commit 7d7e07fefa
24 changed files with 1037 additions and 3 deletions

View File

@ -35,6 +35,7 @@ before_install:
- if [[ "$TRAVIS_PHP_VERSION" = 5.* ]]; then (pecl install -f memcached-2.1.0 && echo "extension = memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini) || echo "Let's continue without memcache extension"; fi;
- if [[ "$TRAVIS_PHP_VERSION" = 5.* ]] && [ "$deps" = "no" ]; then (cd src/Symfony/Component/Debug/Resources/ext && phpize && ./configure && make && echo "extension = $(pwd)/modules/symfony_debug.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini); fi;
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then php -i; fi;
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then echo "extension = ldap.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi;
- ./phpunit install
- export PHPUNIT="$(readlink -f ./phpunit)"

View File

@ -45,6 +45,7 @@ install:
- IF %PHP_EXT%==1 echo extension=php_mbstring.dll >> php.ini
- IF %PHP_EXT%==1 echo extension=php_fileinfo.dll >> php.ini
- IF %PHP_EXT%==1 echo extension=php_pdo_sqlite.dll >> php.ini
- IF %PHP_EXT%==1 echo extension=php_ldap.dll >> php.ini
- cd c:\projects\symfony
- php phpunit install
- IF %APPVEYOR_REPO_BRANCH%==master (SET COMPOSER_ROOT_VERSION=dev-master) ELSE (SET COMPOSER_ROOT_VERSION=%APPVEYOR_REPO_BRANCH%.x-dev)

View File

@ -0,0 +1,57 @@
<?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\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* FormLoginLdapFactory creates services for form login ldap authentication.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*/
class FormLoginLdapFactory extends FormLoginFactory
{
protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
{
$provider = 'security.authentication.provider.ldap_bind.'.$id;
$container
->setDefinition($provider, new DefinitionDecorator('security.authentication.provider.ldap_bind'))
->replaceArgument(0, new Reference($userProviderId))
->replaceArgument(2, $id)
->replaceArgument(3, new Reference($config['service']))
->replaceArgument(4, $config['dn_string'])
;
return $provider;
}
public function addConfiguration(NodeDefinition $node)
{
parent::addConfiguration($node);
$node
->children()
->scalarNode('service')->end()
->scalarNode('dn_string')->defaultValue('{username}')->end()
->end()
;
}
public function getKey()
{
return 'form-login-ldap';
}
}

View File

@ -0,0 +1,67 @@
<?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\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* HttpBasicFactory creates services for HTTP basic authentication.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*/
class HttpBasicLdapFactory extends HttpBasicFactory
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$provider = 'security.authentication.provider.ldap_bind.'.$id;
$container
->setDefinition($provider, new DefinitionDecorator('security.authentication.provider.ldap_bind'))
->replaceArgument(0, new Reference($userProvider))
->replaceArgument(2, $id)
->replaceArgument(3, new Reference($config['service']))
->replaceArgument(4, $config['dn_string'])
;
// entry point
$entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPoint);
// listener
$listenerId = 'security.authentication.listener.basic.'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.authentication.listener.basic'));
$listener->replaceArgument(2, $id);
$listener->replaceArgument(3, new Reference($entryPointId));
return array($provider, $listenerId, $entryPointId);
}
public function addConfiguration(NodeDefinition $node)
{
parent::addConfiguration($node);
$node
->children()
->scalarNode('service')->end()
->scalarNode('dn_string')->defaultValue('{username}')->end()
->end()
;
}
public function getKey()
{
return 'http-basic-ldap';
}
}

View File

@ -0,0 +1,64 @@
<?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\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* LdapFactory creates services for Ldap user provider.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*/
class LdapFactory implements UserProviderFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config)
{
$container
->setDefinition($id, new DefinitionDecorator('security.user.provider.ldap'))
->replaceArgument(0, new Reference($config['service']))
->replaceArgument(1, $config['base_dn'])
->replaceArgument(2, $config['search_dn'])
->replaceArgument(3, $config['search_password'])
->replaceArgument(4, $config['default_roles'])
->replaceArgument(5, $config['uid_key'])
->replaceArgument(6, $config['filter'])
;
}
public function getKey()
{
return 'ldap';
}
public function addConfiguration(NodeDefinition $node)
{
$node
->children()
->scalarNode('service')->isRequired()->cannotBeEmpty()->end()
->scalarNode('base_dn')->isRequired()->cannotBeEmpty()->end()
->scalarNode('search_dn')->end()
->scalarNode('search_password')->end()
->arrayNode('default_roles')
->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
->requiresAtLeastOneElement()
->prototype('scalar')->end()
->end()
->scalarNode('uid_key')->defaultValue('sAMAccountName')->end()
->scalarNode('filter')->defaultValue('({uid_key}={username})')->end()
->end()
;
}
}

View File

@ -158,10 +158,21 @@
<argument type="service" id="security.token_storage" on-invalid="null" />
</service>
<!-- Provisioning -->
<service id="security.user.provider.in_memory" class="%security.user.provider.in_memory.class%" abstract="true" public="false" />
<service id="security.user.provider.in_memory.user" class="%security.user.provider.in_memory.user.class%" abstract="true" public="false" />
<service id="security.user.provider.ldap" class="Symfony\Component\Security\Core\User\LdapUserProvider" abstract="true" public="false">
<argument /> <!-- security.ldap.ldap -->
<argument /> <!-- base dn -->
<argument /> <!-- search dn -->
<argument /> <!-- search password -->
<argument /> <!-- default_roles -->
<argument /> <!-- uid key -->
<argument /> <!-- filter -->
</service>
<service id="security.user.provider.chain" class="%security.user.provider.chain.class%" abstract="true" public="false" />
<service id="security.http_utils" class="%security.http_utils.class%" public="false">
@ -169,6 +180,7 @@
<argument type="service" id="router" on-invalid="null" />
</service>
<!-- Validator -->
<service id="security.validator.user_password" class="%security.validator.user_password.class%">
<tag name="validator.constraint_validator" alias="security.validator.user_password" />

View File

@ -223,6 +223,15 @@
<argument>%security.authentication.hide_user_not_found%</argument>
</service>
<service id="security.authentication.provider.ldap_bind" class="Symfony\Component\Security\Core\Authentication\Provider\LdapBindAuthenticationProvider" public="false" abstract="true">
<argument /> <!-- User Provider -->
<argument type="service" id="security.user_checker" />
<argument /> <!-- Provider-shared Key -->
<argument /> <!-- LDAP -->
<argument /> <!-- Base DN -->
<argument>%security.authentication.hide_user_not_found%</argument>
</service>
<service id="security.authentication.provider.simple" class="%security.authentication.provider.simple.class%" abstract="true" public="false">
<argument /> <!-- Simple Authenticator -->
<argument /> <!-- User Provider -->

View File

@ -15,7 +15,9 @@ use Symfony\Component\HttpKernel\Bundle\Bundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginLdapFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicLdapFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpDigestFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\X509Factory;
@ -24,6 +26,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimplePre
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimpleFormFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\InMemoryFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\LdapFactory;
/**
* Bundle.
@ -38,7 +41,9 @@ class SecurityBundle extends Bundle
$extension = $container->getExtension('security');
$extension->addSecurityListenerFactory(new FormLoginFactory());
$extension->addSecurityListenerFactory(new FormLoginLdapFactory());
$extension->addSecurityListenerFactory(new HttpBasicFactory());
$extension->addSecurityListenerFactory(new HttpBasicLdapFactory());
$extension->addSecurityListenerFactory(new HttpDigestFactory());
$extension->addSecurityListenerFactory(new RememberMeFactory());
$extension->addSecurityListenerFactory(new X509Factory());
@ -48,6 +53,7 @@ class SecurityBundle extends Bundle
$extension->addSecurityListenerFactory(new GuardAuthenticationFactory());
$extension->addUserProviderFactory(new InMemoryFactory());
$extension->addUserProviderFactory(new LdapFactory());
$container->addCompilerPass(new AddSecurityVotersPass());
}
}

3
src/Symfony/Component/Ldap/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
vendor/
composer.lock
phpunit.xml

View File

@ -0,0 +1,21 @@
<?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\Exception;
/**
* ConnectionException is throw if binding to ldap can not be established.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class ConnectionException extends \RuntimeException
{
}

View File

@ -0,0 +1,21 @@
<?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\Exception;
/**
* LdapException is throw if php ldap module is not loaded.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
*/
class LdapException extends \RuntimeException
{
}

View File

@ -0,0 +1,19 @@
Copyright (c) 2004-2015 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@ -0,0 +1,220 @@
<?php
namespace Symfony\Component\Ldap;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Ldap\Exception\LdapException;
/**
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Francis Besset <francis.besset@gmail.com>
* @author Charles Sarrazin <charles@sarraz.in>
*/
class LdapClient implements LdapClientInterface
{
private $host;
private $port;
private $version;
private $useSsl;
private $useStartTls;
private $optReferrals;
private $connection;
private $charmaps;
/**
* Constructor.
*
* @param string $host
* @param int $port
* @param int $version
* @param bool $useSsl
* @param bool $useStartTls
* @param bool $optReferrals
*/
public function __construct($host = null, $port = 389, $version = 3, $useSsl = false, $useStartTls = false, $optReferrals = false)
{
if (!extension_loaded('ldap')) {
throw new LdapException('The ldap module is needed.');
}
$this->host = $host;
$this->port = $port;
$this->version = $version;
$this->useSsl = (bool) $useSsl;
$this->useStartTls = (bool) $useStartTls;
$this->optReferrals = (bool) $optReferrals;
}
public function __destruct()
{
$this->disconnect();
}
/**
* {@inheritdoc}
*/
public function bind($dn = null, $password = null)
{
if (!$this->connection) {
$this->connect();
}
if (false === @ldap_bind($this->connection, $dn, $password)) {
throw new ConnectionException(ldap_error($this->connection));
}
}
/**
* {@inheritdoc}
*/
public function find($dn, $query, $filter = '*')
{
if (!is_array($filter)) {
$filter = array($filter);
}
$search = ldap_search($this->connection, $dn, $query, $filter);
$infos = ldap_get_entries($this->connection, $search);
if (0 === $infos['count']) {
return;
}
return $infos;
}
/**
* {@inheritdoc}
*/
public function escape($subject, $ignore = '', $flags = 0)
{
if (function_exists('ldap_escape')) {
return ldap_escape($subject, $ignore, $flags);
}
return $this->doEscape($subject, $ignore, $flags);
}
private function connect()
{
if (!$this->connection) {
$host = $this->host;
if ($this->useSsl) {
$host = 'ldaps://'.$host;
}
ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $this->version);
ldap_set_option($this->connection, LDAP_OPT_REFERRALS, $this->optReferrals);
$this->connection = ldap_connect($host, $this->port);
if ($this->useStartTls) {
ldap_start_tls($this->connection);
}
}
}
private function disconnect()
{
if ($this->connection && is_resource($this->connection)) {
ldap_unbind($this->connection);
}
$this->connection = null;
}
/**
* Stub implementation of the {@link ldap_escape()} function of the ldap
* extension.
*
* Escape strings for safe use in LDAP filters and DNs.
*
* @author Chris Wright <ldapi@daverandom.com>
*
* @param string $subject
* @param string $ignore
* @param int $flags
*
* @return string
*
* @see http://stackoverflow.com/a/8561604
*/
private function doEscape($subject, $ignore = '', $flags = 0)
{
$charMaps = $this->getCharmaps();
// Create the base char map to escape
$flags = (int) $flags;
$charMap = array();
if ($flags & self::LDAP_ESCAPE_FILTER) {
$charMap += $charMaps[self::LDAP_ESCAPE_FILTER];
}
if ($flags & self::LDAP_ESCAPE_DN) {
$charMap += $charMaps[self::LDAP_ESCAPE_DN];
}
if (!$charMap) {
$charMap = $charMaps[0];
}
// Remove any chars to ignore from the list
$ignore = (string) $ignore;
for ($i = 0, $l = strlen($ignore); $i < $l; ++$i) {
unset($charMap[$ignore[$i]]);
}
// Do the main replacement
$result = strtr($subject, $charMap);
// Encode leading/trailing spaces if LDAP_ESCAPE_DN is passed
if ($flags & self::LDAP_ESCAPE_DN) {
if ($result[0] === ' ') {
$result = '\\20'.substr($result, 1);
}
if ($result[strlen($result) - 1] === ' ') {
$result = substr($result, 0, -1).'\\20';
}
}
return $result;
}
private function getCharmaps()
{
if (null !== $this->charmaps) {
return $this->charmaps;
}
$charMaps = array(
self::LDAP_ESCAPE_FILTER => array('\\', '*', '(', ')', "\x00"),
self::LDAP_ESCAPE_DN => array('\\', ',', '=', '+', '<', '>', ';', '"', '#'),
);
$charMaps[0] = array();
for ($i = 0; $i < 256; ++$i) {
$charMaps[0][chr($i)] = sprintf('\\%02x', $i);
}
for ($i = 0, $l = count($charMaps[self::LDAP_ESCAPE_FILTER]); $i < $l; ++$i) {
$chr = $charMaps[self::LDAP_ESCAPE_FILTER][$i];
unset($charMaps[self::LDAP_ESCAPE_FILTER][$i]);
$charMaps[self::LDAP_ESCAPE_FILTER][$chr] = $charMaps[0][$chr];
}
for ($i = 0, $l = count($charMaps[self::LDAP_ESCAPE_DN]); $i < $l; ++$i) {
$chr = $charMaps[self::LDAP_ESCAPE_DN][$i];
unset($charMaps[self::LDAP_ESCAPE_DN][$i]);
$charMaps[self::LDAP_ESCAPE_DN][$chr] = $charMaps[0][$chr];
}
$this->charmaps = $charMaps;
return $this->charmaps;
}
}

View File

@ -0,0 +1,49 @@
<?php
namespace Symfony\Component\Ldap;
use Symfony\Component\Ldap\Exception\ConnectionException;
/**
* Ldap interface.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*/
interface LdapClientInterface
{
const LDAP_ESCAPE_FILTER = 0x01;
const LDAP_ESCAPE_DN = 0x02;
/**
* Return a connection bound to the ldap.
*
* @param string $dn A LDAP dn
* @param string $password A password
*
* @throws ConnectionException If dn / password could not be bound.
*/
public function bind($dn = null, $password = null);
/*
* Find a username into ldap connection.
*
* @param string $dn
* @param string $query
* @param mixed $filter
*
* @return array|null
*/
public function find($dn, $query, $filter = '*');
/**
* Escape a string for use in an LDAP filter or DN.
*
* @param string $subject
* @param string $ignore
* @param int $flags
*
* @return string
*/
public function escape($subject, $ignore = '', $flags = 0);
}

View File

@ -0,0 +1,23 @@
Ldap Component
=============
A Ldap client for PHP on top of PHP's ldap extension.
This component also provides a stub for the missing
`ldap_escape` function in PHP versions lower than 5.6.
Documentation
-------------
The documentation for the component can be found [online] [0].
Resources
---------
You can run the unit tests with the following command:
$ cd path/to/Symfony/Component/Ldap/
$ composer install
$ phpunit
[0]: https://symfony.com/doc/2.8/components/ldap.html

View File

@ -0,0 +1,56 @@
<?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\Security\Core\Tests\Authentication\Provider;
use Symfony\Component\Ldap\LdapClient;
class LdapClientTest extends \PHPUnit_Framework_TestCase
{
protected function setUp()
{
if (!extension_loaded('ldap')) {
$this->markTestSkipped('The ldap extension is not available');
}
}
/**
* @dataProvider provideLdapEscapeValues
*/
public function testLdapEscape($subject, $ignore, $flags, $expected)
{
$ldap = new LdapClient();
$this->assertSame($expected, $ldap->escape($subject, $ignore, $flags));
}
/**
* Provides values for the ldap_escape shim. These tests come from the official
* extension.
*
* @see https://github.com/php/php-src/blob/master/ext/ldap/tests/ldap_escape_dn.phpt
* @see https://github.com/php/php-src/blob/master/ext/ldap/tests/ldap_escape_all.phpt
* @see https://github.com/php/php-src/blob/master/ext/ldap/tests/ldap_escape_both.phpt
* @see https://github.com/php/php-src/blob/master/ext/ldap/tests/ldap_escape_filter.phpt
* @see https://github.com/php/php-src/blob/master/ext/ldap/tests/ldap_escape_ignore.phpt
*
* @return array
*/
public function provideLdapEscapeValues()
{
return array(
array('foo=bar(baz)*', null, LdapClient::LDAP_ESCAPE_DN, 'foo\3dbar(baz)*'),
array('foo=bar(baz)*', null, null, '\66\6f\6f\3d\62\61\72\28\62\61\7a\29\2a'),
array('foo=bar(baz)*', null, LdapClient::LDAP_ESCAPE_DN | LdapClient::LDAP_ESCAPE_FILTER, 'foo\3dbar\28baz\29\2a'),
array('foo=bar(baz)*', null, LdapClient::LDAP_ESCAPE_FILTER, 'foo=bar\28baz\29\2a'),
array('foo=bar(baz)*', 'ao', null, '\66oo\3d\62a\72\28\62a\7a\29\2a'),
);
}
}

View File

@ -0,0 +1,34 @@
{
"name": "symfony/ldap",
"type": "library",
"description": "An abstraction in front of PHP's LDAP functions, compatible with PHP 5.3.9 onwards.",
"keywords": ["ldap", "active directory"],
"homepage": "https://symfony.com",
"license": "MIT",
"authors": [
{
"name": "Charles Sarrazin",
"email": "charles@sarraz.in"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=5.3.9",
"ext-ldap": "*"
},
"require-dev": {
"symfony/phpunit-bridge": "~2.7|~3.0.0"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Ldap\\": "" }
},
"minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-master": "2.8-dev"
}
}
}

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony Ldap Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory>./</directory>
<exclude>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>

View File

@ -0,0 +1,76 @@
<?php
namespace Symfony\Component\Security\Core\Authentication\Provider;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Ldap\LdapClientInterface;
use Symfony\Component\Ldap\Exception\ConnectionException;
/**
* LdapBindAuthenticationProvider authenticates a user against an LDAP server.
*
* The only way to check user credentials is to try to connect the user with its
* credentials to the ldap.
*
* @author Charles Sarrazin <charles@sarraz.in>
*/
class LdapBindAuthenticationProvider extends UserAuthenticationProvider
{
private $userProvider;
private $ldap;
private $dnString;
/**
* Constructor.
*
* @param UserProviderInterface $userProvider A UserProvider
* @param UserCheckerInterface $userChecker A UserChecker
* @param string $providerKey The provider key
* @param LdapClientInterface $ldap An Ldap client
* @param string $dnString A string used to create the bind DN
* @param bool $hideUserNotFoundExceptions Whether to hide user not found exception or not
*/
public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, $providerKey, LdapClientInterface $ldap, $dnString = '{username}', $hideUserNotFoundExceptions = true)
{
parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);
$this->userProvider = $userProvider;
$this->ldap = $ldap;
$this->dnString = $dnString;
}
/**
* {@inheritdoc}
*/
protected function retrieveUser($username, UsernamePasswordToken $token)
{
if ('NONE_PROVIDED' === $username) {
throw new UsernameNotFoundException('Username can not be null');
}
return $this->userProvider->loadUserByUsername($username);
}
/**
* {@inheritdoc}
*/
protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
{
$username = $token->getUsername();
$password = $token->getCredentials();
try {
$username = $this->ldap->escape($username, '', LdapClientInterface::LDAP_ESCAPE_DN);
$dn = str_replace('{username}', $username, $this->dnString);
$this->ldap->bind($dn, $password);
} catch (ConnectionException $e) {
throw new BadCredentialsException('The presented password is invalid.');
}
}
}

View File

@ -0,0 +1,61 @@
<?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\Security\Core\Tests\Authentication\Provider;
use Symfony\Component\Security\Core\Authentication\Provider\LdapBindAuthenticationProvider;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\User\User;
use Symfony\Component\Ldap\Exception\ConnectionException;
class LdapBindAuthenticationProviderTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException
* @expectedExceptionMessage The presented password is invalid.
*/
public function testBindFailureShouldThrowAnException()
{
$userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface');
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$ldap
->expects($this->once())
->method('bind')
->will($this->throwException(new ConnectionException()))
;
$userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface');
$provider = new LdapBindAuthenticationProvider($userProvider, $userChecker, 'key', $ldap);
$reflection = new \ReflectionMethod($provider, 'checkAuthentication');
$reflection->setAccessible(true);
$reflection->invoke($provider, new User('foo', null), new UsernamePasswordToken('foo', '', 'key'));
}
public function testRetrieveUser()
{
$userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface');
$userProvider
->expects($this->once())
->method('loadUserByUsername')
->with('foo')
;
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface');
$provider = new LdapBindAuthenticationProvider($userProvider, $userChecker, 'key', $ldap);
$reflection = new \ReflectionMethod($provider, 'retrieveUser');
$reflection->setAccessible(true);
$reflection->invoke($provider, 'foo', new UsernamePasswordToken('foo', 'bar', 'key'));
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Symfony\Component\Security\Core\Tests\User;
use Symfony\Component\Security\Core\User\LdapUserProvider;
use Symfony\Component\Ldap\Exception\ConnectionException;
class LdapUserProviderTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
public function testLoadUserByUsernameFailsIfCantConnectToLdap()
{
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$ldap
->expects($this->once())
->method('bind')
->will($this->throwException(new ConnectionException()))
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');
$provider->loadUserByUsername('foo');
}
/**
* @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
public function testLoadUserByUsernameFailsIfNoLdapEntries()
{
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$ldap
->expects($this->once())
->method('escape')
->will($this->returnValue('foo'))
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');
$provider->loadUserByUsername('foo');
}
/**
* @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
public function testLoadUserByUsernameFailsIfMoreThanOneLdapEntry()
{
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$ldap
->expects($this->once())
->method('escape')
->will($this->returnValue('foo'))
;
$ldap
->expects($this->once())
->method('find')
->will($this->returnValue(array(
array(),
array(),
'count' => 2,
)))
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');
$provider->loadUserByUsername('foo');
}
public function testSuccessfulLoadUserByUsername()
{
$ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface');
$ldap
->expects($this->once())
->method('escape')
->will($this->returnValue('foo'))
;
$ldap
->expects($this->once())
->method('find')
->will($this->returnValue(array(
array(
'sAMAccountName' => 'foo',
'userpassword' => 'bar',
),
'count' => 1,
)))
;
$provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com');
$this->assertInstanceOf(
'Symfony\Component\Security\Core\User\User',
$provider->loadUserByUsername('foo')
);
}
}

View File

@ -0,0 +1,109 @@
<?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\Security\Core\User;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Ldap\LdapClientInterface;
/**
* LdapUserProvider is a simple user provider on top of ldap.
*
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*/
class LdapUserProvider implements UserProviderInterface
{
private $ldap;
private $baseDn;
private $searchDn;
private $searchPassword;
private $defaultRoles;
private $defaultSearch;
/**
* @param LdapClientInterface $ldap
* @param string $baseDn
* @param string $searchDn
* @param string $searchPassword
* @param array $defaultRoles
* @param string $uidKey
* @param string $filter
*/
public function __construct(LdapClientInterface $ldap, $baseDn, $searchDn = null, $searchPassword = null, array $defaultRoles = array(), $uidKey = 'sAMAccountName', $filter = '({uid_key}={username})')
{
$this->ldap = $ldap;
$this->baseDn = $baseDn;
$this->searchDn = $searchDn;
$this->searchPassword = $searchPassword;
$this->defaultRoles = $defaultRoles;
$this->defaultSearch = str_replace('{uid_key}', $uidKey, $filter);
}
/**
* {@inheritdoc}
*/
public function loadUserByUsername($username)
{
try {
$this->ldap->bind($this->searchDn, $this->searchPassword);
$username = $this->ldap->escape($username, '', LdapClientInterface::LDAP_ESCAPE_FILTER);
$query = str_replace('{username}', $username, $this->defaultSearch);
$search = $this->ldap->find($this->baseDn, $query);
} catch (ConnectionException $e) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username), 0, $e);
}
if (!$search) {
throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username));
}
if ($search['count'] > 1) {
throw new UsernameNotFoundException('More than one user found');
}
$user = $search[0];
return $this->loadUser($username, $user);
}
public function loadUser($username, $user)
{
$password = isset($user['userpassword']) ? $user['userpassword'] : null;
$roles = $this->defaultRoles;
return new User($username, $password, $roles);
}
/**
* {@inheritdoc}
*/
public function refreshUser(UserInterface $user)
{
if (!$user instanceof User) {
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
}
return new User($user->getUsername(), null, $user->getRoles());
}
/**
* {@inheritdoc}
*/
public function supportsClass($class)
{
return $class === 'Symfony\Component\Security\Core\User\User';
}
}

View File

@ -26,13 +26,15 @@
"symfony/translation": "~2.0,>=2.0.5|~3.0.0",
"symfony/validator": "~2.5,>=2.5.5|~3.0.0",
"psr/log": "~1.0",
"ircmaxell/password-compat": "1.0.*"
"ircmaxell/password-compat": "1.0.*",
"symfony/ldap": "~2.8|~3.0.0"
},
"suggest": {
"symfony/event-dispatcher": "",
"symfony/http-foundation": "",
"symfony/validator": "For using the user password constraint",
"symfony/expression-language": "For using the expression voter",
"symfony/ldap": "For using LDAP integration",
"ircmaxell/password-compat": "For using the BCrypt password encoder in PHP <5.5"
},
"autoload": {

View File

@ -38,7 +38,8 @@
"doctrine/dbal": "~2.2",
"psr/log": "~1.0",
"ircmaxell/password-compat": "~1.0",
"symfony/expression-language": "~2.6|~3.0.0"
"symfony/expression-language": "~2.6|~3.0.0",
"symfony/ldap": "~2.8|~3.0.0"
},
"suggest": {
"symfony/class-loader": "For using the ACL generateSql script",
@ -48,7 +49,8 @@
"symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs",
"symfony/expression-language": "For using the expression voter",
"ircmaxell/password-compat": "For using the BCrypt password encoder in PHP <5.5",
"paragonie/random_compat": ""
"paragonie/random_compat": "",
"symfony/ldap": "For using the LDAP user and authentication providers"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Security\\": "" }