From 1c964b993fdb78391797a2558c065acb49747880 Mon Sep 17 00:00:00 2001 From: Charles Sarrazin Date: Fri, 25 Sep 2015 11:29:08 +0200 Subject: [PATCH 1/2] Introducing the LDAP component --- .travis.yml | 1 + appveyor.yml | 1 + src/Symfony/Component/Ldap/.gitignore | 3 + .../Ldap/Exception/ConnectionException.php | 21 ++ .../Ldap/Exception/LdapException.php | 21 ++ src/Symfony/Component/Ldap/LICENSE | 19 ++ src/Symfony/Component/Ldap/LdapClient.php | 220 ++++++++++++++++++ .../Component/Ldap/LdapClientInterface.php | 49 ++++ src/Symfony/Component/Ldap/README.md | 23 ++ .../Component/Ldap/Tests/LdapClientTest.php | 56 +++++ src/Symfony/Component/Ldap/composer.json | 34 +++ src/Symfony/Component/Ldap/phpunit.xml.dist | 28 +++ 12 files changed, 476 insertions(+) create mode 100644 src/Symfony/Component/Ldap/.gitignore create mode 100644 src/Symfony/Component/Ldap/Exception/ConnectionException.php create mode 100644 src/Symfony/Component/Ldap/Exception/LdapException.php create mode 100644 src/Symfony/Component/Ldap/LICENSE create mode 100644 src/Symfony/Component/Ldap/LdapClient.php create mode 100644 src/Symfony/Component/Ldap/LdapClientInterface.php create mode 100644 src/Symfony/Component/Ldap/README.md create mode 100644 src/Symfony/Component/Ldap/Tests/LdapClientTest.php create mode 100644 src/Symfony/Component/Ldap/composer.json create mode 100644 src/Symfony/Component/Ldap/phpunit.xml.dist diff --git a/.travis.yml b/.travis.yml index 23533ea632643..764897ff66da4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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)" diff --git a/appveyor.yml b/appveyor.yml index 2e4d1cce40218..069bee46c5230 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -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) diff --git a/src/Symfony/Component/Ldap/.gitignore b/src/Symfony/Component/Ldap/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/Ldap/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/Ldap/Exception/ConnectionException.php b/src/Symfony/Component/Ldap/Exception/ConnectionException.php new file mode 100644 index 0000000000000..80d9af51ea911 --- /dev/null +++ b/src/Symfony/Component/Ldap/Exception/ConnectionException.php @@ -0,0 +1,21 @@ + + * + * 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 + */ +class ConnectionException extends \RuntimeException +{ +} diff --git a/src/Symfony/Component/Ldap/Exception/LdapException.php b/src/Symfony/Component/Ldap/Exception/LdapException.php new file mode 100644 index 0000000000000..213625b021b2b --- /dev/null +++ b/src/Symfony/Component/Ldap/Exception/LdapException.php @@ -0,0 +1,21 @@ + + * + * 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 + */ +class LdapException extends \RuntimeException +{ +} diff --git a/src/Symfony/Component/Ldap/LICENSE b/src/Symfony/Component/Ldap/LICENSE new file mode 100644 index 0000000000000..43028bc600f26 --- /dev/null +++ b/src/Symfony/Component/Ldap/LICENSE @@ -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. diff --git a/src/Symfony/Component/Ldap/LdapClient.php b/src/Symfony/Component/Ldap/LdapClient.php new file mode 100644 index 0000000000000..5887a623ad84e --- /dev/null +++ b/src/Symfony/Component/Ldap/LdapClient.php @@ -0,0 +1,220 @@ + + * @author Francis Besset + * @author Charles Sarrazin + */ +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 + * + * @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; + } +} diff --git a/src/Symfony/Component/Ldap/LdapClientInterface.php b/src/Symfony/Component/Ldap/LdapClientInterface.php new file mode 100644 index 0000000000000..75e063d339dfc --- /dev/null +++ b/src/Symfony/Component/Ldap/LdapClientInterface.php @@ -0,0 +1,49 @@ + + * @author Charles Sarrazin + */ +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); +} diff --git a/src/Symfony/Component/Ldap/README.md b/src/Symfony/Component/Ldap/README.md new file mode 100644 index 0000000000000..751ce6ad2511d --- /dev/null +++ b/src/Symfony/Component/Ldap/README.md @@ -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 diff --git a/src/Symfony/Component/Ldap/Tests/LdapClientTest.php b/src/Symfony/Component/Ldap/Tests/LdapClientTest.php new file mode 100644 index 0000000000000..5b67ea8540458 --- /dev/null +++ b/src/Symfony/Component/Ldap/Tests/LdapClientTest.php @@ -0,0 +1,56 @@ + + * + * 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'), + ); + } +} diff --git a/src/Symfony/Component/Ldap/composer.json b/src/Symfony/Component/Ldap/composer.json new file mode 100644 index 0000000000000..5fa6dda3d72f3 --- /dev/null +++ b/src/Symfony/Component/Ldap/composer.json @@ -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" + } + } +} diff --git a/src/Symfony/Component/Ldap/phpunit.xml.dist b/src/Symfony/Component/Ldap/phpunit.xml.dist new file mode 100644 index 0000000000000..82f3331146ada --- /dev/null +++ b/src/Symfony/Component/Ldap/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + From 60b9f2e7ec4538a85eeb4ece7922ebe5531bd60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 25 Sep 2015 11:30:03 +0200 Subject: [PATCH 2/2] Implemented LDAP authentication and LDAP user provider --- .../Security/Factory/FormLoginLdapFactory.php | 57 +++++++++ .../Security/Factory/HttpBasicLdapFactory.php | 67 +++++++++++ .../Security/UserProvider/LdapFactory.php | 64 ++++++++++ .../Resources/config/security.xml | 12 ++ .../Resources/config/security_listeners.xml | 9 ++ .../Bundle/SecurityBundle/SecurityBundle.php | 6 + .../LdapBindAuthenticationProvider.php | 76 ++++++++++++ .../LdapBindAuthenticationProviderTest.php | 61 ++++++++++ .../Core/Tests/User/LdapUserProviderTest.php | 93 +++++++++++++++ .../Security/Core/User/LdapUserProvider.php | 109 ++++++++++++++++++ .../Component/Security/Core/composer.json | 4 +- src/Symfony/Component/Security/composer.json | 6 +- 12 files changed, 561 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php create mode 100644 src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php create mode 100644 src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php create mode 100644 src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php create mode 100644 src/Symfony/Component/Security/Core/User/LdapUserProvider.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php new file mode 100644 index 0000000000000..c758b32b8d867 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php @@ -0,0 +1,57 @@ + + * + * 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 + * @author Charles Sarrazin + */ +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'; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php new file mode 100644 index 0000000000000..23c0130584089 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php @@ -0,0 +1,67 @@ + + * + * 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 + * @author Grégoire Pineau + * @author Charles Sarrazin + */ +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'; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php new file mode 100644 index 0000000000000..068cda6a1fe81 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php @@ -0,0 +1,64 @@ + + * + * 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 + * @author Charles Sarrazin + */ +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() + ; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index b7c1407c1cc56..b1a2cdfc80cb7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -158,10 +158,21 @@ + + + + + + + + + + + @@ -169,6 +180,7 @@ + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 917e90f79281b..948286bb5a857 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -223,6 +223,15 @@ %security.authentication.hide_user_not_found% + + + + + + + %security.authentication.hide_user_not_found% + + diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index f2dcab1061d1f..f2dfc991fbcef 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -15,7 +15,9 @@ 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\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 @@ public function build(ContainerBuilder $container) $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 @@ public function build(ContainerBuilder $container) $extension->addSecurityListenerFactory(new GuardAuthenticationFactory()); $extension->addUserProviderFactory(new InMemoryFactory()); + $extension->addUserProviderFactory(new LdapFactory()); $container->addCompilerPass(new AddSecurityVotersPass()); } } diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php new file mode 100644 index 0000000000000..9ce3bd2428425 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php @@ -0,0 +1,76 @@ + + */ +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.'); + } + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php new file mode 100644 index 0000000000000..f1b5c0355ff66 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php @@ -0,0 +1,61 @@ + + * + * 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')); + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php b/src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php new file mode 100644 index 0000000000000..b69987aa53f0e --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php @@ -0,0 +1,93 @@ +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') + ); + } +} diff --git a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php new file mode 100644 index 0000000000000..ec699fc022739 --- /dev/null +++ b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php @@ -0,0 +1,109 @@ + + * + * 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 + * @author Charles Sarrazin + */ +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'; + } + +} diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json index 4d2405317c89f..6a1ac993c9db9 100644 --- a/src/Symfony/Component/Security/Core/composer.json +++ b/src/Symfony/Component/Security/Core/composer.json @@ -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": { diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json index e4e9a8b701e18..87a47a53ade99 100644 --- a/src/Symfony/Component/Security/composer.json +++ b/src/Symfony/Component/Security/composer.json @@ -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\\": "" } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy