261 lines
8 KiB
PHP
261 lines
8 KiB
PHP
<?php
|
|
|
|
namespace EesyPHP\Auth;
|
|
|
|
use EesyPHP\App;
|
|
use EesyPHP\Auth\User;
|
|
use EesyPHP\Config;
|
|
use EesyPHP\Log;
|
|
use function EesyPHP\ensure_is_array;
|
|
use function EesyPHP\cast;
|
|
use function EesyPHP\vardump;
|
|
|
|
use PEAR;
|
|
use Net_LDAP2;
|
|
use Net_LDAP2_Filter;
|
|
|
|
class Ldap extends Backend {
|
|
|
|
/**
|
|
* LDAP configuration as expected by Net_LDAP2
|
|
* @var array
|
|
*/
|
|
private static $ldap_config;
|
|
|
|
/**
|
|
* Net_LDAP2 connection (if connected)
|
|
* @var Net_LDAP2|null
|
|
*/
|
|
private static $connection = null;
|
|
|
|
/**
|
|
* Default LDAP user attributes configuration
|
|
* @var array<string,array>
|
|
*/
|
|
private static $default_user_attributes = array(
|
|
'uid' => array(
|
|
'name' => 'login',
|
|
'type' => 'string',
|
|
'multivalued' => false,
|
|
'default' => null,
|
|
),
|
|
'mail' => array(
|
|
'type' => 'string',
|
|
'multivalued' => false,
|
|
'default' => null,
|
|
),
|
|
'cn' => array(
|
|
'name' => 'name',
|
|
'type' => 'string',
|
|
'multivalued' => false,
|
|
'default' => null,
|
|
),
|
|
);
|
|
|
|
/**
|
|
* Initialize
|
|
* @return bool
|
|
*/
|
|
public static function init() {
|
|
if (!class_exists('Net_LDAP2')) {
|
|
$path = App::get('auth.ldap.netldap2_path', 'Net/LDAP2.php', 'string');
|
|
if (!@include($path)) {
|
|
Log::error('Fail to load Net_LDAP2 (%s)', $path);
|
|
return false;
|
|
}
|
|
}
|
|
foreach(array('host', 'basedn') as $param) {
|
|
if (!App::get("auth.ldap.$param")) {
|
|
Log :: error('LDAP %s not configured. Check your configuration!', $param);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
self :: $ldap_config = array (
|
|
'host' => implode(' ', App :: get('auth.ldap.host', array(), 'array')),
|
|
'basedn' => App :: get('auth.ldap.basedn', null, 'string'),
|
|
'binddn' => App :: get('auth.ldap.bind_dn', null, 'string'),
|
|
'bindpw' => App :: get('auth.ldap.bind_password', null, 'string'),
|
|
'starttls' => App :: get('starttls', false, 'bool'),
|
|
);
|
|
if ($port = App :: get('auth.ldap.port', null, 'int'))
|
|
self :: $ldap_config['port'] = $port;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Connect on the LDAP directory
|
|
* @return bool
|
|
*/
|
|
private static function connect() {
|
|
if (is_a(self :: $connection, 'Net_LDAP2')) return true;
|
|
Log :: debug(
|
|
'Connect on LDAP host "%s" as %s (base DN="%s")',
|
|
self :: $ldap_config['host'],
|
|
isset(self :: $ldap_config['binddn'])?self :: $ldap_config['binddn']:"anonymous",
|
|
self :: $ldap_config['basedn']
|
|
);
|
|
|
|
// @phpstan-ignore-next-line
|
|
self :: $connection = Net_LDAP2::connect(self :: $ldap_config);
|
|
// @phpstan-ignore-next-line
|
|
if (PEAR::isError(self :: $connection)) {
|
|
Log :: error(
|
|
'Could not connect to LDAP server (%s): %s',
|
|
self :: $ldap_config['host'], self :: $connection->getMessage());
|
|
self :: $connection = null;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Make a search in the LDAP directory
|
|
* @param string $filter The LDAP filter string
|
|
* @param array|null $attrs Expected attributes (optional, default: all existing attributes)
|
|
* @param string|null $basedn The base DN of the search (optional, default: configured root base
|
|
* DN of the LDAP connection)
|
|
* @param string|array<string>|null $sorted If defined, sort return objects by specified attribute(s)
|
|
* @param array|null $options Search options as expected by NetLDAP::search() (optional, default: null)
|
|
*/
|
|
public static function search($filter, $attrs=null, $basedn=null, $sorted=null, $options=null) {
|
|
if (!self :: connect()) return false;
|
|
$options = is_array($options)?$options:array();
|
|
if (!is_null($attrs))
|
|
$options['attributes'] = $attrs;
|
|
|
|
Log :: debug(
|
|
'Run search in LDAP directory with filter "%s" on base DN "%s"',
|
|
$filter, $basedn?$basedn:"unset");
|
|
$search = self :: $connection -> search($basedn, $filter, $options);
|
|
|
|
// @phpstan-ignore-next-line
|
|
if (PEAR::isError($search)) {
|
|
Log :: error(
|
|
'Error occured searching in LDAP with filter "%s" on base DN "%s": %s',
|
|
$filter, $basedn?$basedn:"unset", $search->getMessage()
|
|
);
|
|
return false;
|
|
}
|
|
|
|
$entries = (
|
|
$sorted?
|
|
$search -> sorted(ensure_is_array($sorted)):
|
|
$search -> entries()
|
|
);
|
|
|
|
$result = array();
|
|
foreach ($entries as $entry)
|
|
$result[$entry->dn()] = $entry -> getValues();
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Cast an LDAP value
|
|
* @param mixed $value The raw LDAP value
|
|
* @param string $type The expected type: see cast() for supported types, but boolean value will
|
|
* be casted as LDAP boolean string.
|
|
* @return mixed The casted value
|
|
*/
|
|
public static function cast($value, $type) {
|
|
switch($type) {
|
|
case 'bool':
|
|
case 'boolean':
|
|
return $value == 'TRUE';
|
|
case 'array_of_bool':
|
|
case 'array_of_boolean':
|
|
$values = array();
|
|
foreach(ensure_is_array($value) as $value)
|
|
$values[] = $value == 'TRUE';
|
|
return $values;
|
|
default:
|
|
return cast($value, $type);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Retreive LDAP attribute value(s) from LDAP entry
|
|
* @param array<string,mixed> $entry The LDAP entry
|
|
* @param string $attr The LDAP attribute name
|
|
* @param bool $all_values Return all values or just the first one (optional, default: false)
|
|
* @param mixed $default The default value to return if the LDAP attribute is undefined
|
|
* (optional, default: an empty array if $all_values, null otherwise)
|
|
* @param string|null $cast The expected type of value (optional, default: string)
|
|
*/
|
|
public static function get_attr($entry, $attr, $all_values=False, $default=null, $cast=null) {
|
|
$values = self :: cast(
|
|
isset($entry[$attr])?ensure_is_array($entry[$attr]):array(),
|
|
"array_of_".($cast?$cast:'string')
|
|
);
|
|
if ($values)
|
|
return $all_values?$values:$values[0];
|
|
if ($all_values)
|
|
return !is_null($default)?$default:array();
|
|
return $default;
|
|
}
|
|
|
|
/**
|
|
* Retreive a user by its username
|
|
* @param string $username
|
|
* @return \EesyPHP\Auth\User|null|false The user object if found, null it not, false in case of error
|
|
*/
|
|
public static function get_user($username) {
|
|
$attrs = App::get('auth.ldap.user_attributes', self :: $default_user_attributes, 'array');
|
|
$users = self :: search(
|
|
str_replace(
|
|
'[username]', Net_LDAP2_Filter::escape($username),
|
|
App::get('auth.ldap.user_filter_by_uid', 'uid=[username]', 'string')
|
|
),
|
|
array_keys($attrs),
|
|
App::get('auth.ldap.user_basedn', null, 'string')
|
|
);
|
|
if (!is_array($users)) {
|
|
Log::warning('An error occured looking for user "%s" in LDAP directory', $username);
|
|
return false;
|
|
}
|
|
if (!$users) {
|
|
Log::debug('User "%s" not found in LDAP directory', $username);
|
|
return null;
|
|
}
|
|
if (count($users) > 1) {
|
|
Log::warning(
|
|
'More than on users found with username "%s": %s',
|
|
$username, implode(' / ', array_keys($users))
|
|
);
|
|
}
|
|
$dn = key($users);
|
|
$info = array('dn' => $dn);
|
|
foreach($attrs as $attr => $attr_config) {
|
|
$info[Config::get("name", $attr, 'string', false, $attr_config)] = self :: get_attr(
|
|
$users[$dn],
|
|
$attr,
|
|
Config::get("multivalued", false, 'bool', false, $attr_config),
|
|
Config::get("default", null, null, false, $attr_config)
|
|
);
|
|
}
|
|
Log::debug('User "%s" found in LDAP directory (%s):\n%s', $username, $dn, vardump($info));
|
|
return new User($username, '\\EesyPHP\\Auth\\LDAP', $info);
|
|
}
|
|
|
|
/**
|
|
* Check a user password
|
|
* @param \EesyPHP\Auth\User $user The user object
|
|
* @param string $password The password to check
|
|
* @return boolean
|
|
*/
|
|
public static function check_password($user, $password) {
|
|
$config = self :: $ldap_config;
|
|
$config['binddn'] = (
|
|
App::get('auth.ldap.bind_with_username', false, 'bool')?
|
|
$user->username:
|
|
$user->dn
|
|
);
|
|
$config['bindpw'] = $password;
|
|
$result = Net_LDAP2::connect($config);
|
|
// @phpstan-ignore-next-line
|
|
return !PEAR::isError($result);
|
|
}
|
|
|
|
}
|