581 lines
20 KiB
PHP
581 lines
20 KiB
PHP
<?php
|
|
namespace EesyLDAP;
|
|
|
|
/**
|
|
* @property-read array<string> $hosts
|
|
* @property-read array<string> $host
|
|
* @property-read int $port
|
|
* @property-read int $version
|
|
* @property-read bool $starttls
|
|
* @property-read string|null $bind_dn
|
|
* @property-read string|null $binddn
|
|
* @property-read string|null $dn
|
|
* @property-read string|null $bind_password
|
|
* @property-read string|null $bindpw
|
|
* @property-read string|null $password
|
|
* @property-read string|null $pwd
|
|
* @property-read string|null $basedn
|
|
* @property-read array $options
|
|
* @property-read array $required_options
|
|
* @property-read string $filter
|
|
* @property-read string $scope
|
|
* @property-read bool $raise_on_error
|
|
* @property-read bool $use_schema
|
|
* @property-read array<string,string> $config_aliases
|
|
* @property-read array<string,mixed> $default_config
|
|
* @property-read Schema|false $schema
|
|
*/
|
|
class Ldap {
|
|
|
|
/**
|
|
* Configuration
|
|
* @var array<string,mixed>
|
|
*/
|
|
protected $config = array();
|
|
|
|
/**
|
|
* Default configuration
|
|
* @var array<string,mixed>
|
|
*/
|
|
protected static $default_config = array(
|
|
// LDAP server name to connect to. You can provide several hosts in an array in which case the
|
|
// hosts are tried from left to right.
|
|
'hosts' => array('localhost'),
|
|
// Port on the server
|
|
'port' => 389,
|
|
// LDAP protocol version
|
|
'version' => 3,
|
|
// TLS is started after connecting
|
|
'starttls' => false,
|
|
// The distinguished name to bind as (username). If you don't supply this, an anonymous bind
|
|
// will be established.
|
|
'bind_dn' => null,
|
|
// Password for the binddn. If the credentials are wrong, the bind will fail server-side and an
|
|
// anonymous bind will be established instead. An empty bindpw string requests an
|
|
// unauthenticated bind. This can cause security problems in your application, if you rely on a
|
|
// bind to make security decisions (see RFC-4513, section 6.3.1).
|
|
'bind_password' => null,
|
|
// LDAP base name (root directory)
|
|
'basedn' => null,
|
|
// Array of additional ldap options as key-value pairs
|
|
'options' => array(),
|
|
// Required options: consider LDAP link as bad if the required option could not be set.
|
|
// Possibles values:
|
|
// - array of expected options
|
|
// - True to consider all options as required (default)
|
|
// - False to consider all options as optional
|
|
'required_options' => true,
|
|
// Default search filter (string or preferably \EesyLDAP\Filter object).
|
|
'filter' => '(objectClass=*)',
|
|
// Default search scope
|
|
'scope' => 'sub',
|
|
// Raise LdapException on error
|
|
'raise_on_error' => true,
|
|
// Use schema
|
|
'use_schema' => false,
|
|
);
|
|
|
|
/**
|
|
* Configuration parameter aliases
|
|
* @var array<string,string>
|
|
*/
|
|
protected static $config_aliases = array(
|
|
'host' => 'hosts',
|
|
'binddn' => 'bind_dn',
|
|
'dn' => 'bind_dn',
|
|
'bindpw' => 'bind_password',
|
|
'password' => 'bind_password',
|
|
'pwd' => 'bind_password',
|
|
);
|
|
|
|
/**
|
|
* The LDAP connection
|
|
* @var Link|false
|
|
*/
|
|
protected $_link = false;
|
|
|
|
/**
|
|
* The Schema object
|
|
* @var Schema|false
|
|
*/
|
|
protected $schema = false;
|
|
|
|
/**
|
|
* Scopes associated with their search function
|
|
* @var array<string,string>
|
|
*/
|
|
protected static $search_function_by_scope = array(
|
|
'one' => 'list',
|
|
'base' => 'read',
|
|
'sub' => 'search',
|
|
);
|
|
|
|
/**
|
|
* Constructor
|
|
* @param array<string,mixed>|null $config Configuration (see $default_config for expected content)
|
|
* @param bool $connect If true, start LDAP connection (optional, default=true)
|
|
* @return void
|
|
*/
|
|
public function __construct($config=null, $connect=true) {
|
|
if (is_array($config))
|
|
$this -> set_config($config);
|
|
if ($connect)
|
|
$this -> connect();
|
|
}
|
|
|
|
/**
|
|
* Set configuration
|
|
* @param array<string,mixed> $config
|
|
* @return bool
|
|
*/
|
|
public function set_config($config) {
|
|
if (!is_array($config))
|
|
return $this->error('Expect $config to be an array');
|
|
|
|
foreach($config as $key => $value) {
|
|
if (array_key_exists($key, self :: $config_aliases))
|
|
$key = self :: $config_aliases[$key];
|
|
if (!array_key_exists($key, self :: $default_config))
|
|
return $this -> error("Invalid configuration parameter '$key'");
|
|
switch ($key) {
|
|
case 'hosts':
|
|
if (is_string($value)) {
|
|
$hosts = array();
|
|
foreach(explode(',', $value) as $host) {
|
|
$host = trim($host);
|
|
if (!$host)
|
|
return $this -> error("Empty host found in configuration!");
|
|
$hosts[] = $host;
|
|
}
|
|
$value = $hosts;
|
|
}
|
|
else if (!is_array($value)) {
|
|
$this -> error(
|
|
"Invalid hosts found in configuration: expect to be an array of LDAP host URI (or ".
|
|
"hostname) or a comma separated list of LDAP host URI (or hostname)."
|
|
);
|
|
continue 2;
|
|
}
|
|
break;
|
|
case 'port':
|
|
case 'version':
|
|
$value = intval($value);
|
|
break;
|
|
case 'starttls':
|
|
$value = boolval($value);
|
|
break;
|
|
case 'options':
|
|
if (!is_array($value)) {
|
|
$this -> error(
|
|
"Invalid options found in configuration: expect to be an array."
|
|
);
|
|
continue 2;
|
|
}
|
|
break;
|
|
case 'required_options':
|
|
if (!is_array($value) && !is_bool($value)) {
|
|
$this -> error(
|
|
"Invalid required_options found in configuration: expect to be an array or a boolean."
|
|
);
|
|
continue 2;
|
|
}
|
|
break;
|
|
case 'filter':
|
|
if (is_string($value))
|
|
$value = Filter::parse($value);
|
|
elseif (!$value instanceof Filter) {
|
|
$this -> error(
|
|
"Invalid filter found in configuration: expect to be a string or a \EesyLDAP\Filter ".
|
|
"object."
|
|
);
|
|
continue 2;
|
|
}
|
|
break;
|
|
case 'scope':
|
|
if (!is_string($value) || !array_key_exists($value, self :: $search_function_by_scope)){
|
|
$this -> error(
|
|
"Invalid scope '%s' found in configuration: expect to be one of the following: %s",
|
|
null,
|
|
is_string($value)?$value:gettype($value),
|
|
implode(', ', array_keys(self :: $search_function_by_scope))
|
|
);
|
|
continue 2;
|
|
}
|
|
}
|
|
$this -> config[$key] = $value;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if phps ldap-extension is loaded
|
|
*
|
|
* Note: If it is not loaded, it tries to load it manually using PHPs dl().
|
|
* It knows both windows-dll and *nix-so.
|
|
*
|
|
* @param bool|null $raise See error() (optional, default: null)
|
|
* @return bool
|
|
*/
|
|
public function check_ldap_extension($raise=null) {
|
|
if (!extension_loaded('ldap') && !@dl('ldap.' . PHP_SHLIB_SUFFIX)) {
|
|
return $this -> error(
|
|
"It seems that you do not have the ldap-extension installed. Please install it before ".
|
|
"using the EesyLDAP.",
|
|
$raise
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Start connection on configured LDAP hosts
|
|
* @param bool|null $raise See error() (optional, default: null)
|
|
* @param string|null $bind_dn Bind DN (optional, default: null = use configuration)
|
|
* @param string|null $bind_password Bind password (optional, default: null = use configuration)
|
|
* @return bool
|
|
* @throws LdapException
|
|
*/
|
|
public function connect($raise=null, $bind_dn=null, $bind_password=null) {
|
|
if ($this -> _link)
|
|
return true;
|
|
if (!$this -> check_ldap_extension($raise))
|
|
return false;
|
|
|
|
foreach($this -> hosts as $host) {
|
|
$this->_link = new Link();
|
|
if ($this->_link->connect($host, $this->port) === false) {
|
|
$this->_link = false;
|
|
$this -> error("Fail to connect on $host", false);
|
|
continue;
|
|
}
|
|
|
|
if ($this->starttls) {
|
|
if ($this->_link->start_tls() === false) {
|
|
$this -> close();
|
|
$this -> error("Fail to start TLS on $host", false);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if ($this->version && !$this->set_option("LDAP_OPT_PROTOCOL_VERSION", $this->version, false)) {
|
|
$this -> close();
|
|
$this -> error(
|
|
"Fail to switch to LDAP protocol version %d on %s", false, $this->version, $host);
|
|
continue;
|
|
}
|
|
|
|
if (!$this -> bind($bind_dn, $bind_password, false)) {
|
|
$this -> close();
|
|
$this -> error(
|
|
"Fail to bind on %s", false);
|
|
continue;
|
|
}
|
|
|
|
// Set LDAP parameters, now we know we have a valid connection.
|
|
foreach ($this->options as $option => $value) {
|
|
if (!$this -> set_option($option, $value, false)) {
|
|
if (in_array($option, $this -> required_options)) {
|
|
$this -> close();
|
|
$this -> error('Fail to set option %s on %s', false, $option, $host);
|
|
continue 2;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return $this -> error('Fail to connect on configured host(s)', $raise);
|
|
}
|
|
|
|
/**
|
|
* Bind on LDAP link
|
|
* Note: if not already connected, start connection with the specified credentials.
|
|
* @param string|null $dn Bind DN (optional, default: null = use configuration)
|
|
* @param string|null $password Bind password (optional, default: null = use configuration)
|
|
* @param bool|null $raise See error() (optional, default: null)
|
|
* @return bool
|
|
*/
|
|
public function bind($dn=null, $password=null, $raise=null) {
|
|
if (is_null($dn)) $dn = $this -> bind_dn;
|
|
if (is_null($password)) $password = $this -> bind_password;
|
|
if (!$this -> _link) return $this -> connect($raise, $dn, $password);
|
|
if ($this->_link->bind($dn, $password) === false)
|
|
return $this -> log_error($dn?"Fail to bind as $dn":"Fail to anonymously bind", $raise);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Set an LDAP option
|
|
*
|
|
* @param string $option Option to set
|
|
* @param mixed $value Value to set Option to
|
|
* @param bool|null $raise See error() (optional, default: null)
|
|
*
|
|
* @access public
|
|
* @return bool
|
|
* @throws LdapException
|
|
*/
|
|
public function set_option($option, $value, $raise=null) {
|
|
if (!$this->_link)
|
|
return $this->error("Could not set LDAP option: No LDAP connection", $raise);
|
|
if (!is_string($option) || !defined($option) || strpos($option, 'LDAP_') !== 0)
|
|
return $this->error("Unkown Option requested", $raise);
|
|
// @phpstan-ignore-next-line
|
|
if ($this->_link->set_option(constant($option), $value))
|
|
return true;
|
|
return $this -> log_error("Fail to set option $option", $raise);
|
|
}
|
|
|
|
/**
|
|
* Close LDAP link (if established)
|
|
* @return void
|
|
*/
|
|
public function close() {
|
|
if ($this->_link)
|
|
$this->_link->close();
|
|
}
|
|
|
|
/**
|
|
* Log the last error occured on LDAP link
|
|
* @param string $prefix
|
|
* @param bool|null $raise See error() (optional, default: null)
|
|
* @param array<mixed> $extra_args If passed, will be used to compute the prefix message using sprintf
|
|
* @return false
|
|
*/
|
|
protected function log_error($prefix, $raise=null, ...$extra_args) {
|
|
if ($extra_args)
|
|
$prefix = call_user_func_array('sprintf', array_merge(array($prefix), $extra_args));
|
|
$errno = $this->_link?$this->_link->errno():false;
|
|
if (!$errno)
|
|
return $this -> error("%s: unkown error", $raise, $prefix);
|
|
$err = $this->_link?$this->_link->err2str($errno):false;
|
|
if (!$err)
|
|
return $this -> error("%s: error #%s", $raise, $prefix, $errno);
|
|
return $this -> error("%s: %s (#%s)", $raise, $prefix, $err, $errno);
|
|
}
|
|
|
|
/**
|
|
* Log and eventually raise an error
|
|
* @param string $error The error message
|
|
* @param bool|null $raise If true, raise an LdapException, otherwise only log it and return false
|
|
* @param array<mixed> $extra_args If passed, will be used to compute the error message using sprintf
|
|
* @return false
|
|
* @throws LdapException
|
|
*/
|
|
public function error($error, $raise=null, ...$extra_args) {
|
|
if ($extra_args)
|
|
$error = call_user_func_array('sprintf', array_merge(array($error), $extra_args));
|
|
// Note: sprintf always return string
|
|
if (is_null($raise))
|
|
$raise = $this -> raise_on_error;
|
|
if ($raise)
|
|
throw new LdapException($error); // @phpstan-ignore-line
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get property
|
|
* @param string $key
|
|
* @return mixed
|
|
* @throws LdapException
|
|
*/
|
|
public function __get($key) {
|
|
if (
|
|
array_key_exists($key, self :: $default_config)
|
|
|| array_key_exists($key, self :: $config_aliases)
|
|
)
|
|
return $this -> get_config($key);
|
|
switch ($key) {
|
|
case 'config_aliases':
|
|
return self :: $config_aliases;
|
|
case 'default_config':
|
|
return self :: $default_config;
|
|
case 'schema':
|
|
if (!$this->use_schema)
|
|
return false;
|
|
if (!$this->schema)
|
|
$this->schema = Schema :: load($this);
|
|
return $this->schema && $this->schema->loaded?$this->schema:false;
|
|
}
|
|
return $this -> error("Invalid property '$key' requested");
|
|
}
|
|
|
|
/**
|
|
* Get configuration parameter
|
|
* @param string $key
|
|
* @return mixed
|
|
* @throws LdapException
|
|
*/
|
|
public function get_config($key) {
|
|
if (array_key_exists($key, self :: $config_aliases))
|
|
$key = self :: $config_aliases[$key];
|
|
|
|
$value = null;
|
|
if (array_key_exists($key, $this -> config))
|
|
$value = $this -> config[$key];
|
|
elseif (array_key_exists($key, self :: $default_config))
|
|
$value = self :: $default_config[$key];
|
|
else
|
|
return $this -> error("Invalid configuration parameter '$key'");
|
|
switch ($key) {
|
|
case 'required_options':
|
|
if (is_array($value))
|
|
return $value;
|
|
if ($value === false)
|
|
return array();
|
|
return array_keys($this->options);
|
|
case 'filter':
|
|
if (is_string($value))
|
|
return Filter :: parse($value);
|
|
return $value;
|
|
}
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Search on LDAP directory
|
|
*
|
|
* Optional expected parameters are:
|
|
* - scope: The scope which will be used for searching.
|
|
* Possible values:
|
|
* - base: Just one entry (the specified base DN, see $base)
|
|
* - sub: The whole subtree
|
|
* - one: Immediately below the specified base DN (see $base)
|
|
* - sizelimit: Limit the number of entries returned (default: 0 = unlimited),
|
|
* - timelimit: Limit the time spent for searching in seconds (default: 0 = unlimited),
|
|
* - attrsonly: If true, the search will only return the attribute names,
|
|
* - attributes: Array of attribute names, which the entries in the expected result should contain.
|
|
* It is good practice to limit this to just the ones you need.
|
|
*
|
|
* @param string|null $filter Filter of the search (optional, default: null == use config)
|
|
* @param string|Entry|null $base Base DN of the search (optional, default: null == use config)
|
|
* @param string|null $scope Scope of the search (optional, default: null == use params or config)
|
|
* @param array<string>|null $attributes Expected attributes returned by the search (optional,
|
|
* default: null == use params or config)
|
|
* @param array<string,mixed>|null $params Other optional parameters of the search (optional, default: null == use config)
|
|
* @param bool|null $raise See error() (optional, default: null)
|
|
* @return array<string,Entry>|false Array of Entry object with DN as key, or False in case of error
|
|
* @throws LdapException
|
|
*/
|
|
public function search($filter=null, $base=null, $scope=null, $attributes=null, $params=null, $raise=null) {
|
|
if (!$this -> _link)
|
|
return $this -> error("Can't search: no LDAP link");
|
|
if (is_null($base))
|
|
$base = $this->basedn;
|
|
elseif ($base instanceof Entry)
|
|
$base = $base->dn?$base->dn:null;
|
|
|
|
if (is_null($filter)) $filter = $this->filter;
|
|
if ($filter instanceof Filter)
|
|
$filter = $filter->as_string(); // convert Filter object as string
|
|
|
|
// Adjust search function to expected scope
|
|
$scope = $scope?$scope:self :: get_param($params, 'scope', $this->scope, 'string');
|
|
if (!is_string($scope) || !array_key_exists($scope, self :: $search_function_by_scope))
|
|
return $this -> error("Invalid scope '%s' specified", $raise, $scope);
|
|
|
|
// Compute expected attributes
|
|
$attributes = $attributes?$attributes:self :: get_param($params, 'attributes', array(), 'array');
|
|
if (!is_array($attributes))
|
|
return $this -> error("Invalid expected attributes specified: must be an array", $raise);
|
|
|
|
// Run the search
|
|
$search = @call_user_func(
|
|
// @phpstan-ignore-next-line
|
|
array($this->_link, self :: $search_function_by_scope[$scope]),
|
|
$base,
|
|
$filter,
|
|
$attributes,
|
|
self :: get_param($params, 'attrsonly', false, 'bool')?1:0,
|
|
self :: get_param($params, 'sizelimit', 0, 'int'),
|
|
self :: get_param($params, 'timelimit', 0, 'int')
|
|
);
|
|
|
|
if (!$search)
|
|
return $this -> log_error(
|
|
"Error occured searching on base '%s' with filter '%s'", $raise, $base, $filter);
|
|
|
|
// @phpstan-ignore-next-line
|
|
$result = $this->_link->get_entries($search);
|
|
if (!is_array($result))
|
|
return $this -> log_error(
|
|
"Error occured retreiving the result of the search on base '%s' with filter '%s'",
|
|
$raise, $base, $filter
|
|
);
|
|
$entries = array();
|
|
for ($i=0; $i<$result['count']; $i++) {
|
|
$dn = $result[$i]['dn'];
|
|
$attrs = array();
|
|
for($j=0; $j<$result[$i]['count']; $j++) {
|
|
$attr = $result[$i][$j];
|
|
$attrs[$attr] = array();
|
|
for($k=0; $k<$result[$i][$attr]['count']; $k++)
|
|
$attrs[$attr][$k] = $result[$i][$attr][$k];
|
|
}
|
|
// @phpstan-ignore-next-line
|
|
$entries[$dn] = new Entry($dn, $attrs, $this);
|
|
}
|
|
// @phpstan-ignore-next-line
|
|
return $entries;
|
|
}
|
|
|
|
/**
|
|
* Get one entry from LDAP directory by DN
|
|
*
|
|
* This method make easier to retreive a specific entry by its DN. A search will be runned on the
|
|
* specified DN with a base scope and if one and only one entry is found, it will be returned.
|
|
*
|
|
* @param string $dn DN of the expected entry
|
|
* @param string|null $filter Filter of the search (optional, default: null == use config)
|
|
* @param array<string>|null $attributes Expected attributes returned by the search (optional,
|
|
* default: null == use params or config)
|
|
* @param array<string,mixed>|null $params Other optional parameters of the search (optional,
|
|
* default: null == use config, see search())
|
|
* @param bool|null $raise See error() (optional, default: null)
|
|
* @return Entry|false The requested Entry object, or False in case of error
|
|
* @throws LdapException
|
|
*/
|
|
public function get_entry($dn, $filter=null, $attributes=null, $params=null, $raise=null) {
|
|
$params = is_array($params)?$params:array();
|
|
$entries = $this -> search($filter, $dn, 'base', $attributes, $params, $raise);
|
|
if (is_array($entries) && count($entries) == 1)
|
|
return array_pop($entries);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get parameter value
|
|
* @param array<string,mixed>|null $params Array of provided parameter values
|
|
* @param string $key Expected parameter key
|
|
* @param mixed $default Default value if parameter if not set (optional, default: null)
|
|
* @param string|null $cast Expected type of value: if provided, the parameter value will be casted
|
|
* as the expected type (optional, default: null == no cast)
|
|
* @return mixed
|
|
*/
|
|
protected static function get_param($params, $key, $default=null, $cast=null) {
|
|
if (!is_array($params) || !array_key_exists($key, $params))
|
|
return $default;
|
|
$value = $params[$key];
|
|
switch($cast) {
|
|
case 'bool':
|
|
case 'boolean':
|
|
return boolval($value);
|
|
case 'int':
|
|
case 'integer':
|
|
return intval($value);
|
|
case 'float':
|
|
return floatval($value);
|
|
case 'str':
|
|
case 'string':
|
|
return strval($value);
|
|
case 'array':
|
|
if (is_array($value))
|
|
return $value;
|
|
if (is_null($value))
|
|
return array();
|
|
return array($value);
|
|
}
|
|
return $value;
|
|
}
|
|
}
|