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.

322 lines
10 KiB

<?php declare(strict_types = 0);
/*
** 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 used for user fields, media and groups provisioning.
*/
class CProvisioning {
public const AUDITLOG_USERNAME = 'System';
/**
* User directory data array.
*
* @var int $userdirectory['userdirectoryid']
* @var int $userdirectory['provision_status']
* @var string $userdirectory['user_username']
* @var string $userdirectory['user_lastname']
* @var string $userdirectory['search_attribute']
* @var array $userdirectory['provision_media']
* @var string $userdirectory['provision_media'][]['mediatypeid']
* @var string $userdirectory['provision_media'][]['attribute']
* @var string $userdirectory['provision_groups']
*
* @var array
*/
protected $userdirectory = [];
/**
* Array of user roles data used in group mappings.
*
* @var array $mapping_roles[]
* @var int $mapping_roles[roleid]['roleid']
* @var int $mapping_roles[roleid]['user_type']
* @var string $mapping_roles[roleid]['name']
*/
protected $mapping_roles = [];
public function __construct(array $userdirectory, array $mapping_roles) {
$this->userdirectory = $userdirectory;
$this->mapping_roles = $mapping_roles;
}
/**
* Create instance for specific user directory by id.
*
* @param int $userdirectoryid User directory id to create CProvisioning instance for.
*/
public static function forUserDirectoryId($userdirectoryid): self {
$userdirectories = API::getApiService('userdirectory')->get([
'output' => ['userdirectoryid', 'idp_type', 'provision_status', 'user_username', 'user_lastname',
'host', 'port', 'base_dn', 'bind_dn', 'search_attribute', 'start_tls', 'idp_entityid', 'sso_url',
'slo_url', 'username_attribute', 'sp_entityid', 'nameid_format', 'sign_messages', 'sign_assertions',
'sign_authn_requests', 'sign_logout_requests', 'sign_logout_responses', 'encrypt_nameid',
'encrypt_assertions', 'search_filter', 'group_basedn', 'group_name', 'group_member', 'user_ref_attr',
'group_filter', 'group_membership'
],
'userdirectoryids' => [$userdirectoryid],
'selectProvisionMedia' => ['name', 'mediatypeid', 'attribute'],
'selectProvisionGroups' => ['name', 'roleid', 'user_groups']
]);
$userdirectory = reset($userdirectories);
if (!$userdirectory || $userdirectory['provision_status'] == JIT_PROVISIONING_DISABLED) {
return new self($userdirectory, []);
}
if ($userdirectory['idp_type'] == IDP_TYPE_LDAP) {
$userdirectory += DB::select('userdirectory_ldap', [
'output' => ['bind_password'],
'filter' => ['userdirectoryid' => $userdirectoryid]
])[0];
}
$mapping_roles = [];
if ($userdirectory['provision_groups']) {
$mapping_roles = DB::select('role', [
'output' => ['roleid', 'name', 'type'],
'roleids' => array_column($userdirectory['provision_groups'], 'roleid', 'roleid'),
'preservekeys' => true
]);
}
return new self($userdirectory, $mapping_roles);
}
/**
* Get configuration options for idp.
*
* @return array
*/
public function getIdpConfig(): array {
$keys = [
IDP_TYPE_LDAP => ['host', 'port', 'base_dn', 'bind_dn', 'search_attribute', 'start_tls', 'search_filter',
'group_basedn', 'group_name', 'group_member', 'user_ref_attr', 'group_filter', 'group_membership',
'user_username', 'user_lastname', 'bind_password'
],
IDP_TYPE_SAML => ['idp_entityid', 'sso_url', 'slo_url', 'username_attribute', 'sp_entityid',
'nameid_format', 'sign_messages', 'sign_assertions', 'sign_authn_requests', 'sign_logout_requests',
'sign_logout_responses', 'encrypt_nameid', 'encrypt_assertions', 'group_name', 'user_username',
'user_lastname', 'scim_status'
]
];
return array_key_exists($this->userdirectory['idp_type'], $keys)
? array_intersect_key($this->userdirectory, array_flip($keys[$this->userdirectory['idp_type']]))
: [];
}
/**
* Get provisioning status.
*
* @return bool Return true when enabled.
*/
public function isProvisioningEnabled(): bool {
return $this->userdirectory['provision_status'] == JIT_PROVISIONING_ENABLED;
}
/**
* Get array of attributes to request from external source when requesting user data.
*
* @return array Array of attributes names to request from external source.
*/
public function getUserIdpAttributes(): array {
$fields = $this->userdirectory['idp_type'] == IDP_TYPE_LDAP
? ['user_username', 'user_lastname', 'search_attribute', 'group_membership', 'user_ref_attr']
: ['user_username', 'user_lastname'];
$attributes = array_intersect_key($this->userdirectory, array_flip($fields));
$attributes = array_merge($this->getUserIdpMediaAttributes(), $attributes);
return array_keys(array_flip(array_filter($attributes, 'strlen')));
}
/**
* Get array of attributes to request from external source when requesting user group data.
*
* @return array
*/
public function getGroupIdpAttributes(): array {
$attributes = [$this->userdirectory['group_name']];
return array_values(array_filter($attributes, 'strlen'));
}
/**
* Get array of media attributes to request from external source.
*
* @return array
*/
public function getUserIdpMediaAttributes(): array {
$attributes = array_column($this->userdirectory['provision_media'], 'attribute');
return array_keys(array_flip(array_filter($attributes, 'strlen')));
}
/**
* Return array with user fields created from matched provision data on external user data attributes.
*
* @param array $idp_user User data from external source, LDAP/SAML.
* @param bool $case_sensitive How IdP attributes should be matched.
*
* @return array
*/
public function getUserAttributes(array $idp_user, bool $case_sensitive = true): array {
$user_idp_fields = array_filter([
'name' => $this->userdirectory['user_username'],
'surname' => $this->userdirectory['user_lastname']
], 'strlen');
$user = [];
$idp_user_lowercased = [];
if (!$case_sensitive) {
foreach ($idp_user as $idp_attr => $idp_value) {
$idp_user_lowercased[strtolower($idp_attr)] = $idp_value;
}
}
foreach ($user_idp_fields as $user_field => $idp_field) {
if (array_key_exists($idp_field, $idp_user)) {
$user[$user_field] = $idp_user[$idp_field];
continue;
}
$idp_field = strtolower($idp_field);
if (array_key_exists($idp_field, $idp_user_lowercased)) {
$user[$user_field] = $idp_user_lowercased[$idp_field];
}
}
return $user;
}
/**
* Return array with user media created from matched provision_media on external user data attributes.
*
* @param array $idp_user User data from external source, LDAP/SAML.
* @param bool $case_sensitive How IdP attributes should be matched.
*
* @return array
* []['mediatypeid']
* []['sendto']
*/
public function getUserMedias(array $idp_user, bool $case_sensitive = true): array {
$user_medias = [];
$idp_user_lowercased = [];
if (!$case_sensitive) {
foreach ($idp_user as $idp_attr => $idp_value) {
$idp_user_lowercased[strtolower($idp_attr)] = $idp_value;
}
}
foreach ($this->userdirectory['provision_media'] as $idp_attributes) {
$idp_field = $idp_attributes['attribute'];
if (array_key_exists($idp_field, $idp_user)) {
$user_medias[] = [
'name' => $idp_attributes['name'],
'mediatypeid' => $idp_attributes['mediatypeid'],
'sendto' => [$idp_user[$idp_field]]
];
continue;
}
$idp_field = strtolower($idp_field);
if (array_key_exists($idp_field, $idp_user_lowercased)) {
$user_medias[] = [
'name' => $idp_attributes['name'],
'mediatypeid' => $idp_attributes['mediatypeid'],
'sendto' => [$idp_user_lowercased[$idp_field]]
];
}
}
return $user_medias;
}
/**
* Return array with user groups matched to provision groups criteria and roleid.
*
* @param array $group_names User group names data from external source, LDAP/SAML.
*
* @return array
* ['roleid'] Matched roleid to set for user. Is 0 when no match.
* ['usrgrps'] Matched mapping user groups to set for user. Empty array when no match.
* ['usrgrps'][]['usrgrpid'] User group
*/
public function getUserGroupsAndRole(array $group_names): array {
$user = ['usrgrps' => [], 'roleid' => 0];
if (!$this->userdirectory['provision_groups']) {
return $user;
}
$roleids = [];
$user_groups = [];
foreach ($this->userdirectory['provision_groups'] as $provision_group) {
$match = ($provision_group['name'] === '*');
if (strpos($provision_group['name'], '*') === false) {
$match = in_array($provision_group['name'], $group_names);
}
elseif (!$match) {
$regex = preg_quote($provision_group['name'], '/');
$regex = '/'.str_replace('\\*', '.*', $regex).'/';
$match = false;
foreach ($group_names as $group_name) {
$match = $match || preg_match($regex, $group_name);
}
}
if ($match) {
$roleids[$provision_group['roleid']] = true;
$user_groups = array_merge($user_groups, $provision_group['user_groups']);
}
}
if (!$user_groups) {
return $user;
}
$user['usrgrps'] = array_values(array_column($user_groups, null, 'usrgrpid'));
$roles = array_intersect_key($this->mapping_roles, $roleids);
if ($roles) {
CArrayHelper::sort($roles, [
['field' => 'type', 'order' => ZBX_SORT_DOWN],
['field' => 'name', 'order' => ZBX_SORT_UP]
]);
['roleid' => $user['roleid']] = reset($roles);
}
return $user;
}
}