Initial commit of LDAP plugin

This commit is contained in:
Andy Miller
2018-05-07 05:40:25 -06:00
parent cef5268e09
commit d4d885a874
90 changed files with 7961 additions and 1 deletions

View File

@@ -0,0 +1,87 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Adapter\AdapterInterface;
use Symfony\Component\Ldap\Exception\LdapException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Adapter implements AdapterInterface
{
private $config;
private $connection;
private $entryManager;
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 getEntryManager()
{
if (null === $this->entryManager) {
$this->entryManager = new EntryManager($this->getConnection());
}
return $this->entryManager;
}
/**
* {@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;
}
}

View File

@@ -0,0 +1,134 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Adapter\CollectionInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Exception\LdapException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Collection implements CollectionInterface
{
private $connection;
private $search;
private $entries;
public function __construct(Connection $connection, Query $search)
{
$this->connection = $connection;
$this->search = $search;
}
/**
* {@inheritdoc}
*/
public function toArray()
{
if (null === $this->entries) {
$this->entries = iterator_to_array($this->getIterator(), false);
}
return $this->entries;
}
public function count()
{
if (false !== $count = ldap_count_entries($this->connection->getResource(), $this->search->getResource())) {
return $count;
}
throw new LdapException(sprintf('Error while retrieving entry count: %s.', ldap_error($this->connection->getResource())));
}
public function getIterator()
{
$con = $this->connection->getResource();
$search = $this->search->getResource();
$current = ldap_first_entry($con, $search);
if (0 === $this->count()) {
return;
}
if (false === $current) {
throw new LdapException(sprintf('Could not rewind entries array: %s.', ldap_error($con)));
}
yield $this->getSingleEntry($con, $current);
while (false !== $current = ldap_next_entry($con, $current)) {
yield $this->getSingleEntry($con, $current);
}
}
public function offsetExists($offset)
{
$this->toArray();
return isset($this->entries[$offset]);
}
public function offsetGet($offset)
{
$this->toArray();
return isset($this->entries[$offset]) ? $this->entries[$offset] : null;
}
public function offsetSet($offset, $value)
{
$this->toArray();
$this->entries[$offset] = $value;
}
public function offsetUnset($offset)
{
$this->toArray();
unset($this->entries[$offset]);
}
private function getSingleEntry($con, $current)
{
$attributes = ldap_get_attributes($con, $current);
if (false === $attributes) {
throw new LdapException(sprintf('Could not fetch attributes: %s.', ldap_error($con)));
}
$attributes = $this->cleanupAttributes($attributes);
$dn = ldap_get_dn($con, $current);
if (false === $dn) {
throw new LdapException(sprintf('Could not fetch DN: %s.', ldap_error($con)));
}
return new Entry($dn, $attributes);
}
private function cleanupAttributes(array $entry)
{
$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 $attributes;
}
}

View File

@@ -0,0 +1,154 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Adapter\AbstractConnection;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Ldap\Exception\LdapException;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* @author Charles Sarrazin <charles@sarraz.in>
*/
class Connection extends AbstractConnection
{
/** @var bool */
private $bound = false;
/** @var resource */
private $connection;
public function __destruct()
{
$this->disconnect();
}
/**
* {@inheritdoc}
*/
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
*
* @internal
*/
public function getResource()
{
return $this->connection;
}
public function setOption($name, $value)
{
if (!@ldap_set_option($this->connection, ConnectionOptions::getOption($name), $value)) {
throw new LdapException(sprintf('Could not set value "%s" for option "%s".', $value, $name));
}
}
public function getOption($name)
{
if (!@ldap_get_option($this->connection, ConnectionOptions::getOption($name), $ret)) {
throw new LdapException(sprintf('Could not retrieve value for option "%s".', $name));
}
return $ret;
}
protected function configureOptions(OptionsResolver $resolver)
{
parent::configureOptions($resolver);
$resolver->setDefault('debug', false);
$resolver->setAllowedTypes('debug', 'bool');
$resolver->setDefault('referrals', false);
$resolver->setAllowedTypes('referrals', 'bool');
$resolver->setNormalizer('options', function (Options $options, $value) {
if (true === $options['debug']) {
$value['debug_level'] = 7;
}
if (!isset($value['protocol_version'])) {
$value['protocol_version'] = $options['version'];
}
if (!isset($value['referrals'])) {
$value['referrals'] = $options['referrals'];
}
return $value;
});
$resolver->setAllowedValues('options', function (array $values) {
foreach ($values as $name => $value) {
if (!ConnectionOptions::isOption($name)) {
return false;
}
}
return true;
});
}
private function connect()
{
if ($this->connection) {
return;
}
$this->connection = ldap_connect($this->config['connection_string']);
foreach ($this->config['options'] as $name => $value) {
$this->setOption($name, $value);
}
if (false === $this->connection) {
throw new LdapException(sprintf('Could not connect to Ldap server: %s.', ldap_error($this->connection)));
}
if ('tls' === $this->config['encryption'] && false === ldap_start_tls($this->connection)) {
throw new LdapException(sprintf('Could not initiate TLS connection: %s.', ldap_error($this->connection)));
}
}
private function disconnect()
{
if ($this->connection && is_resource($this->connection)) {
ldap_close($this->connection);
}
$this->connection = null;
$this->bound = false;
}
}

View File

@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Exception\LdapException;
/**
* A class representing the Ldap extension's options, which can be used with
* ldap_set_option or ldap_get_option.
*
* @author Charles Sarrazin <charles@sarraz.in>
*
* @internal
*/
final class ConnectionOptions
{
const API_INFO = 0x00;
const DEREF = 0x02;
const SIZELIMIT = 0x03;
const TIMELIMIT = 0x04;
const REFERRALS = 0x08;
const RESTART = 0x09;
const PROTOCOL_VERSION = 0x11;
const SERVER_CONTROLS = 0x12;
const CLIENT_CONTROLS = 0x13;
const API_FEATURE_INFO = 0x15;
const HOST_NAME = 0x30;
const ERROR_NUMBER = 0x31;
const ERROR_STRING = 0x32;
const MATCHED_DN = 0x33;
const DEBUG_LEVEL = 0x5001;
const NETWORK_TIMEOUT = 0x5005;
const X_SASL_MECH = 0x6100;
const X_SASL_REALM = 0x6101;
const X_SASL_AUTHCID = 0x6102;
const X_SASL_AUTHZID = 0x6103;
public static function getOptionName($name)
{
return sprintf('%s::%s', self::class, strtoupper($name));
}
/**
* Fetches an option's corresponding constant value from an option name.
* The option name can either be in snake or camel case.
*
* @param string $name
*
* @return int
*
* @throws LdapException
*/
public static function getOption($name)
{
// Convert
$constantName = self::getOptionName($name);
if (!defined($constantName)) {
throw new LdapException(sprintf('Unknown option "%s".', $name));
}
return constant($constantName);
}
public static function isOption($name)
{
return defined(self::getOptionName($name));
}
}

View File

@@ -0,0 +1,95 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Adapter\EntryManagerInterface;
use Symfony\Component\Ldap\Adapter\RenameEntryInterface;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\Exception\LdapException;
use Symfony\Component\Ldap\Exception\NotBoundException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
* @author Bob van de Vijver <bobvandevijver@hotmail.com>
*/
class EntryManager implements EntryManagerInterface, RenameEntryInterface
{
private $connection;
public function __construct(Connection $connection)
{
$this->connection = $connection;
}
/**
* {@inheritdoc}
*/
public function add(Entry $entry)
{
$con = $this->getConnectionResource();
if (!@ldap_add($con, $entry->getDn(), $entry->getAttributes())) {
throw new LdapException(sprintf('Could not add entry "%s": %s.', $entry->getDn(), ldap_error($con)));
}
return $this;
}
/**
* {@inheritdoc}
*/
public function update(Entry $entry)
{
$con = $this->getConnectionResource();
if (!@ldap_modify($con, $entry->getDn(), $entry->getAttributes())) {
throw new LdapException(sprintf('Could not update entry "%s": %s.', $entry->getDn(), ldap_error($con)));
}
}
/**
* {@inheritdoc}
*/
public function remove(Entry $entry)
{
$con = $this->getConnectionResource();
if (!@ldap_delete($con, $entry->getDn())) {
throw new LdapException(sprintf('Could not remove entry "%s": %s.', $entry->getDn(), ldap_error($con)));
}
}
/**
* {@inheritdoc}
*/
public function rename(Entry $entry, $newRdn, $removeOldRdn = true)
{
$con = $this->getConnectionResource();
if (!@ldap_rename($con, $entry->getDn(), $newRdn, null, $removeOldRdn)) {
throw new LdapException(sprintf('Could not rename entry "%s" to "%s": %s.', $entry->getDn(), $newRdn, ldap_error($con)));
}
}
/**
* Get the connection resource, but first check if the connection is bound.
*/
private function getConnectionResource()
{
// If the connection is not bound, throw an exception. Users should use an explicit bind call first.
if (!$this->connection->isBound()) {
throw new NotBoundException('Query execution is not possible without binding the connection first.');
}
return $this->connection->getResource();
}
}

View File

@@ -0,0 +1,109 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Ldap\Adapter\ExtLdap;
use Symfony\Component\Ldap\Adapter\AbstractQuery;
use Symfony\Component\Ldap\Exception\LdapException;
use Symfony\Component\Ldap\Exception\NotBoundException;
/**
* @author Charles Sarrazin <charles@sarraz.in>
* @author Bob van de Vijver <bobvandevijver@hotmail.com>
*/
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);
}
public function __destruct()
{
$con = $this->connection->getResource();
$this->connection = null;
if (null === $this->search || false === $this->search) {
return;
}
$success = ldap_free_result($this->search);
$this->search = null;
if (!$success) {
throw new LdapException(sprintf('Could not free results: %s.', ldap_error($con)));
}
}
/**
* {@inheritdoc}
*/
public function execute()
{
if (null === $this->search) {
// If the connection is not bound, throw an exception. Users should use an explicit bind call first.
if (!$this->connection->isBound()) {
throw new NotBoundException('Query execution is not possible without binding the connection first.');
}
$con = $this->connection->getResource();
switch ($this->options['scope']) {
case static::SCOPE_BASE:
$func = 'ldap_read';
break;
case static::SCOPE_ONE:
$func = 'ldap_list';
break;
case static::SCOPE_SUB:
$func = 'ldap_search';
break;
default:
throw new LdapException(sprintf('Could not search in scope "%s".', $this->options['scope']));
}
$this->search = @$func(
$con,
$this->dn,
$this->query,
$this->options['filter'],
$this->options['attrsOnly'],
$this->options['maxItems'],
$this->options['timeout'],
$this->options['deref']
);
}
if (false === $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
*
* @internal
*/
public function getResource()
{
return $this->search;
}
}