From 81cb79b10a0a9f6fa0e40e225bb9bd48310d025f Mon Sep 17 00:00:00 2001 From: Charles Sarrazin Date: Thu, 28 Jan 2016 14:54:41 +0100 Subject: [PATCH 1/2] Improved the Ldap Component * Moved connection logic to dedicated class * Added support for Ldap result entries iterator and renamed LdapClient to Ldap * Added support for multiple adapters * Attempt anonymous bind if the connection is not bound beforehand * Finalized API * Updated the Security component to use v3.1 of the Ldap component * Updated unit tests * Added support for functional tests * Updated README file --- .travis.yml | 7 + appveyor.yml | 1 - .../Ldap/Adapter/AbstractConnection.php | 45 ++++++ .../Component/Ldap/Adapter/AbstractQuery.php | 48 ++++++ .../Ldap/Adapter/AdapterInterface.php | 47 ++++++ .../Ldap/Adapter/CollectionInterface.php | 25 +++ .../Ldap/Adapter/ConnectionInterface.php | 33 ++++ .../Ldap/Adapter/ExtLdap/Adapter.php | 74 +++++++++ .../Ldap/Adapter/ExtLdap/Collection.php | 108 +++++++++++++ .../Ldap/Adapter/ExtLdap/Connection.php | 89 +++++++++++ .../Component/Ldap/Adapter/ExtLdap/Query.php | 72 +++++++++ .../Ldap/Adapter/ExtLdap/ResultIterator.php | 81 ++++++++++ .../Component/Ldap/Adapter/QueryInterface.php | 35 +++++ src/Symfony/Component/Ldap/Entry.php | 62 ++++++++ .../Ldap/Exception/ConnectionException.php | 4 +- .../Exception/DriverNotFoundException.php | 21 +++ .../Ldap/Exception/ExceptionInterface.php | 21 +++ .../Ldap/Exception/LdapException.php | 4 +- src/Symfony/Component/Ldap/Ldap.php | 81 ++++++++++ src/Symfony/Component/Ldap/LdapClient.php | 145 ------------------ ...pClientInterface.php => LdapInterface.php} | 18 ++- src/Symfony/Component/Ldap/README.md | 9 +- .../Tests/Adapter/ExtLdap/AdapterTest.php | 50 ++++++ .../Ldap/Tests/Fixtures/conf/slapd.conf | 17 ++ .../Ldap/Tests/Fixtures/data/base.ldif | 4 + .../Ldap/Tests/Fixtures/data/fixtures.ldif | 14 ++ .../Component/Ldap/Tests/LdapClientTest.php | 28 ---- src/Symfony/Component/Ldap/Tests/LdapTest.php | 83 ++++++++++ src/Symfony/Component/Ldap/composer.json | 1 + .../LdapBindAuthenticationProvider.php | 8 +- .../LdapBindAuthenticationProviderTest.php | 15 +- .../Core/Tests/User/LdapUserProviderTest.php | 81 +++++++--- .../Security/Core/User/LdapUserProvider.php | 48 +++--- .../Component/Security/Core/composer.json | 2 +- src/Symfony/Component/Security/composer.json | 2 +- 35 files changed, 1136 insertions(+), 247 deletions(-) create mode 100644 src/Symfony/Component/Ldap/Adapter/AbstractConnection.php create mode 100644 src/Symfony/Component/Ldap/Adapter/AbstractQuery.php create mode 100644 src/Symfony/Component/Ldap/Adapter/AdapterInterface.php create mode 100644 src/Symfony/Component/Ldap/Adapter/CollectionInterface.php create mode 100644 src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php create mode 100644 src/Symfony/Component/Ldap/Adapter/ExtLdap/Adapter.php create mode 100644 src/Symfony/Component/Ldap/Adapter/ExtLdap/Collection.php create mode 100644 src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php create mode 100644 src/Symfony/Component/Ldap/Adapter/ExtLdap/Query.php create mode 100644 src/Symfony/Component/Ldap/Adapter/ExtLdap/ResultIterator.php create mode 100644 src/Symfony/Component/Ldap/Adapter/QueryInterface.php create mode 100644 src/Symfony/Component/Ldap/Entry.php create mode 100644 src/Symfony/Component/Ldap/Exception/DriverNotFoundException.php create mode 100644 src/Symfony/Component/Ldap/Exception/ExceptionInterface.php create mode 100644 src/Symfony/Component/Ldap/Ldap.php delete mode 100644 src/Symfony/Component/Ldap/LdapClient.php rename src/Symfony/Component/Ldap/{LdapClientInterface.php => LdapInterface.php} (76%) create mode 100644 src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php create mode 100644 src/Symfony/Component/Ldap/Tests/Fixtures/conf/slapd.conf create mode 100644 src/Symfony/Component/Ldap/Tests/Fixtures/data/base.ldif create mode 100644 src/Symfony/Component/Ldap/Tests/Fixtures/data/fixtures.ldif delete mode 100644 src/Symfony/Component/Ldap/Tests/LdapClientTest.php create mode 100644 src/Symfony/Component/Ldap/Tests/LdapTest.php diff --git a/.travis.yml b/.travis.yml index 0c8fd6a2e4cd8..71d285fc84e3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,8 @@ addons: apt_packages: - parallel - language-pack-fr-base + - ldap-utils + - slapd env: global: @@ -48,6 +50,11 @@ before_install: - if [[ $deps != skip ]]; then composer self-update; fi; - if [[ $deps != skip ]]; then ./phpunit install; fi; - export PHPUNIT=$(readlink -f ./phpunit) + - mkdir /tmp/slapd + - slapd -f src/Symfony/Component/Ldap/Tests/Fixtures/conf/slapd.conf -h ldap://localhost:3389 & + - sleep 3 + - ldapadd -h localhost:3389 -D cn=admin,dc=symfony,dc=com -w symfony -f src/Symfony/Component/Ldap/Tests/Fixtures/data/base.ldif + - ldapadd -h localhost:3389 -D cn=admin,dc=symfony,dc=com -w symfony -f src/Symfony/Component/Ldap/Tests/Fixtures/data/fixtures.ldif install: - if [[ $deps != skip ]]; then COMPONENTS=$(find src/Symfony -mindepth 3 -type f -name phpunit.xml.dist -printf '%h\n'); fi; diff --git a/appveyor.yml b/appveyor.yml index 6456c27916d13..9386cf7e3a240 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -43,7 +43,6 @@ install: - IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini-max - IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini-max - IF %PHP%==1 echo extension=php_pdo_sqlite.dll >> php.ini-max - - IF %PHP%==1 echo extension=php_ldap.dll >> php.ini-max - appveyor DownloadFile https://getcomposer.org/composer.phar - copy /Y php.ini-max php.ini - cd c:\projects\symfony diff --git a/src/Symfony/Component/Ldap/Adapter/AbstractConnection.php b/src/Symfony/Component/Ldap/Adapter/AbstractConnection.php new file mode 100644 index 0000000000000..2f012a66639a3 --- /dev/null +++ b/src/Symfony/Component/Ldap/Adapter/AbstractConnection.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Adapter; + +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Charles Sarrazin + */ +abstract class AbstractConnection implements ConnectionInterface +{ + protected $config; + + public function __construct(array $config = array()) + { + $resolver = new OptionsResolver(); + $resolver->setDefaults(array( + 'host' => null, + 'port' => 389, + 'version' => 3, + 'useSsl' => false, + 'useStartTls' => false, + 'optReferrals' => false, + )); + $resolver->setNormalizer('host', function (Options $options, $value) { + if ($value && $options['useSsl']) { + return 'ldaps://'.$value; + } + + return $value; + }); + + $this->config = $resolver->resolve($config); + } +} diff --git a/src/Symfony/Component/Ldap/Adapter/AbstractQuery.php b/src/Symfony/Component/Ldap/Adapter/AbstractQuery.php new file mode 100644 index 0000000000000..41889da1333d4 --- /dev/null +++ b/src/Symfony/Component/Ldap/Adapter/AbstractQuery.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Adapter; + +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; + +/** + * @author Charles Sarrazin + */ +abstract class AbstractQuery implements QueryInterface +{ + protected $connection; + protected $dn; + protected $query; + protected $options; + + public function __construct(ConnectionInterface $connection, $dn, $query, array $options = array()) + { + $resolver = new OptionsResolver(); + $resolver->setDefaults(array( + 'filter' => '*', + 'maxItems' => 0, + 'sizeLimit' => 0, + 'timeout' => 0, + 'deref' => static::DEREF_NEVER, + 'attrsOnly' => 0, + )); + $resolver->setAllowedValues('deref', array(static::DEREF_ALWAYS, static::DEREF_NEVER, static::DEREF_FINDING, static::DEREF_SEARCHING)); + $resolver->setNormalizer('filter', function (Options $options, $value) { + return is_array($value) ? $value : array($value); + }); + + $this->connection = $connection; + $this->dn = $dn; + $this->query = $query; + $this->options = $resolver->resolve($options); + } +} diff --git a/src/Symfony/Component/Ldap/Adapter/AdapterInterface.php b/src/Symfony/Component/Ldap/Adapter/AdapterInterface.php new file mode 100644 index 0000000000000..1e2f72d5bddff --- /dev/null +++ b/src/Symfony/Component/Ldap/Adapter/AdapterInterface.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Adapter; + +/** + * @author Charles Sarrazin + */ +interface AdapterInterface +{ + /** + * Returns the current connection. + * + * @return ConnectionInterface + */ + public function getConnection(); + + /** + * Creates a new Query. + * + * @param $dn + * @param $query + * @param array $options + * + * @return QueryInterface + */ + public function createQuery($dn, $query, array $options = array()); + + /** + * 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/Adapter/CollectionInterface.php b/src/Symfony/Component/Ldap/Adapter/CollectionInterface.php new file mode 100644 index 0000000000000..2db4d2bd4a297 --- /dev/null +++ b/src/Symfony/Component/Ldap/Adapter/CollectionInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Adapter; + +use Symfony\Component\Ldap\Entry; + +/** + * @author Charles Sarrazin + */ +interface CollectionInterface extends \Countable, \IteratorAggregate, \ArrayAccess +{ + /** + * @return Entry[] + */ + public function toArray(); +} diff --git a/src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php b/src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php new file mode 100644 index 0000000000000..347a852a82ea3 --- /dev/null +++ b/src/Symfony/Component/Ldap/Adapter/ConnectionInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Adapter; + +/** + * @author Charles Sarrazin + */ +interface ConnectionInterface +{ + /** + * Checks whether the connection was already bound or not. + * + * @return bool + */ + public function isBound(); + + /** + * Binds the connection against a DN and password. + * + * @param string $dn The user's DN + * @param string $password The associated password + */ + public function bind($dn = null, $password = null); +} diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Adapter.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Adapter.php new file mode 100644 index 0000000000000..4bf0303df0574 --- /dev/null +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Adapter.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Adapter\ExtLdap; + +use Symfony\Component\Ldap\Adapter\AdapterInterface; +use Symfony\Component\Ldap\Exception\LdapException; + +/** + * @author Charles Sarrazin + */ +class Adapter implements AdapterInterface +{ + private $config; + private $connection; + + public function __construct(array $config = array()) + { + if (!extension_loaded('ldap')) { + throw new LdapException('The LDAP PHP extension is not enabled.'); + } + + $this->config = $config; + } + + /** + * {@inheritdoc} + */ + public function getConnection() + { + if (null === $this->connection) { + $this->connection = new Connection($this->config); + } + + return $this->connection; + } + + /** + * {@inheritdoc} + */ + public function createQuery($dn, $query, array $options = array()) + { + return new Query($this->getConnection(), $dn, $query, $options); + } + + /** + * {@inheritdoc} + */ + public function escape($subject, $ignore = '', $flags = 0) + { + $value = ldap_escape($subject, $ignore, $flags); + + // Per RFC 4514, leading/trailing spaces should be encoded in DNs, as well as carriage returns. + if ((int) $flags & LDAP_ESCAPE_DN) { + if (!empty($value) && $value[0] === ' ') { + $value = '\\20'.substr($value, 1); + } + if (!empty($value) && $value[strlen($value) - 1] === ' ') { + $value = substr($value, 0, -1).'\\20'; + } + $value = str_replace("\r", '\0d', $value); + } + + return $value; + } +} diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Collection.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Collection.php new file mode 100644 index 0000000000000..e56c0d916ed16 --- /dev/null +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Collection.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Adapter\ExtLdap; + +use Symfony\Component\Ldap\Adapter\CollectionInterface; +use Symfony\Component\Ldap\Entry; + +/** + * @author Charles Sarrazin + */ +class Collection implements CollectionInterface +{ + private $connection; + private $search; + private $entries; + + public function __construct(Connection $connection, Query $search, array $entries = array()) + { + $this->connection = $connection; + $this->search = $search; + $this->entries = array(); + } + + /** + * {@inheritdoc} + */ + public function toArray() + { + $this->initialize(); + + return $this->entries; + } + + public function count() + { + $this->initialize(); + + return count($this->entries); + } + + public function getIterator() + { + return new ResultIterator($this->connection, $this->search); + } + + public function offsetExists($offset) + { + $this->initialize(); + + return isset($this->entries[$offset]); + } + + public function offsetGet($offset) + { + return isset($this->entries[$offset]) ? $this->entries[$offset] : null; + } + + public function offsetSet($offset, $value) + { + $this->initialize(); + + $this->entries[$offset] = $value; + } + + public function offsetUnset($offset) + { + $this->initialize(); + + unset($this->entries[$offset]); + } + + private function initialize() + { + if (null === $this->entries) { + return; + } + + $entries = ldap_get_entries($this->connection->getResource(), $this->search->getResource()); + + if (0 === $entries['count']) { + return array(); + } + + unset($entries['count']); + + $this->entries = array_map(function (array $entry) { + $dn = $entry['dn']; + $attributes = array_diff_key($entry, array_flip(range(0, $entry['count'] - 1)) + array( + 'count' => null, + 'dn' => null, + )); + array_walk($attributes, function (&$value) { + unset($value['count']); + }); + + return new Entry($dn, $attributes); + }, $entries); + } +} diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php new file mode 100644 index 0000000000000..103c4d343a069 --- /dev/null +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Connection.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Adapter\ExtLdap; + +use Symfony\Component\Ldap\Adapter\AbstractConnection; +use Symfony\Component\Ldap\Exception\ConnectionException; + +/** + * @author Charles Sarrazin + */ +class Connection extends AbstractConnection +{ + /** @var bool */ + private $bound = false; + + /** @var resource */ + private $connection; + + public function __destruct() + { + $this->disconnect(); + } + + public function isBound() + { + return $this->bound; + } + + /** + * {@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)); + } + + $this->bound = true; + } + + /** + * Returns a link resource. + * + * @return resource + */ + public function getResource() + { + return $this->connection; + } + + private function connect() + { + if ($this->connection) { + return; + } + $host = $this->config['host']; + + ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $this->config['version']); + ldap_set_option($this->connection, LDAP_OPT_REFERRALS, $this->config['optReferrals']); + + $this->connection = ldap_connect($host, $this->config['port']); + + if ($this->config['useStartTls']) { + ldap_start_tls($this->connection); + } + } + + private function disconnect() + { + if ($this->connection && is_resource($this->connection)) { + ldap_close($this->connection); + } + + $this->connection = null; + } +} diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/Query.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Query.php new file mode 100644 index 0000000000000..bbff589f76f16 --- /dev/null +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/Query.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Adapter\ExtLdap; + +use Symfony\Component\Ldap\Adapter\AbstractQuery; +use Symfony\Component\Ldap\Exception\LdapException; + +/** + * @author Charles Sarrazin + */ +class Query extends AbstractQuery +{ + /** @var Connection */ + protected $connection; + + /** @var resource */ + private $search; + + public function __construct(Connection $connection, $dn, $query, array $options = array()) + { + parent::__construct($connection, $dn, $query, $options); + } + + /** + * {@inheritdoc} + */ + public function execute() + { + // If the connection is not bound, then we try an anonymous bind. + if (!$this->connection->isBound()) { + $this->connection->bind(); + } + + $con = $this->connection->getResource(); + + $this->search = ldap_search( + $con, + $this->dn, + $this->query, + $this->options['filter'], + $this->options['attrsOnly'], + $this->options['maxItems'], + $this->options['timeout'], + $this->options['deref'] + ); + + if (!$this->search) { + throw new LdapException(sprintf('Could not complete search with dn "%s", query "%s" and filters "%s"', $this->dn, $this->query, implode(',', $this->options['filter']))); + }; + + return new Collection($this->connection, $this); + } + + /** + * Returns a LDAP search resource. + * + * @return resource + */ + public function getResource() + { + return $this->search; + } +} diff --git a/src/Symfony/Component/Ldap/Adapter/ExtLdap/ResultIterator.php b/src/Symfony/Component/Ldap/Adapter/ExtLdap/ResultIterator.php new file mode 100644 index 0000000000000..997e6b48cc0ce --- /dev/null +++ b/src/Symfony/Component/Ldap/Adapter/ExtLdap/ResultIterator.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Adapter\ExtLdap; + +use Symfony\Component\Ldap\Entry; + +/** + * @author Charles Sarrazin + */ +class ResultIterator implements \Iterator +{ + private $connection; + private $search; + private $current; + private $key; + + public function __construct(Connection $connection, Query $search) + { + $this->connection = $connection->getResource(); + $this->search = $search->getResource(); + } + + /** + * Fetches the current entry. + * + * @return Entry + */ + public function current() + { + $attributes = ldap_get_attributes($this->connection, $this->current); + $dn = ldap_get_dn($this->connection, $this->current); + + return new Entry($dn, $attributes); + } + + /** + * Sets the cursor to the next entry. + */ + public function next() + { + $this->current = ldap_next_entry($this->connection, $this->current); + ++$this->key; + } + + /** + * Returns the current key. + * + * @return int + */ + public function key() + { + return $this->key; + } + + /** + * Checks whether the current entry is valid or not. + * + * @return bool + */ + public function valid() + { + return false !== $this->current; + } + + /** + * Rewinds the iterator to the first entry. + */ + public function rewind() + { + $this->current = ldap_first_entry($this->connection, $this->search); + } +} diff --git a/src/Symfony/Component/Ldap/Adapter/QueryInterface.php b/src/Symfony/Component/Ldap/Adapter/QueryInterface.php new file mode 100644 index 0000000000000..db100a0688333 --- /dev/null +++ b/src/Symfony/Component/Ldap/Adapter/QueryInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @author Charles Sarrazin + */ +namespace Symfony\Component\Ldap\Adapter; + +use Symfony\Component\Ldap\Entry; + +/** + * @author Charles Sarrazin + */ +interface QueryInterface +{ + const DEREF_NEVER = 0; + const DEREF_SEARCHING = 1; + const DEREF_FINDING = 2; + const DEREF_ALWAYS = 3; + + /** + * Executes a query and returns the list of Ldap entries. + * + * @return CollectionInterface|Entry[] + */ + public function execute(); +} diff --git a/src/Symfony/Component/Ldap/Entry.php b/src/Symfony/Component/Ldap/Entry.php new file mode 100644 index 0000000000000..f4da743742b57 --- /dev/null +++ b/src/Symfony/Component/Ldap/Entry.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap; + +/** + * @author Charles Sarrazin + */ +class Entry +{ + private $dn; + private $attributes; + + public function __construct($dn, array $attributes = array()) + { + $this->dn = $dn; + $this->attributes = $attributes; + } + + /** + * Returns the entry's DN. + * + * @return string + */ + public function getDn() + { + return $this->dn; + } + + /** + * Returns a specific attribute's value. + * + * As LDAP can return multiple values for a single attribute, + * this value is returned as an array. + * + * @param $name string The name of the attribute + * + * @return null|array + */ + public function getAttribute($name) + { + return isset($this->attributes[$name]) ? $this->attributes[$name] : null; + } + + /** + * Returns the complete list of attributes. + * + * @return array + */ + public function getAttributes() + { + return $this->attributes; + } +} diff --git a/src/Symfony/Component/Ldap/Exception/ConnectionException.php b/src/Symfony/Component/Ldap/Exception/ConnectionException.php index d5023c5d02d7c..cded4cf2a389a 100644 --- a/src/Symfony/Component/Ldap/Exception/ConnectionException.php +++ b/src/Symfony/Component/Ldap/Exception/ConnectionException.php @@ -15,9 +15,7 @@ * ConnectionException is throw if binding to ldap can not be established. * * @author Grégoire Pineau - * - * @internal */ -class ConnectionException extends \RuntimeException +class ConnectionException extends \RuntimeException implements ExceptionInterface { } diff --git a/src/Symfony/Component/Ldap/Exception/DriverNotFoundException.php b/src/Symfony/Component/Ldap/Exception/DriverNotFoundException.php new file mode 100644 index 0000000000000..40258435bb6a7 --- /dev/null +++ b/src/Symfony/Component/Ldap/Exception/DriverNotFoundException.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 Charles Sarrazin + */ +class DriverNotFoundException extends \RuntimeException implements ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Ldap/Exception/ExceptionInterface.php b/src/Symfony/Component/Ldap/Exception/ExceptionInterface.php new file mode 100644 index 0000000000000..b861a3fe8d3ca --- /dev/null +++ b/src/Symfony/Component/Ldap/Exception/ExceptionInterface.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; + +/** + * Base ExceptionInterface for the Ldap component. + * + * @author Charles Sarrazin + */ +interface ExceptionInterface +{ +} diff --git a/src/Symfony/Component/Ldap/Exception/LdapException.php b/src/Symfony/Component/Ldap/Exception/LdapException.php index ef3bd929bb852..4045f32cf44b5 100644 --- a/src/Symfony/Component/Ldap/Exception/LdapException.php +++ b/src/Symfony/Component/Ldap/Exception/LdapException.php @@ -15,9 +15,7 @@ * LdapException is throw if php ldap module is not loaded. * * @author Grégoire Pineau - * - * @internal */ -class LdapException extends \RuntimeException +class LdapException extends \RuntimeException implements ExceptionInterface { } diff --git a/src/Symfony/Component/Ldap/Ldap.php b/src/Symfony/Component/Ldap/Ldap.php new file mode 100644 index 0000000000000..76136ad77456a --- /dev/null +++ b/src/Symfony/Component/Ldap/Ldap.php @@ -0,0 +1,81 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap; + +use Symfony\Component\Ldap\Adapter\AdapterInterface; +use Symfony\Component\Ldap\Exception\DriverNotFoundException; + +/** + * @author Grégoire Pineau + * @author Francis Besset + * @author Charles Sarrazin + */ +final class Ldap implements LdapInterface +{ + private $adapter; + + private static $adapterMap = array( + 'ext_ldap' => 'Symfony\Component\Ldap\Adapter\ExtLdap\Adapter', + ); + + public function __construct(AdapterInterface $adapter) + { + $this->adapter = $adapter; + } + + /** + * {@inheritdoc} + */ + public function bind($dn = null, $password = null) + { + $this->adapter->getConnection()->bind($dn, $password); + } + + /** + * {@inheritdoc} + */ + public function query($dn, $query, array $options = array()) + { + return $this->adapter->createQuery($dn, $query, $options); + } + + /** + * {@inheritdoc} + */ + public function escape($subject, $ignore = '', $flags = 0) + { + return $this->adapter->escape($subject, $ignore, $flags); + } + + /** + * Creates a new Ldap instance. + * + * @param string $adapter The adapter name + * @param array $config The adapter's configuration + * + * @return static + */ + public static function create($adapter, array $config = array()) + { + if (!isset(self::$adapterMap[$adapter])) { + throw new DriverNotFoundException(sprintf( + 'Adapter "%s" not found. You should use one of: %s', + $adapter, + implode(', ', self::$adapterMap) + )); + } + + $class = self::$adapterMap[$adapter]; + + return new self(new $class($config)); + } +} diff --git a/src/Symfony/Component/Ldap/LdapClient.php b/src/Symfony/Component/Ldap/LdapClient.php deleted file mode 100644 index e7a0bc45e64d9..0000000000000 --- a/src/Symfony/Component/Ldap/LdapClient.php +++ /dev/null @@ -1,145 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Ldap; - -use Symfony\Component\Ldap\Exception\ConnectionException; -use Symfony\Component\Ldap\Exception\LdapException; - -/** - * @author Grégoire Pineau - * @author Francis Besset - * @author Charles Sarrazin - * - * @internal - */ -class LdapClient implements LdapClientInterface -{ - private $host; - private $port; - private $version; - private $useSsl; - private $useStartTls; - private $optReferrals; - private $connection; - - /** - * 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) - { - $value = ldap_escape($subject, $ignore, $flags); - - // Per RFC 4514, leading/trailing spaces should be encoded in DNs, as well as carriage returns. - if ((int) $flags & LDAP_ESCAPE_DN) { - if (!empty($value) && $value[0] === ' ') { - $value = '\\20'.substr($value, 1); - } - if (!empty($value) && $value[strlen($value) - 1] === ' ') { - $value = substr($value, 0, -1).'\\20'; - } - $value = str_replace("\r", '\0d', $value); - } - - return $value; - } - - private function connect() - { - if (!$this->connection) { - $host = $this->host; - - if ($this->useSsl) { - $host = 'ldaps://'.$host; - } - - $this->connection = ldap_connect($host, $this->port); - - ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $this->version); - ldap_set_option($this->connection, LDAP_OPT_REFERRALS, $this->optReferrals); - - 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; - } -} diff --git a/src/Symfony/Component/Ldap/LdapClientInterface.php b/src/Symfony/Component/Ldap/LdapInterface.php similarity index 76% rename from src/Symfony/Component/Ldap/LdapClientInterface.php rename to src/Symfony/Component/Ldap/LdapInterface.php index dcdc0818da10b..f0e9a3ef6c651 100644 --- a/src/Symfony/Component/Ldap/LdapClientInterface.php +++ b/src/Symfony/Component/Ldap/LdapInterface.php @@ -11,6 +11,7 @@ namespace Symfony\Component\Ldap; +use Symfony\Component\Ldap\Adapter\QueryInterface; use Symfony\Component\Ldap\Exception\ConnectionException; /** @@ -18,11 +19,12 @@ * * @author Grégoire Pineau * @author Charles Sarrazin - * - * @internal */ -interface LdapClientInterface +interface LdapInterface { + const ESCAPE_FILTER = 0x01; + const ESCAPE_DN = 0x02; + /** * Return a connection bound to the ldap. * @@ -33,16 +35,16 @@ interface LdapClientInterface */ public function bind($dn = null, $password = null); - /* - * Find a username into ldap connection. + /** + * Queries a ldap server for entries matching the given criteria. * * @param string $dn * @param string $query - * @param mixed $filter + * @param array $options * - * @return array|null + * @return QueryInterface */ - public function find($dn, $query, $filter = '*'); + public function query($dn, $query, array $options = array()); /** * Escape a string for use in an LDAP filter or DN. diff --git a/src/Symfony/Component/Ldap/README.md b/src/Symfony/Component/Ldap/README.md index 6de60f10bbb9e..28ac472da608d 100644 --- a/src/Symfony/Component/Ldap/README.md +++ b/src/Symfony/Component/Ldap/README.md @@ -6,9 +6,10 @@ A Ldap client for PHP on top of PHP's ldap extension. Disclaimer ---------- -This component is currently marked as internal, as it -still needs some work. Breaking changes will be introduced -in the next minor version of Symfony. +This component is only stable since Symfony 3.1. Earlier versions +have been marked as internal as they still needed some work. +Breaking changes were introduced in Symfony 3.1, so code relying on +previous version of the component will break with this version. Documentation ------------- @@ -24,4 +25,4 @@ You can run the unit tests with the following command: $ composer install $ phpunit -[0]: https://symfony.com/doc/2.8/components/ldap.html +[0]: https://symfony.com/doc/3.1/components/ldap.html diff --git a/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php b/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php new file mode 100644 index 0000000000000..6d478d6911861 --- /dev/null +++ b/src/Symfony/Component/Ldap/Tests/Adapter/ExtLdap/AdapterTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Tests; + +use Symfony\Component\Ldap\Adapter\ExtLdap\Adapter; +use Symfony\Component\Ldap\Adapter\ExtLdap\Collection; +use Symfony\Component\Ldap\Entry; +use Symfony\Component\Ldap\LdapInterface; + +/** + * @requires extension ldap + */ +class AdapterTest extends \PHPUnit_Framework_TestCase +{ + public function testLdapEscape() + { + $ldap = new Adapter(); + + $this->assertEquals('\20foo\3dbar\0d(baz)*\20', $ldap->escape(" foo=bar\r(baz)* ", null, LdapInterface::ESCAPE_DN)); + } + + /** + * @group functional + */ + public function testLdapQuery() + { + $ldap = new Adapter(array('host' => 'localhost', 'port' => 3389)); + + $ldap->getConnection()->bind('cn=admin,dc=symfony,dc=com', 'symfony'); + $query = $ldap->createQuery('dc=symfony,dc=com', '(&(objectclass=person)(ou=Maintainers))', array()); + $result = $query->execute(); + + $this->assertInstanceOf(Collection::class, $result); + $this->assertCount(1, $result); + + $entry = $result[0]; + $this->assertInstanceOf(Entry::class, $entry); + $this->assertEquals(array('Fabien Potencier'), $entry->getAttribute('cn')); + $this->assertEquals(array('fabpot@symfony.com', 'fabien@potencier.com'), $entry->getAttribute('mail')); + } +} diff --git a/src/Symfony/Component/Ldap/Tests/Fixtures/conf/slapd.conf b/src/Symfony/Component/Ldap/Tests/Fixtures/conf/slapd.conf new file mode 100644 index 0000000000000..35f7fe5652b38 --- /dev/null +++ b/src/Symfony/Component/Ldap/Tests/Fixtures/conf/slapd.conf @@ -0,0 +1,17 @@ +# See slapd.conf(5) for details on configuration options. +include /etc/ldap/schema/core.schema +include /etc/ldap/schema/cosine.schema +include /etc/ldap/schema/inetorgperson.schema +include /etc/ldap/schema/nis.schema + +pidfile /tmp/slapd/slapd.pid +argsfile /tmp/slapd/slapd.args + +modulepath /usr/lib/openldap + +database ldif +directory /tmp/slapd + +suffix "dc=symfony,dc=com" +rootdn "cn=admin,dc=symfony,dc=com" +rootpw {SSHA}btWUi971ytYpVMbZLkaQ2A6ETh3VA0lL diff --git a/src/Symfony/Component/Ldap/Tests/Fixtures/data/base.ldif b/src/Symfony/Component/Ldap/Tests/Fixtures/data/base.ldif new file mode 100644 index 0000000000000..25abb296c9a6c --- /dev/null +++ b/src/Symfony/Component/Ldap/Tests/Fixtures/data/base.ldif @@ -0,0 +1,4 @@ +dn: dc=symfony,dc=com +objectClass: dcObject +objectClass: organizationalUnit +ou: Organization diff --git a/src/Symfony/Component/Ldap/Tests/Fixtures/data/fixtures.ldif b/src/Symfony/Component/Ldap/Tests/Fixtures/data/fixtures.ldif new file mode 100644 index 0000000000000..21dc42d77edd4 --- /dev/null +++ b/src/Symfony/Component/Ldap/Tests/Fixtures/data/fixtures.ldif @@ -0,0 +1,14 @@ +dn: cn=Fabien Potencier,dc=symfony,dc=com +objectClass: inetOrgPerson +objectClass: organizationalPerson +objectClass: person +objectClass: top +cn: Fabien Potencier +sn: fabpot +mail: fabpot@symfony.com +mail: fabien@potencier.com +ou: People +ou: Maintainers +ou: Founder +givenName: Fabien Potencier +description: Founder and project lead @Symfony diff --git a/src/Symfony/Component/Ldap/Tests/LdapClientTest.php b/src/Symfony/Component/Ldap/Tests/LdapClientTest.php deleted file mode 100644 index acf15b06d62c3..0000000000000 --- a/src/Symfony/Component/Ldap/Tests/LdapClientTest.php +++ /dev/null @@ -1,28 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Component\Ldap\Tests; - -use Symfony\Component\Ldap\LdapClient; -use Symfony\Polyfill\Php56\Php56 as p; - -/** - * @requires extension ldap - */ -class LdapClientTest extends \PHPUnit_Framework_TestCase -{ - public function testLdapEscape() - { - $ldap = new LdapClient(); - - $this->assertEquals('\20foo\3dbar\0d(baz)*\20', $ldap->escape(" foo=bar\r(baz)* ", null, p::LDAP_ESCAPE_DN)); - } -} diff --git a/src/Symfony/Component/Ldap/Tests/LdapTest.php b/src/Symfony/Component/Ldap/Tests/LdapTest.php new file mode 100644 index 0000000000000..baa433c16ae0d --- /dev/null +++ b/src/Symfony/Component/Ldap/Tests/LdapTest.php @@ -0,0 +1,83 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Tests; + +use Symfony\Component\Ldap\Adapter\AdapterInterface; +use Symfony\Component\Ldap\Adapter\ConnectionInterface; +use Symfony\Component\Ldap\Exception\DriverNotFoundException; +use Symfony\Component\Ldap\Ldap; + +class LdapTest extends \PHPUnit_Framework_TestCase +{ + /** @var AdapterInterface */ + private $adapter; + + /** @var Ldap */ + private $ldap; + + protected function setUp() + { + $this->adapter = $this->getMock(AdapterInterface::class); + $this->ldap = new Ldap($this->adapter); + } + + public function testLdapBind() + { + $connection = $this->getMock(ConnectionInterface::class); + $connection + ->expects($this->once()) + ->method('bind') + ->with('foo', 'bar') + ; + $this->adapter + ->expects($this->once()) + ->method('getConnection') + ->will($this->returnValue($connection)) + ; + $this->ldap->bind('foo', 'bar'); + } + + public function testLdapEscape() + { + $this->adapter + ->expects($this->once()) + ->method('escape') + ->with('foo', 'bar', 'baz') + ; + $this->ldap->escape('foo', 'bar', 'baz'); + } + + public function testLdapQuery() + { + $this->adapter + ->expects($this->once()) + ->method('createQuery') + ->with('foo', 'bar', array('baz')) + ; + $this->ldap->query('foo', 'bar', array('baz')); + } + + /** + * @requires extension ldap + */ + public function testLdapCreate() + { + $ldap = Ldap::create('ext_ldap'); + $this->assertInstanceOf(Ldap::class, $ldap); + } + + public function testCreateWithInvalidAdapterName() + { + $this->setExpectedException(DriverNotFoundException::class); + Ldap::create('foo'); + } +} diff --git a/src/Symfony/Component/Ldap/composer.json b/src/Symfony/Component/Ldap/composer.json index 91d5ec881e5ab..df93ef7d932f2 100644 --- a/src/Symfony/Component/Ldap/composer.json +++ b/src/Symfony/Component/Ldap/composer.json @@ -18,6 +18,7 @@ "require": { "php": ">=5.5.9", "symfony/polyfill-php56": "~1.0", + "symfony/options-resolver": "~2.8|~3.0", "ext-ldap": "*" }, "autoload": { diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php index 7283ab9d507a7..950b603600ca9 100644 --- a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php +++ b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php @@ -17,7 +17,7 @@ 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\LdapInterface; use Symfony\Component\Ldap\Exception\ConnectionException; /** @@ -40,11 +40,11 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider * @param UserProviderInterface $userProvider A UserProvider * @param UserCheckerInterface $userChecker A UserChecker * @param string $providerKey The provider key - * @param LdapClientInterface $ldap An Ldap client + * @param LdapInterface $ldap A 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) + public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, $providerKey, LdapInterface $ldap, $dnString = '{username}', $hideUserNotFoundExceptions = true) { parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions); @@ -74,7 +74,7 @@ protected function checkAuthentication(UserInterface $user, UsernamePasswordToke $password = $token->getCredentials(); try { - $username = $this->ldap->escape($username, '', LDAP_ESCAPE_DN); + $username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_DN); $dn = str_replace('{username}', $username, $this->dnString); $this->ldap->bind($dn, $password); diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php index 844bceff019cb..4d2eead63a267 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php @@ -11,10 +11,13 @@ namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; +use Symfony\Component\Ldap\LdapInterface; 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; +use Symfony\Component\Security\Core\User\UserCheckerInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; /** * @requires extension ldap @@ -27,14 +30,14 @@ class LdapBindAuthenticationProviderTest extends \PHPUnit_Framework_TestCase */ public function testBindFailureShouldThrowAnException() { - $userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface'); - $ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface'); + $userProvider = $this->getMock(UserProviderInterface::class); + $ldap = $this->getMock(LdapInterface::class); $ldap ->expects($this->once()) ->method('bind') ->will($this->throwException(new ConnectionException())) ; - $userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface'); + $userChecker = $this->getMock(UserCheckerInterface::class); $provider = new LdapBindAuthenticationProvider($userProvider, $userChecker, 'key', $ldap); $reflection = new \ReflectionMethod($provider, 'checkAuthentication'); @@ -45,15 +48,15 @@ public function testBindFailureShouldThrowAnException() public function testRetrieveUser() { - $userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface'); + $userProvider = $this->getMock(UserProviderInterface::class); $userProvider ->expects($this->once()) ->method('loadUserByUsername') ->with('foo') ; - $ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface'); + $ldap = $this->getMock(LdapInterface::class); - $userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface'); + $userChecker = $this->getMock(UserCheckerInterface::class); $provider = new LdapBindAuthenticationProvider($userProvider, $userChecker, 'key', $ldap); $reflection = new \ReflectionMethod($provider, 'retrieveUser'); diff --git a/src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php b/src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php index 9b126e95180f3..6876eec8df997 100644 --- a/src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php +++ b/src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php @@ -11,6 +11,10 @@ namespace Symfony\Component\Security\Core\Tests\User; +use Symfony\Component\Ldap\Adapter\CollectionInterface; +use Symfony\Component\Ldap\Adapter\QueryInterface; +use Symfony\Component\Ldap\Entry; +use Symfony\Component\Ldap\LdapInterface; use Symfony\Component\Security\Core\User\LdapUserProvider; use Symfony\Component\Ldap\Exception\ConnectionException; @@ -24,7 +28,7 @@ class LdapUserProviderTest extends \PHPUnit_Framework_TestCase */ public function testLoadUserByUsernameFailsIfCantConnectToLdap() { - $ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface'); + $ldap = $this->getMock(LdapInterface::class); $ldap ->expects($this->once()) ->method('bind') @@ -40,12 +44,29 @@ public function testLoadUserByUsernameFailsIfCantConnectToLdap() */ public function testLoadUserByUsernameFailsIfNoLdapEntries() { - $ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface'); + $result = $this->getMock(CollectionInterface::class); + $query = $this->getMock(QueryInterface::class); + $query + ->expects($this->once()) + ->method('execute') + ->will($this->returnValue($result)) + ; + $result + ->expects($this->once()) + ->method('count') + ->will($this->returnValue(0)) + ; + $ldap = $this->getMock(LdapInterface::class); $ldap ->expects($this->once()) ->method('escape') ->will($this->returnValue('foo')) ; + $ldap + ->expects($this->once()) + ->method('query') + ->will($this->returnValue($query)) + ; $provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com'); $provider->loadUserByUsername('foo'); @@ -56,7 +77,19 @@ public function testLoadUserByUsernameFailsIfNoLdapEntries() */ public function testLoadUserByUsernameFailsIfMoreThanOneLdapEntry() { - $ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface'); + $result = $this->getMock(CollectionInterface::class); + $query = $this->getMock(QueryInterface::class); + $query + ->expects($this->once()) + ->method('execute') + ->will($this->returnValue($result)) + ; + $result + ->expects($this->once()) + ->method('count') + ->will($this->returnValue(2)) + ; + $ldap = $this->getMock(LdapInterface::class); $ldap ->expects($this->once()) ->method('escape') @@ -64,12 +97,8 @@ public function testLoadUserByUsernameFailsIfMoreThanOneLdapEntry() ; $ldap ->expects($this->once()) - ->method('find') - ->will($this->returnValue(array( - array(), - array(), - 'count' => 2, - ))) + ->method('query') + ->will($this->returnValue($query)) ; $provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com'); @@ -78,7 +107,29 @@ public function testLoadUserByUsernameFailsIfMoreThanOneLdapEntry() public function testSuccessfulLoadUserByUsername() { - $ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface'); + $result = $this->getMock(CollectionInterface::class); + $query = $this->getMock(QueryInterface::class); + $query + ->expects($this->once()) + ->method('execute') + ->will($this->returnValue($result)) + ; + $ldap = $this->getMock(LdapInterface::class); + $result + ->expects($this->once()) + ->method('offsetGet') + ->with(0) + ->will($this->returnValue(new Entry('foo', array( + 'sAMAccountName' => 'foo', + 'userpassword' => 'bar', + ) + ))) + ; + $result + ->expects($this->once()) + ->method('count') + ->will($this->returnValue(1)) + ; $ldap ->expects($this->once()) ->method('escape') @@ -86,14 +137,8 @@ public function testSuccessfulLoadUserByUsername() ; $ldap ->expects($this->once()) - ->method('find') - ->will($this->returnValue(array( - array( - 'sAMAccountName' => 'foo', - 'userpassword' => 'bar', - ), - 'count' => 1, - ))) + ->method('query') + ->will($this->returnValue($query)) ; $provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com'); diff --git a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php index 15935648abd30..a37981cc3f515 100644 --- a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php +++ b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php @@ -11,10 +11,11 @@ namespace Symfony\Component\Security\Core\User; +use Symfony\Component\Ldap\Entry; 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; +use Symfony\Component\Ldap\LdapInterface; /** * LdapUserProvider is a simple user provider on top of ldap. @@ -32,15 +33,15 @@ class LdapUserProvider implements UserProviderInterface private $defaultSearch; /** - * @param LdapClientInterface $ldap - * @param string $baseDn - * @param string $searchDn - * @param string $searchPassword - * @param array $defaultRoles - * @param string $uidKey - * @param string $filter + * @param LdapInterface $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})') + public function __construct(LdapInterface $ldap, $baseDn, $searchDn = null, $searchPassword = null, array $defaultRoles = array(), $uidKey = 'sAMAccountName', $filter = '({uid_key}={username})') { $this->ldap = $ldap; $this->baseDn = $baseDn; @@ -57,33 +58,25 @@ public function loadUserByUsername($username) { try { $this->ldap->bind($this->searchDn, $this->searchPassword); - $username = $this->ldap->escape($username, '', LDAP_ESCAPE_FILTER); + $username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_FILTER); $query = str_replace('{username}', $username, $this->defaultSearch); - $search = $this->ldap->find($this->baseDn, $query); + $search = $this->ldap->query($this->baseDn, $query); } catch (ConnectionException $e) { throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username), 0, $e); } - if (!$search) { + $entries = $search->execute(); + $count = count($entries); + + if (!$count) { throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username)); } - if ($search['count'] > 1) { + if ($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); + return $this->loadUser($username, $entries[0]); } /** @@ -105,4 +98,9 @@ public function supportsClass($class) { return $class === 'Symfony\Component\Security\Core\User\User'; } + + private function loadUser($username, Entry $entry) + { + return new User($username, $entry->getAttribute('userpassword'), $this->defaultRoles); + } } diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json index f6391e0fe2f05..e2915b03fd612 100644 --- a/src/Symfony/Component/Security/Core/composer.json +++ b/src/Symfony/Component/Security/Core/composer.json @@ -24,7 +24,7 @@ "symfony/event-dispatcher": "~2.8|~3.0", "symfony/expression-language": "~2.8|~3.0", "symfony/http-foundation": "~2.8|~3.0", - "symfony/ldap": "~2.8|~3.0.0", + "symfony/ldap": "~3.1", "symfony/validator": "~2.8|~3.0", "psr/log": "~1.0" }, diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json index 509bc28b127ec..7b3801f3d97dc 100644 --- a/src/Symfony/Component/Security/composer.json +++ b/src/Symfony/Component/Security/composer.json @@ -37,7 +37,7 @@ "symfony/routing": "~2.8|~3.0", "symfony/validator": "~2.8|~3.0", "symfony/expression-language": "~2.8|~3.0", - "symfony/ldap": "~2.8|~3.0.0", + "symfony/ldap": "~3.1", "psr/log": "~1.0" }, "suggest": { From 34d3c853cba3640c5cb8bce2570c1a3c9d7b44d5 Mon Sep 17 00:00:00 2001 From: Charles Sarrazin Date: Mon, 8 Feb 2016 19:00:45 +0100 Subject: [PATCH 2/2] Added compatibility layer for previous version of the Security component --- .../Component/Ldap/BaseLdapInterface.php | 48 +++++ src/Symfony/Component/Ldap/Ldap.php | 2 - src/Symfony/Component/Ldap/LdapClient.php | 85 +++++++++ .../Component/Ldap/LdapClientInterface.php | 36 ++++ src/Symfony/Component/Ldap/LdapInterface.php | 25 +-- .../Component/Ldap/Tests/LdapClientTest.php | 170 ++++++++++++++++++ src/Symfony/Component/Ldap/Tests/LdapTest.php | 2 +- 7 files changed, 341 insertions(+), 27 deletions(-) create mode 100644 src/Symfony/Component/Ldap/BaseLdapInterface.php 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/Tests/LdapClientTest.php diff --git a/src/Symfony/Component/Ldap/BaseLdapInterface.php b/src/Symfony/Component/Ldap/BaseLdapInterface.php new file mode 100644 index 0000000000000..ab247421eb5c7 --- /dev/null +++ b/src/Symfony/Component/Ldap/BaseLdapInterface.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap; + +use Symfony\Component\Ldap\Exception\ConnectionException; + +/** + * Base Ldap interface. + * + * This interface is here for reusability in the BC layer, + * and will be merged in LdapInterface in Symfony 4.0. + * + * @author Charles Sarrazin + * + * @internal + */ +interface BaseLdapInterface +{ + /** + * 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); + + /** + * 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/Ldap.php b/src/Symfony/Component/Ldap/Ldap.php index 76136ad77456a..82a443ca57f77 100644 --- a/src/Symfony/Component/Ldap/Ldap.php +++ b/src/Symfony/Component/Ldap/Ldap.php @@ -15,8 +15,6 @@ use Symfony\Component\Ldap\Exception\DriverNotFoundException; /** - * @author Grégoire Pineau - * @author Francis Besset * @author Charles Sarrazin */ final class Ldap implements LdapInterface diff --git a/src/Symfony/Component/Ldap/LdapClient.php b/src/Symfony/Component/Ldap/LdapClient.php new file mode 100644 index 0000000000000..3a41c0d58d0a0 --- /dev/null +++ b/src/Symfony/Component/Ldap/LdapClient.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap; + +/** + * @author Grégoire Pineau + * @author Francis Besset + * @author Charles Sarrazin + * + * @deprecated The LdapClient class will be removed in Symfony 4.0. You should use the Ldap class instead. + */ +final class LdapClient implements LdapClientInterface +{ + private $ldap; + + public function __construct($host = null, $port = 389, $version = 3, $useSsl = false, $useStartTls = false, $optReferrals = false, LdapInterface $ldap = null) + { + $config = array( + 'host' => $host, + 'port' => $port, + 'version' => $version, + 'useSsl' => (bool) $useSsl, + 'useStartTls' => (bool) $useStartTls, + 'optReferrals' => (bool) $optReferrals, + ); + + $this->ldap = null !== $ldap ? $ldap : Ldap::create('ext_ldap', $config); + } + + /** + * {@inheritdoc} + */ + public function bind($dn = null, $password = null) + { + $this->ldap->bind($dn, $password); + } + + /** + * {@inheritdoc} + */ + public function find($dn, $query, $filter = '*') + { + @trigger_error('The "find" method is deprecated since version 3.1 and will be removed in 4.0. Use the "query" method instead.', E_USER_DEPRECATED); + + $query = $this->ldap->query($dn, $query, array('filter' => $filter)); + $entries = $query->execute(); + $result = array(); + + foreach ($entries as $entry) { + $resultEntry = array(); + + foreach ($entry->getAttributes() as $attribute => $values) { + $resultAttribute = $values; + + $resultAttribute['count'] = count($values); + $resultEntry[] = $resultAttribute; + $resultEntry[$attribute] = $resultAttribute; + } + + $resultEntry['count'] = count($resultEntry) / 2; + $result[] = $resultEntry; + } + + $result['count'] = count($result); + + return $result; + } + + /** + * {@inheritdoc} + */ + public function escape($subject, $ignore = '', $flags = 0) + { + return $this->ldap->escape($subject, $ignore, $flags); + } +} diff --git a/src/Symfony/Component/Ldap/LdapClientInterface.php b/src/Symfony/Component/Ldap/LdapClientInterface.php new file mode 100644 index 0000000000000..2e5d61132d801 --- /dev/null +++ b/src/Symfony/Component/Ldap/LdapClientInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap; + +/** + * Ldap interface. + * + * This interface is used for the BC layer with branch 2.8 and 3.0. + * + * @author Grégoire Pineau + * @author Charles Sarrazin + * + * @deprecated You should use LdapInterface instead + */ +interface LdapClientInterface extends BaseLdapInterface +{ + /* + * Find a username into ldap connection. + * + * @param string $dn + * @param string $query + * @param mixed $filter + * + * @return array|null + */ + public function find($dn, $query, $filter = '*'); +} diff --git a/src/Symfony/Component/Ldap/LdapInterface.php b/src/Symfony/Component/Ldap/LdapInterface.php index f0e9a3ef6c651..8f89bd60dfd99 100644 --- a/src/Symfony/Component/Ldap/LdapInterface.php +++ b/src/Symfony/Component/Ldap/LdapInterface.php @@ -12,29 +12,17 @@ namespace Symfony\Component\Ldap; use Symfony\Component\Ldap\Adapter\QueryInterface; -use Symfony\Component\Ldap\Exception\ConnectionException; /** * Ldap interface. * - * @author Grégoire Pineau * @author Charles Sarrazin */ -interface LdapInterface +interface LdapInterface extends BaseLdapInterface { const ESCAPE_FILTER = 0x01; const 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); - /** * Queries a ldap server for entries matching the given criteria. * @@ -45,15 +33,4 @@ public function bind($dn = null, $password = null); * @return QueryInterface */ public function query($dn, $query, array $options = array()); - - /** - * 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/Tests/LdapClientTest.php b/src/Symfony/Component/Ldap/Tests/LdapClientTest.php new file mode 100644 index 0000000000000..fa85f68a66b25 --- /dev/null +++ b/src/Symfony/Component/Ldap/Tests/LdapClientTest.php @@ -0,0 +1,170 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Ldap\Tests; + +use Symfony\Component\Ldap\Adapter\CollectionInterface; +use Symfony\Component\Ldap\Adapter\QueryInterface; +use Symfony\Component\Ldap\Entry; +use Symfony\Component\Ldap\LdapClient; +use Symfony\Component\Ldap\LdapInterface; + +/** + * @group legacy + */ +class LdapClientTest extends \PHPUnit_Framework_TestCase +{ + /** @var LdapClient */ + private $client; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + private $ldap; + + protected function setUp() + { + $this->ldap = $this->getMock(LdapInterface::class); + + $this->client = new LdapClient(null, 389, 3, false, false, false, $this->ldap); + } + + public function testLdapBind() + { + $this->ldap + ->expects($this->once()) + ->method('bind') + ->with('foo', 'bar') + ; + $this->client->bind('foo', 'bar'); + } + + public function testLdapEscape() + { + $this->ldap + ->expects($this->once()) + ->method('escape') + ->with('foo', 'bar', 'baz') + ; + $this->client->escape('foo', 'bar', 'baz'); + } + + public function testLdapFind() + { + $collection = $this->getMock(CollectionInterface::class); + $collection + ->expects($this->once()) + ->method('getIterator') + ->will($this->returnValue(new \ArrayIterator(array( + new Entry('cn=qux,dc=foo,dc=com', array( + 'dn' => array('cn=qux,dc=foo,dc=com'), + 'cn' => array('qux'), + 'dc' => array('com', 'foo'), + 'givenName' => array('Qux'), + )), + new Entry('cn=baz,dc=foo,dc=com', array( + 'dn' => array('cn=baz,dc=foo,dc=com'), + 'cn' => array('baz'), + 'dc' => array('com', 'foo'), + 'givenName' => array('Baz'), + )), + )))) + ; + $query = $this->getMock(QueryInterface::class); + $query + ->expects($this->once()) + ->method('execute') + ->will($this->returnValue($collection)) + ; + $this->ldap + ->expects($this->once()) + ->method('query') + ->with('dc=foo,dc=com', 'bar', array('filter' => 'baz')) + ->willReturn($query) + ; + + $expected = array( + 'count' => 2, + 0 => array( + 'count' => 4, + 0 => array( + 'count' => 1, + 0 => 'cn=qux,dc=foo,dc=com', + ), + 'dn' => array( + 'count' => 1, + 0 => 'cn=qux,dc=foo,dc=com', + ), + 1 => array( + 'count' => 1, + 0 => 'qux', + ), + 'cn' => array( + 'count' => 1, + 0 => 'qux', + ), + 2 => array( + 'count' => 2, + 0 => 'com', + 1 => 'foo', + ), + 'dc' => array( + 'count' => 2, + 0 => 'com', + 1 => 'foo', + ), + 3 => array( + 'count' => 1, + 0 => 'Qux', + ), + 'givenName' => array( + 'count' => 1, + 0 => 'Qux', + ), + ), + 1 => array( + 'count' => 4, + 0 => array( + 'count' => 1, + 0 => 'cn=baz,dc=foo,dc=com', + ), + 'dn' => array( + 'count' => 1, + 0 => 'cn=baz,dc=foo,dc=com', + ), + 1 => array( + 'count' => 1, + 0 => 'baz', + ), + 'cn' => array( + 'count' => 1, + 0 => 'baz', + ), + 2 => array( + 'count' => 2, + 0 => 'com', + 1 => 'foo', + ), + 'dc' => array( + 'count' => 2, + 0 => 'com', + 1 => 'foo', + ), + 3 => array( + 'count' => 1, + 0 => 'Baz', + ), + 'givenName' => array( + 'count' => 1, + 0 => 'Baz', + ), + ), + ); + $this->assertEquals($expected, $this->client->find('dc=foo,dc=com', 'bar', 'baz')); + } +} diff --git a/src/Symfony/Component/Ldap/Tests/LdapTest.php b/src/Symfony/Component/Ldap/Tests/LdapTest.php index baa433c16ae0d..ddd294f3c2ad0 100644 --- a/src/Symfony/Component/Ldap/Tests/LdapTest.php +++ b/src/Symfony/Component/Ldap/Tests/LdapTest.php @@ -18,7 +18,7 @@ class LdapTest extends \PHPUnit_Framework_TestCase { - /** @var AdapterInterface */ + /** @var \PHPUnit_Framework_MockObject_MockObject */ private $adapter; /** @var Ldap */ 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