You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
560 lines
16 KiB
560 lines
16 KiB
<?php
|
|
/*
|
|
** Zabbix
|
|
** Copyright (C) 2001-2023 Zabbix SIA
|
|
**
|
|
** This program is free software; you can redistribute it and/or modify
|
|
** it under the terms of the GNU General Public License as published by
|
|
** the Free Software Foundation; either version 2 of the License, or
|
|
** (at your option) any later version.
|
|
**
|
|
** This program is distributed in the hope that it will be useful,
|
|
** but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
** GNU General Public License for more details.
|
|
**
|
|
** You should have received a copy of the GNU General Public License
|
|
** along with this program; if not, write to the Free Software
|
|
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
**/
|
|
|
|
|
|
class CLdap {
|
|
|
|
const BIND_NONE = 0;
|
|
const BIND_ANONYMOUS = 1;
|
|
const BIND_CONFIG_CREDENTIALS = 2;
|
|
const BIND_DNSTRING = 3;
|
|
|
|
const ERR_NONE = 0;
|
|
const ERR_PHP_EXTENSION = 1;
|
|
const ERR_SERVER_UNAVAILABLE = 2;
|
|
const ERR_BIND_FAILED = 3;
|
|
const ERR_BIND_ANON_FAILED = 4;
|
|
const ERR_USER_NOT_FOUND = 5;
|
|
const ERR_OPT_PROTOCOL_FAILED = 10;
|
|
const ERR_OPT_TLS_FAILED = 11;
|
|
const ERR_OPT_REFERRALS_FAILED = 12;
|
|
const ERR_OPT_DEREF_FAILED = 13;
|
|
const ERR_BIND_DNSTRING_UNAVAILABLE = 14;
|
|
const ERR_QUERY_FAILED = 15;
|
|
|
|
const DEFAULT_FILTER_USER = '(%{attr}=%{user})';
|
|
const DEFAULT_FILTER_GROUP = '(%{groupattr}=%{user})';
|
|
const DEFAULT_MEMBERSHIP_ATTRIBUTE = 'memberOf';
|
|
|
|
/**
|
|
* Type of binding made to LDAP server. One of static::BIND_ constant value.
|
|
*
|
|
* @var int
|
|
*/
|
|
public $bound;
|
|
|
|
/**
|
|
* @var int
|
|
*/
|
|
public $error;
|
|
|
|
/**
|
|
* Bind type to use when searching in LDAP tree. One of static::BIND_ constant value.
|
|
*
|
|
* @var int
|
|
*/
|
|
public $bind_type;
|
|
|
|
/**
|
|
* Bind DN string, may contain placeholders when BIND_TYPE_DNSTRING is detected.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $bind_dn;
|
|
|
|
/**
|
|
* @var array $cnf LDAP connection settings.
|
|
*/
|
|
protected $cnf = [
|
|
'host' => '',
|
|
'port' => '',
|
|
'bind_dn' => '',
|
|
'bind_password' => '',
|
|
'base_dn' => '',
|
|
'search_attribute' => '',
|
|
'search_filter' => '',
|
|
'group_basedn' => '',
|
|
'group_name' => '',
|
|
'group_member' => '',
|
|
'group_filter' => '',
|
|
'group_membership' => '',
|
|
'referrals' => 0,
|
|
'version' => 3,
|
|
'start_tls' => ZBX_AUTH_START_TLS_OFF,
|
|
'deref' => null
|
|
];
|
|
|
|
/**
|
|
* Placeholders with value used for building bind or search filter query.
|
|
* Key is placeholder name, %{attr} and value is placeholder value to replace to.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $placeholders = [];
|
|
|
|
/**
|
|
* Estabilished LDAP connection resource, for PHP8.1.0+ LDAP\Connection class instance.
|
|
*
|
|
* @var bool|resource|LDAP\Connection
|
|
*/
|
|
protected $ds;
|
|
|
|
public function __construct(array $config = []) {
|
|
$this->ds = false;
|
|
$this->bound = static::BIND_NONE;
|
|
$this->error = static::ERR_NONE;
|
|
|
|
$this->cnf = zbx_array_merge($this->cnf, $config);
|
|
|
|
if ($this->cnf['search_filter'] === '') {
|
|
$this->cnf['search_filter'] = static::DEFAULT_FILTER_USER;
|
|
}
|
|
|
|
if ($this->cnf['group_filter'] === '') {
|
|
$this->cnf['group_filter'] = static::DEFAULT_FILTER_GROUP;
|
|
}
|
|
|
|
if ($this->cnf['group_membership'] === '') {
|
|
$this->cnf['group_membership'] = static::DEFAULT_MEMBERSHIP_ATTRIBUTE;
|
|
}
|
|
|
|
$this->initBindAttributes();
|
|
$this->error = $this->moduleEnabled() ? static::ERR_NONE : static::ERR_PHP_EXTENSION;
|
|
}
|
|
|
|
/**
|
|
* Check is the PHP extension enabled.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function moduleEnabled(): bool {
|
|
return function_exists('ldap_connect') && function_exists('ldap_set_option') && function_exists('ldap_bind')
|
|
&& function_exists('ldap_search') && function_exists('ldap_get_entries')
|
|
&& function_exists('ldap_free_result') && function_exists('ldap_start_tls');
|
|
}
|
|
|
|
/**
|
|
* Initialize connection to LDAP server. Set LDAP connection options defined in configuration when required.
|
|
* Return true on success.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function connect(): bool {
|
|
$this->error = static::ERR_NONE;
|
|
|
|
if ($this->ds !== false) {
|
|
return true;
|
|
}
|
|
|
|
$this->bound = static::BIND_NONE;
|
|
|
|
if (!$this->ds = @ldap_connect($this->cnf['host'], $this->cnf['port'])) {
|
|
$this->error = static::ERR_SERVER_UNAVAILABLE;
|
|
|
|
return false;
|
|
}
|
|
|
|
// Set protocol version and dependent options.
|
|
if ($this->cnf['version']) {
|
|
if (!@ldap_set_option($this->ds, LDAP_OPT_PROTOCOL_VERSION, $this->cnf['version'])) {
|
|
$this->error = static::ERR_OPT_PROTOCOL_FAILED;
|
|
}
|
|
elseif ($this->cnf['version'] == 3) {
|
|
if ($this->cnf['start_tls'] && !@ldap_start_tls($this->ds)) {
|
|
$this->error = static::ERR_OPT_TLS_FAILED;
|
|
}
|
|
|
|
if (!$this->cnf['referrals']
|
|
&& !@ldap_set_option($this->ds, LDAP_OPT_REFERRALS, $this->cnf['referrals'])) {
|
|
$this->error = static::ERR_OPT_REFERRALS_FAILED;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($this->cnf['deref']) && !@ldap_set_option($this->ds, LDAP_OPT_DEREF, $this->cnf['deref'])) {
|
|
$this->error = static::ERR_OPT_DEREF_FAILED;
|
|
}
|
|
|
|
return $this->error == static::ERR_NONE;
|
|
}
|
|
|
|
/**
|
|
* Bind to LDAP server. Set $this->bound to type of successful binding.
|
|
* Arguments $user and $password are required when bind type BIND_DNSTRING is set.
|
|
*
|
|
* Bind types:
|
|
* BIND_CONFIG_CREDENTIALS - Special configuration user is used to bind and search.
|
|
* BIND_ANONYMOUS - Anonymous user is used to bind and search.
|
|
* BIND_DNSTRING - Logging in user is used to bind and search.
|
|
*
|
|
* Both arguments $user and $password are required for bind type BIND_DNSTRING only.
|
|
*
|
|
* @param string $user User name value.
|
|
* @param string $password Password value.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function bind($user = null, $password = null): bool {
|
|
$this->bound = static::BIND_NONE;
|
|
|
|
if ($this->bind_type == static::BIND_ANONYMOUS) {
|
|
if (!@ldap_bind($this->ds)) {
|
|
$this->error = static::ERR_BIND_ANON_FAILED;
|
|
|
|
return false;
|
|
}
|
|
|
|
$this->bound = static::BIND_ANONYMOUS;
|
|
|
|
return true;
|
|
}
|
|
|
|
$dn = $this->bind_dn;
|
|
$dn_password = $this->cnf['bind_password'];
|
|
|
|
if ($this->bind_type == static::BIND_DNSTRING) {
|
|
if ($user === null || $password === null) {
|
|
$this->error = static::ERR_BIND_DNSTRING_UNAVAILABLE;
|
|
|
|
return false;
|
|
}
|
|
|
|
$dn = $this->makeFilter($this->bind_dn, ['%{user}' => $user], LDAP_ESCAPE_DN);
|
|
$dn_password = $password;
|
|
}
|
|
|
|
if (!@ldap_bind($this->ds, $dn, $dn_password)) {
|
|
$this->error = static::ERR_BIND_FAILED;
|
|
|
|
return false;
|
|
}
|
|
|
|
$this->bound = $this->bind_type;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check validity of user credentials. Do not allow to check credentials when password is empty.
|
|
*
|
|
* @param string $user User name attribute value.
|
|
* @param string $pass User password attribute value.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function checkCredentials(string $user, string $pass): bool {
|
|
if (!$pass) {
|
|
$this->error = static::ERR_USER_NOT_FOUND;
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!$this->connect() || !$this->bind($user, $pass)) {
|
|
return false;
|
|
}
|
|
|
|
if (!$this->bind($user, $pass)) {
|
|
if ($this->bind_type == static::BIND_DNSTRING) {
|
|
$this->error = static::ERR_USER_NOT_FOUND;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if ($this->bound == static::BIND_ANONYMOUS || $this->bound == static::BIND_CONFIG_CREDENTIALS) {
|
|
// No need for user default attributes, only 'dn'.
|
|
$users = $this->search($this->cnf['base_dn'], $this->cnf['search_filter'], ['%{user}' => $user], ['dn']);
|
|
|
|
if ($users['count'] != 1) {
|
|
// Multiple users matched criteria.
|
|
$this->error = static::ERR_USER_NOT_FOUND;
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!array_key_exists('dn', $users[0]) || !@ldap_bind($this->ds, $users[0]['dn'], $pass)) {
|
|
$this->error = static::ERR_USER_NOT_FOUND;
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get array of user groups. Is not available for bind type BIND_DNSTRING if password is not supplied.
|
|
*
|
|
* @param array $attributes Array of group attributes to return for every group.
|
|
* @param string $user User username value.
|
|
* @param string $password User password value, is required only for BIND_DNSTRING.
|
|
*
|
|
* @return array Array of arrays of matched group.
|
|
*/
|
|
public function getGroupAttributes(array $attributes, string $user, $password = null): array {
|
|
if ($attributes == [] || !$this->connect() || !$this->bind($user, $password)) {
|
|
return [];
|
|
}
|
|
|
|
$placeholders = [
|
|
'%{user}' => $user,
|
|
'%{groupattr}' => $this->cnf['group_member']
|
|
];
|
|
$results = $this->search($this->cnf['group_basedn'], $this->cnf['group_filter'], $placeholders, $attributes);
|
|
$groups = [];
|
|
|
|
if ($results['count'] == 0) {
|
|
return $groups;
|
|
}
|
|
|
|
$attributes = array_flip(array_map('strtolower', $attributes));
|
|
|
|
for ($j = 0; $j < $results['count']; $j++) {
|
|
$result = $results[$j];
|
|
$result_attributes = array_intersect_key($result, $attributes);
|
|
|
|
if (!$result_attributes) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($result_attributes as &$value) {
|
|
$value = $value[0];
|
|
}
|
|
unset($value);
|
|
|
|
$groups[] = $result_attributes;
|
|
}
|
|
|
|
return $groups;
|
|
}
|
|
|
|
/**
|
|
* Get user data with specified attributes. Not available for bind type BIND_DNSTRING if password is not supplied.
|
|
* Mapped attribute names will be set to lower case.
|
|
*
|
|
* @param array $attributes Array of LDAP tree attributes names to be returned.
|
|
* @param string $user User to search attributes for.
|
|
* @param string $password (optional) User password, required only for BIND_DNSTRING.
|
|
*
|
|
* @return array Associative array of user attributes.
|
|
*/
|
|
public function getUserAttributes(array $attributes, string $user, $password = null): array {
|
|
if ($attributes == [] || !$this->connect() || !$this->bind($user, $password)) {
|
|
return [];
|
|
}
|
|
|
|
$placeholders = ['%{user}' => $user];
|
|
$results = $this->search($this->cnf['base_dn'], $this->cnf['search_filter'], $placeholders, $attributes);
|
|
$user = [];
|
|
|
|
if ($results['count'] == 0) {
|
|
return $user;
|
|
}
|
|
|
|
$results = $results[0];
|
|
|
|
if ($results['count'] == 0) {
|
|
return $user;
|
|
}
|
|
|
|
$group_key = strtolower($this->cnf['group_membership']);
|
|
$group_name_key = strtolower($this->cnf['group_name']);
|
|
|
|
for ($i = 0; $i < $results['count']; $i++) {
|
|
$key = $results[$i];
|
|
|
|
$user[$key] = $results[$i] === $group_key
|
|
? $this->getGroupPatternsFromDns($group_name_key, $results[$key])
|
|
: $results[$key][0];
|
|
}
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* Extract the group pattern from given DN strings.
|
|
* For DN string "cn=zabbix-admins,ou=Group,dc=example,dc=org" and the "Group name attribute" set to "cn",
|
|
* the string "zabbix-admins" will be stored to the $groups array.
|
|
*
|
|
* @param string $group_name_key Lower case group name attribute for which to extract value from RDN.
|
|
* @param array $group_dns Array of DN strings.
|
|
*
|
|
* @return array Strings with the extracted groups, if any.
|
|
*/
|
|
public function getGroupPatternsFromDns(string $group_name_key, array $group_dns): array {
|
|
$groups = [];
|
|
|
|
foreach ($group_dns as $group_dn) {
|
|
$rdns = ldap_explode_dn($group_dn, 0);
|
|
|
|
if (!is_array($rdns)) {
|
|
continue;
|
|
}
|
|
|
|
foreach ($rdns as $rdn) {
|
|
if (strpos($rdn, '=') === false) {
|
|
continue;
|
|
}
|
|
|
|
/*
|
|
* For multi-value RDNs $rdn_key will be set to key of first key-value pair, the rest of string as value.
|
|
* For example for RDN "cn=John Doe+mail=jdoe@example.com" $rdn_value is "John Doe+mail=jdoe@example.com".
|
|
*/
|
|
[$rdn_key, $rdn_value] = explode('=', $rdn, 2);
|
|
|
|
if (strtolower($rdn_key) !== $group_name_key) {
|
|
continue;
|
|
}
|
|
|
|
// Convert escaped charcodes, f.e. 'Universit\C4\81te' => 'Universitāte'.
|
|
$groups[] = preg_replace_callback('/\\\\([0-9A-F]{2})/i', function (array $match): string {
|
|
return chr(hexdec($match[1]));
|
|
}, $rdn_value);
|
|
}
|
|
}
|
|
|
|
return $groups;
|
|
}
|
|
|
|
/**
|
|
* Setter for additional placeholders supported in bind or search query.
|
|
*
|
|
* @param array $placeholders Associative array where key is placeholder in form %{name}.
|
|
*/
|
|
public function setQueryPlaceholders(array $placeholders) {
|
|
$this->placeholders = $placeholders;
|
|
}
|
|
|
|
/**
|
|
* Return user data with medias, groups, roleid and user attributes matched from LDAP user data according
|
|
* provisioning options. All attributes are matched in case insensitive way.
|
|
*
|
|
* @param CProvisioning $provisioning Provisioning class instance.
|
|
* @param string $username Username of user to get provisioned data for.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getProvisionedData(CProvisioning $provisioning, string $username): array {
|
|
$ldap_groups = [];
|
|
$user = [
|
|
'medias' => [],
|
|
'usrgrps' => [],
|
|
'roleid' => 0
|
|
];
|
|
$config = $provisioning->getIdpConfig();
|
|
$user_attributes = $provisioning->getUserIdpAttributes();
|
|
$idp_user = $this->getUserAttributes($user_attributes, $username);
|
|
$user = $provisioning->getUserAttributes($idp_user, false);
|
|
$user['medias'] = $provisioning->getUserMedias($idp_user, false);
|
|
|
|
if ($config['group_membership'] !== '') {
|
|
$group_key = strtolower($config['group_membership']);
|
|
|
|
if (array_key_exists($group_key, $idp_user) && is_array($idp_user[$group_key])) {
|
|
$ldap_groups = $idp_user[$group_key];
|
|
}
|
|
}
|
|
else if ($config['group_filter'] !== '') {
|
|
$user_ref_attr = strtolower($config['user_ref_attr']);
|
|
|
|
if ($user_ref_attr !== '' && array_key_exists($user_ref_attr, $idp_user)) {
|
|
$this->setQueryPlaceholders(['%{ref}' => $idp_user[$user_ref_attr]]);
|
|
}
|
|
|
|
$group_attributes = $provisioning->getGroupIdpAttributes();
|
|
$ldap_groups = $this->getGroupAttributes($group_attributes, $username);
|
|
$ldap_groups = array_column($ldap_groups, strtolower($config['group_name']));
|
|
}
|
|
|
|
$user = array_merge($user, $provisioning->getUserGroupsAndRole($ldap_groups));
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* Setup bind attributes according LDAP configuration.
|
|
*/
|
|
protected function initBindAttributes() {
|
|
$this->bind_type = static::BIND_ANONYMOUS;
|
|
|
|
if ($this->cnf['bind_dn'] !== '' && $this->cnf['bind_password'] !== '') {
|
|
$this->bind_type = static::BIND_CONFIG_CREDENTIALS;
|
|
$this->bind_dn = $this->cnf['bind_dn'];
|
|
}
|
|
elseif ($this->cnf['bind_dn'] !== '' && $this->cnf['search_filter'] !== static::DEFAULT_FILTER_USER) {
|
|
$this->bind_type = static::BIND_DNSTRING;
|
|
$this->bind_dn = $this->cnf['bind_dn'];
|
|
}
|
|
elseif (strpos($this->cnf['base_dn'], '%{user}') !== false) {
|
|
$this->bind_type = static::BIND_DNSTRING;
|
|
$this->bind_dn = $this->cnf['base_dn'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replaces placeholders found in string with their data.
|
|
*
|
|
* @param string $filter Filter string where to replace placeholders.
|
|
* @param array $placeholders Associative array for replacement in $filter string.
|
|
* Placeholders %{attr}, %{host} will be added by default,
|
|
* array key should be in form %{placeholder_key_value}.
|
|
* @param int escape_context Resulting string usage context:
|
|
* LDAP_ESCAPE_FILTER - use result string as filter argument of ldap_search.
|
|
* LDAP_ESCAPE_DN - use result string as base dn.
|
|
*
|
|
* @return string Filter string with replaced placeholders in it.
|
|
*/
|
|
protected function makeFilter(string $filter, array $placeholders, $escape_context): string {
|
|
$replace_pairs = $placeholders + [
|
|
'%{attr}' => $this->cnf['search_attribute'],
|
|
'%{host}' => $this->cnf['host']
|
|
];
|
|
|
|
foreach ($replace_pairs as &$value) {
|
|
$value = ldap_escape($value, '', $escape_context);
|
|
}
|
|
unset($value);
|
|
|
|
return strtr($filter, $replace_pairs);
|
|
}
|
|
|
|
/**
|
|
* Search for entry in LDAP tree for specified $dn and $filter.
|
|
* Requested attributes in resulting array, will be set in lowercase.
|
|
*
|
|
* @param string $dn DN string value, supports placeholders.
|
|
* @param string $filter Filter string, supports placeholders.
|
|
* @param array $placeholders Associative array of placeholders for creating base and filter for ldap_search.
|
|
* @param array $attributes List of attributes to be returned.
|
|
*
|
|
* @return array Array of ldap_get_entries.
|
|
*/
|
|
protected function search(string $dn, string $filter, array $placeholders, array $attributes): array {
|
|
$this->error = static::ERR_NONE;
|
|
$base = $this->makeFilter($dn, $placeholders, LDAP_ESCAPE_DN);
|
|
$filter = $this->makeFilter($filter, $placeholders + $this->placeholders, LDAP_ESCAPE_FILTER);
|
|
$resource = @ldap_search($this->ds, $base, $filter, $attributes);
|
|
$results = false;
|
|
|
|
if ($resource !== false) {
|
|
$results = @ldap_get_entries($this->ds, $resource);
|
|
ldap_free_result($resource);
|
|
}
|
|
|
|
if ($resource === false || $results === false) {
|
|
$this->error = static::ERR_QUERY_FAILED;
|
|
|
|
return ['count' => 0];
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
}
|