Observium_CE/html/includes/ldap-functions.inc.php

410 lines
12 KiB
PHP

<?php
/**
* Observium
*
* This file is part of Observium.
*
* @package observium
* @subpackage authentication
* @copyright (C) Adam Armstrong
*
*/
/**
* Retrieves list of domain controllers from DNS through SRV records.
* Private function for this LDAP/AD modules only.
*
* @param string $domain Domain name (fqdn-style) for the AD domain.
*
* @return array Array of server names to be used for LDAP.
* @throws Net_DNS2_Exception
*/
function ldap_domain_servers_from_dns($domain) {
$servers = [];
$resolver = new Net_DNS2_Resolver();
if ($response = $resolver->query("_ldap._tcp.dc._msdcs.$domain", 'SRV', 'IN')) {
foreach ($response->answer as $answer) {
$servers[] = $answer->target;
}
}
return $servers;
}
function ldap_paged_entries($filter, $attributes, $dn) {
global $config, $ds;
$ldap_v3 = ($config['auth_mechanism'] === 'ad') || ($config['auth_ldap_version'] >= 3);
$entries = [];
if ($ldap_v3 && PHP_VERSION_ID >= 70300) {
// Use pagination for speedup fetch huge lists, there is new style, see:
// https://www.php.net/manual/en/ldap.examples-controls.php (Example #5)
$page_size = 200;
$cookie = '';
print_debug("Configuring LDAP for paged result ($page_size entries)");
do {
$search = ldap_search(
$ds, $dn, $filter, $attributes, 0, 0, 0, LDAP_DEREF_NEVER,
[['oid' => LDAP_CONTROL_PAGEDRESULTS, 'value' => ['size' => $page_size, 'cookie' => $cookie]]]
);
if (ldap_internal_is_valid($search)) {
ldap_parse_result($ds, $search, $errcode, $matcheddn, $errmsg, $referrals, $controls);
print_debug(ldap_internal_error($ds));
$entries[] = ldap_get_entries($ds, $search);
if (isset($controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'])) {
// You need to pass the cookie from the last call to the next one
$cookie = $controls[LDAP_CONTROL_PAGEDRESULTS]['value']['cookie'];
} else {
$cookie = '';
}
} else {
$cookie = '';
}
// Empty cookie means last page
} while (!empty($cookie));
$entries = array_merge([], ...$entries);
} elseif ($ldap_v3 && function_exists('ldap_control_paged_result')) {
// Use pagination for speedup fetch huge lists, pre 7.3 style
$page_size = 200;
$cookie = '';
print_debug("Configuring LDAP for paged result ($page_size entries)");
do {
// WARNING, do not make any ldap queries between ldap_control_paged_result() and ldap_control_paged_result_response()!!
// this produces a loop and errors in queries
$page_test = ldap_control_paged_result($ds, $page_size, TRUE, $cookie);
//print_vars($page_test);
print_debug(ldap_internal_error($ds));
$search = ldap_search($ds, $dn, $filter, $attributes);
print_debug(ldap_internal_error($ds));
if (ldap_internal_is_valid($search)) {
$entries[] = ldap_get_entries($ds, $search);
//print_vars($filter);
//print_vars($search);
//ldap_internal_user_entries($entries, $userlist);
ldap_control_paged_result_response($ds, $search, $cookie);
} else {
$cookie = '';
}
} while ($page_test && $cookie !== NULL && $cookie != '');
$entries = array_merge([], ...$entries);
// Reset LDAP paged result
ldap_control_paged_result($ds, 1000);
} else {
// Old php < 5.4, trouble with limit 1000 entries, see:
// http://stackoverflow.com/questions/24990243/ldap-search-not-returning-more-than-1000-user
print_debug("Configuring LDAP for Non-Paged result");
$search = ldap_search($ds, $dn, $filter, $attributes);
print_debug(ldap_internal_error($ds));
if (ldap_internal_is_valid($search)) {
$entries = ldap_get_entries($ds, $search);
//print_vars($filter);
//print_vars($search);
//ldap_internal_user_entries($entries, $userlist);
}
}
return $entries;
}
/**
* Constructor of a new part of a LDAP filter.
*
* Example:
* ldap_filter_create('memberOf', 'name', '=') >>> '(memberOf=name)'
*
* @param string $param Name of the attribute the filter should apply to
* @param string $value Filter value
* @param string $condition Matching rule
* @param boolean $escape Should $value be escaped? (default: yes)
*
* @return string Generated filter
*/
function ldap_filter_create($param, $value, $condition = '=', $escape = TRUE)
{
if ($escape) {
$value = ldap_escape_filter_value($value);
$value = array_shift($value);
}
// Convert common rule name to ldap rule
// Default rule is equals
$condition = strtolower(trim($condition));
switch ($condition) {
case 'ge':
case '>=':
$filter = '(' . $param . '>=' . $value . ')';
break;
case 'le':
case '<=':
$filter = '(' . $param . '<=' . $value . ')';
break;
case 'gt':
case 'greater':
case '>':
$filter = '(' . $param . '>' . $value . ')';
break;
case 'lt':
case 'less':
case '<':
$filter = '(' . $param . '<' . $value . ')';
break;
case 'match':
case 'matches':
case '~=':
$filter = '(' . $param . '~=' . $value . ')';
break;
case 'notmatches':
case 'notmatch':
case '!match':
case '!~=':
$filter = '(!(' . $param . '~=' . $value . '))';
break;
case 'notequals':
case 'isnot':
case 'ne':
case '!=':
case '!':
$filter = '(!(' . $param . '=' . $value . '))';
break;
case 'equals':
case 'eq':
case 'is':
case '==':
case '=':
default:
$filter = '(' . $param . '=' . $value . ')';
}
return $filter;
}
/**
* Combine two or more filter objects using a logical operator
*
* @param array $values Array with Filter entries generated by ldap_filter_create()
* @param string $condition The logical operator. May be "and", "or", "not" or the subsequent logical equivalents "&", "|", "!"
*
* @return string Generated filter
*/
function ldap_filter_combine($values = [], $condition = '&')
{
$count = safe_count($values);
if (!$count) {
return '';
}
$condition = strtolower(trim($condition));
switch ($condition) {
case '!':
case 'not':
$filter = '(!' . implode('', $values) . ')';
break;
case '|':
case 'or':
if ($count === 1) {
$filter = array_shift($values);
} else {
$filter = '(|' . implode('', $values) . ')';
}
break;
case '&':
case 'and':
default:
if ($count === 1) {
$filter = array_shift($values);
} else {
$filter = '(&' . implode('', $values) . ')';
}
}
return $filter;
}
/**
* Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
*
* Any control characters with an ACII code < 32 as well as the characters with special meaning in
* LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
* backslash followed by two hex digits representing the hexadecimal value of the character.
*
* @param array|string $values Array of values to escape
*
* @return array Array $values, but escaped
*/
function ldap_escape_filter_value($values = [])
{
// Parameter validation
if (!is_array($values)) {
$values = [$values];
}
foreach ($values as $key => $val) {
// Escaping of filter meta characters
$val = str_replace(['\\', '\5c,', '*', '(', ')'],
['\5c', '\2c', '\2a', '\28', '\29'], $val);
// ASCII < 32 escaping
$val = asc2hex32($val);
if (NULL === $val) {
$val = '\0';
} // apply escaped "null" if string is empty
$values[$key] = $val;
}
return $values;
}
/**
* Undoes the conversion done by {@link ldap_escape_filter_value()}.
*
* Converts any sequences of a backslash followed by two hex digits into the corresponding character.
*
* @param array $values Array of values to escape
*
* @return array Array $values, but unescaped
*/
function ldap_unescape_filter_value($values = [])
{
// Parameter validation
if (!is_array($values)) {
$values = [$values];
}
foreach ($values as $key => $value) {
// Translate hex code into ascii
$values[$key] = hex2asc($value);
}
return $values;
}
/**
* Converts all ASCII chars < 32 to "\HEX"
*
* @param string $string String to convert
*
* @return string
*/
function asc2hex32($string)
{
for ($i = 0, $max = strlen($string); $i < $max; $i++) {
$char = $string[$i];
if (ord($char) < 32) {
$hex = dechex(ord($char));
if (strlen($hex) === 1) {
$hex = '0' . $hex;
}
$string = str_replace($char, '\\' . $hex, $string);
}
}
return $string;
}
/**
* Converts all Hex expressions ("\HEX") to their original ASCII characters
*
* @param string $string String to convert
*
* @return string
* @author beni@php.net, heavily based on work from DavidSmith@byu.net
*/
function hex2asc($string)
{
return preg_replace_callback("/\\\([0-9A-Fa-f]{2})/", function ($matches) {
foreach ($matches as $match) {
return chr(hexdec($match));
}
}, $string);
}
/**
* Returns the textual SID for Active Directory
*
* Source: http://stackoverflow.com/questions/13130291/how-to-query-ldap-adfs-by-objectsid-in-php-or-any-language-really
*
* @param string $binsid Binary SID
*
* @return string Textual SID
*/
function ldap_bin_to_str_sid($binsid)
{
$hex_sid = bin2hex($binsid);
$rev = hexdec(substr($hex_sid, 0, 2));
$subcount = hexdec(substr($hex_sid, 2, 2));
$auth = hexdec(substr($hex_sid, 4, 12));
$result = "$rev-$auth";
for ($x = 0; $x < $subcount; $x++) {
$subauth[$x] = hexdec(ldap_little_endian(substr($hex_sid, 16 + ($x * 8), 8)));
$result .= "-" . $subauth[$x];
}
// Cheat by tacking on the S-
return 'S-' . $result;
}
/**
* Convert a little-endian hex-number to one that 'hexdec' can convert.
*
* Source: http://stackoverflow.com/questions/13130291/how-to-query-ldap-adfs-by-objectsid-in-php-or-any-language-really
*
* @param string $hex Hexadecimal number
*
* @return string Converted hexadecimal number
*/
function ldap_little_endian($hex)
{
$result = '';
for ($x = strlen($hex) - 2; $x >= 0; $x -= 2) {
$result .= substr($hex, $x, 2);
}
return $result;
}
// DOCME
function ldap_internal_is_valid($obj) {
if (PHP_VERSION_ID >= 80100) {
// ldap_bind() returns an LDAP\Connection instance in 8.1; previously, a resource was returned
// ldap_search() returns an LDAP\Result instance in 8.1; previously, a resource was returned.
return is_object($obj);
}
return is_resource($obj);
}
// DOCME
function ldap_internal_error($ds) {
if (is_bool($ds)) { return ''; }
$error_msg = ldap_error($ds);
if ($error_no = ldap_errno($ds)) {
$error_msg .= ' (' . $error_no . ': ' . ldap_err2str($error_no) . ')';
ldap_get_option($ds, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diag);
if ($diag) {
$error_msg .= ' [' . $diag . ']';
}
}
return $error_msg;
}
// EOF