diff --git a/.travis.yml b/.travis.yml index 0c8fd6a2e4cd..71d285fc84e3 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 6456c27916d1..9386cf7e3a24 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 000000000000..2f012a66639a --- /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 000000000000..41889da1333d --- /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 000000000000..1e2f72d5bddf --- /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 000000000000..2db4d2bd4a29 --- /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 000000000000..347a852a82ea --- /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 000000000000..4bf0303df057 --- /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 000000000000..e56c0d916ed1 --- /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 000000000000..103c4d343a06 --- /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 000000000000..bbff589f76f1 --- /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 000000000000..997e6b48cc0c --- /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 000000000000..db100a068833 --- /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/BaseLdapInterface.php b/src/Symfony/Component/Ldap/BaseLdapInterface.php new file mode 100644 index 000000000000..ab247421eb5c --- /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/Entry.php b/src/Symfony/Component/Ldap/Entry.php new file mode 100644 index 000000000000..f4da743742b5 --- /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 d5023c5d02d7..cded4cf2a389 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 000000000000..40258435bb6a --- /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 000000000000..b861a3fe8d3c --- /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 ef3bd929bb85..4045f32cf44b 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 000000000000..82a443ca57f7 --- /dev/null +++ b/src/Symfony/Component/Ldap/Ldap.php @@ -0,0 +1,79 @@ + + * + * 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 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 index e7a0bc45e64d..3a41c0d58d0a 100644 --- a/src/Symfony/Component/Ldap/LdapClient.php +++ b/src/Symfony/Component/Ldap/LdapClient.php @@ -11,53 +11,29 @@ 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 + * @deprecated The LdapClient class will be removed in Symfony 4.0. You should use the Ldap class instead. */ -class LdapClient implements LdapClientInterface +final 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; - } + private $ldap; - public function __destruct() + public function __construct($host = null, $port = 389, $version = 3, $useSsl = false, $useStartTls = false, $optReferrals = false, LdapInterface $ldap = null) { - $this->disconnect(); + $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); } /** @@ -65,13 +41,7 @@ public function __destruct() */ 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->ldap->bind($dn, $password); } /** @@ -79,67 +49,37 @@ public function bind($dn = null, $password = null) */ public function find($dn, $query, $filter = '*') { - if (!is_array($filter)) { - $filter = array($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); - $search = ldap_search($this->connection, $dn, $query, $filter); - $infos = ldap_get_entries($this->connection, $search); + $query = $this->ldap->query($dn, $query, array('filter' => $filter)); + $entries = $query->execute(); + $result = array(); - if (0 === $infos['count']) { - return; - } + foreach ($entries as $entry) { + $resultEntry = array(); - return $infos; - } + foreach ($entry->getAttributes() as $attribute => $values) { + $resultAttribute = $values; - /** - * {@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; + $resultAttribute['count'] = count($values); + $resultEntry[] = $resultAttribute; + $resultEntry[$attribute] = $resultAttribute; } - $this->connection = ldap_connect($host, $this->port); + $resultEntry['count'] = count($resultEntry) / 2; + $result[] = $resultEntry; + } - ldap_set_option($this->connection, LDAP_OPT_PROTOCOL_VERSION, $this->version); - ldap_set_option($this->connection, LDAP_OPT_REFERRALS, $this->optReferrals); + $result['count'] = count($result); - if ($this->useStartTls) { - ldap_start_tls($this->connection); - } - } + return $result; } - private function disconnect() + /** + * {@inheritdoc} + */ + public function escape($subject, $ignore = '', $flags = 0) { - if ($this->connection && is_resource($this->connection)) { - ldap_unbind($this->connection); - } - - $this->connection = null; + return $this->ldap->escape($subject, $ignore, $flags); } } diff --git a/src/Symfony/Component/Ldap/LdapClientInterface.php b/src/Symfony/Component/Ldap/LdapClientInterface.php index dcdc0818da10..2e5d61132d80 100644 --- a/src/Symfony/Component/Ldap/LdapClientInterface.php +++ b/src/Symfony/Component/Ldap/LdapClientInterface.php @@ -11,28 +11,18 @@ namespace Symfony\Component\Ldap; -use Symfony\Component\Ldap\Exception\ConnectionException; - /** * Ldap interface. * + * This interface is used for the BC layer with branch 2.8 and 3.0. + * * @author Grégoire Pineau * @author Charles Sarrazin * - * @internal + * @deprecated You should use LdapInterface instead */ -interface LdapClientInterface +interface LdapClientInterface extends 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); - /* * Find a username into ldap connection. * @@ -43,15 +33,4 @@ public function bind($dn = null, $password = null); * @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/LdapInterface.php b/src/Symfony/Component/Ldap/LdapInterface.php new file mode 100644 index 000000000000..8f89bd60dfd9 --- /dev/null +++ b/src/Symfony/Component/Ldap/LdapInterface.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; + +use Symfony\Component\Ldap\Adapter\QueryInterface; + +/** + * Ldap interface. + * + * @author Charles Sarrazin + */ +interface LdapInterface extends BaseLdapInterface +{ + const ESCAPE_FILTER = 0x01; + const ESCAPE_DN = 0x02; + + /** + * Queries a ldap server for entries matching the given criteria. + * + * @param string $dn + * @param string $query + * @param array $options + * + * @return QueryInterface + */ + public function query($dn, $query, array $options = array()); +} diff --git a/src/Symfony/Component/Ldap/README.md b/src/Symfony/Component/Ldap/README.md index 6de60f10bbb9..28ac472da608 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 000000000000..6d478d691186 --- /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 000000000000..35f7fe5652b3 --- /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 000000000000..25abb296c9a6 --- /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 000000000000..21dc42d77edd --- /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 index acf15b06d62c..fa85f68a66b2 100644 --- a/src/Symfony/Component/Ldap/Tests/LdapClientTest.php +++ b/src/Symfony/Component/Ldap/Tests/LdapClientTest.php @@ -11,18 +11,160 @@ 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\Polyfill\Php56\Php56 as p; +use Symfony\Component\Ldap\LdapInterface; /** - * @requires extension ldap + * @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() { - $ldap = new LdapClient(); + $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) + ; - $this->assertEquals('\20foo\3dbar\0d(baz)*\20', $ldap->escape(" foo=bar\r(baz)* ", null, p::LDAP_ESCAPE_DN)); + $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 new file mode 100644 index 000000000000..ddd294f3c2ad --- /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 \PHPUnit_Framework_MockObject_MockObject */ + 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 91d5ec881e5a..df93ef7d932f 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 7283ab9d507a..950b603600ca 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 844bceff019c..4d2eead63a26 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 9b126e95180f..6876eec8df99 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 15935648abd3..a37981cc3f51 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 f6391e0fe2f0..e2915b03fd61 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 509bc28b127e..7b3801f3d97d 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": { 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