eesyphp/src/Auth/Ldap.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);
}
}