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.
511 lines
18 KiB
511 lines
18 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.
|
|
**/
|
|
|
|
|
|
namespace SCIM\services;
|
|
|
|
use API as APIRPC;
|
|
use APIException;
|
|
use CApiInputValidator;
|
|
use CAuthenticationHelper;
|
|
use CProvisioning;
|
|
use DB;
|
|
use Exception;
|
|
use SCIM\ScimApiService;
|
|
|
|
class User extends ScimApiService {
|
|
|
|
public const SCIM_SCHEMA = 'urn:ietf:params:scim:schemas:core:2.0:User';
|
|
|
|
/**
|
|
* Returns information on specific user or all users if no specific information is requested.
|
|
* If user is not in database, returns only 'schemas' parameter.
|
|
*
|
|
* @param array $options Array with data from request.
|
|
* @param string $options['userName'] UserName parameter from GET request.
|
|
* @param string $options['id'] User id parameter from GET request URL.
|
|
*
|
|
* @return array Array with data necessary to create response.
|
|
*
|
|
* @throws Exception
|
|
*/
|
|
public function get(array $options = []): array {
|
|
$this->validateGet($options);
|
|
|
|
$userdirectoryid = CAuthenticationHelper::getSamlUserdirectoryidForScim();
|
|
|
|
if (array_key_exists('userName', $options)) {
|
|
$users = APIRPC::User()->get([
|
|
'output' => ['userid', 'username', 'userdirectoryid'],
|
|
'selectUsrgrps' => ['usrgrpid'],
|
|
'filter' => ['username' => $options['userName']]
|
|
]);
|
|
|
|
if ($users && $users[0]['userdirectoryid'] != $userdirectoryid) {
|
|
self::exception(ZBX_API_ERROR_PARAMETERS,
|
|
'User with username '.$options["userName"].' already exists.'
|
|
);
|
|
}
|
|
|
|
$user_groups = $users ? array_column($users[0]['usrgrps'], 'usrgrpid') : [];
|
|
$disabled_groupid = CAuthenticationHelper::get(CAuthenticationHelper::DISABLED_USER_GROUPID);
|
|
|
|
if (!$users || (count($user_groups) == 1 && $user_groups[0] == $disabled_groupid)) {
|
|
return [];
|
|
}
|
|
|
|
$user = $users[0];
|
|
$this->addScimUserAttributes($user, $options);
|
|
|
|
return $user;
|
|
}
|
|
|
|
if (array_key_exists('id', $options)) {
|
|
$users = APIRPC::User()->get([
|
|
'output' => ['userid', 'username', 'userdirectoryid'],
|
|
'userids' => $options['id'],
|
|
'filter' => ['userdirectoryid' => $userdirectoryid]
|
|
]);
|
|
|
|
if (!$users) {
|
|
self::exception(ZBX_API_ERROR_NO_ENTITY, 'No permissions to referred object or it does not exist!');
|
|
}
|
|
|
|
$user = $users[0];
|
|
$this->addScimUserAttributes($user, $options);
|
|
|
|
return $user;
|
|
}
|
|
|
|
$users = APIRPC::User()->get([
|
|
'output' => ['userid', 'username'],
|
|
'filter' => ['userdirectoryid' => $userdirectoryid]
|
|
]);
|
|
|
|
foreach ($users as &$user) {
|
|
$user['userdirectoryid'] = $userdirectoryid;
|
|
$this->addScimUserAttributes($user);
|
|
}
|
|
unset($user);
|
|
|
|
return $users;
|
|
}
|
|
|
|
/**
|
|
* @param array $options
|
|
*
|
|
* @throws APIException
|
|
*/
|
|
private function validateGet(array &$options): void {
|
|
$api_input_rules = ['type' => API_OBJECT, 'fields' => [
|
|
'id' => ['type' => API_ID],
|
|
'userName' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY],
|
|
'startIndex' => ['type' => API_INT32, 'default' => 1],
|
|
'count' => ['type' => API_INT32, 'default' => 100]
|
|
]];
|
|
|
|
if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
|
|
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if requested user is in database. If user does not exist, creates new user, if user exists, updates this
|
|
* user.
|
|
*
|
|
* @param array $options Array with different attributes that might be set up in SAML settings.
|
|
* @param string $options['userName'] Users user name based on which user will be searched.
|
|
*
|
|
* @return array Created or updated user data.
|
|
*/
|
|
public function post(array $options): array {
|
|
$this->validatePost($options);
|
|
|
|
$db_users = APIRPC::User()->get([
|
|
'output' => ['userid', 'userdirectoryid'],
|
|
'filter' => ['username' => $options['userName']]
|
|
]);
|
|
|
|
$userdirectoryid = CAuthenticationHelper::getSamlUserdirectoryidForScim();
|
|
$provisioning = CProvisioning::forUserDirectoryId($userdirectoryid);
|
|
|
|
$user_data['userdirectoryid'] = $userdirectoryid;
|
|
$user_data += $provisioning->getUserAttributes($options);
|
|
$user_data['medias'] = $provisioning->getUserMedias($options);
|
|
|
|
if (!$db_users) {
|
|
$user_data['username'] = $options['userName'];
|
|
$user = APIRPC::User()->createProvisionedUser($user_data);
|
|
}
|
|
elseif ($db_users[0]['userdirectoryid'] == $userdirectoryid) {
|
|
$user_data['userid'] = $db_users[0]['userid'];
|
|
$user = APIRPC::User()->updateProvisionedUser($user_data);
|
|
}
|
|
else {
|
|
self::exception(ZBX_API_ERROR_PARAMETERS,
|
|
'User with username '.$options['userName'].' already exists.'
|
|
);
|
|
}
|
|
|
|
$this->addScimUserAttributes($user, $options);
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* @param array $options
|
|
*
|
|
* @throws APIException
|
|
*/
|
|
private function validatePost(array &$options): void {
|
|
$api_input_rules = ['type' => API_OBJECT, 'flags' => API_REQUIRED | API_ALLOW_UNEXPECTED, 'fields' => [
|
|
'schemas' => ['type' => API_STRINGS_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY],
|
|
'userName' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY]
|
|
]];
|
|
|
|
if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
|
|
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
|
|
}
|
|
|
|
if (!in_array(self::SCIM_SCHEMA, $options['schemas'], true)) {
|
|
self::exception(ZBX_API_ERROR_PARAMETERS, 'Incorrect schema was sent in the request.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates user in the database with newly received information. If $options['active'] parameter is false, user
|
|
* is deleted from the database.
|
|
*
|
|
* @param array $options
|
|
* @param string $options['id']
|
|
* @param string $options['userName']
|
|
* @param bool $options['active'] True of false, but sent as string.
|
|
*
|
|
* @return array Array of updated user data.
|
|
*/
|
|
public function put(array $options): array {
|
|
// In order to comply with Azure SCIM without flag "aadOptscim062020", attribute active value is transformed to
|
|
// boolean.
|
|
if (array_key_exists('active', $options) && !is_bool($options['active'])) {
|
|
$options['active'] = strtolower($options['active']) === 'true';
|
|
}
|
|
|
|
$this->validatePut($options, $db_user);
|
|
|
|
$db_user = $db_user[0];
|
|
$user_group_names = [];
|
|
$provisioning = CProvisioning::forUserDirectoryId($db_user['userdirectoryid']);
|
|
|
|
// Some IdPs have group attribute, but others don't.
|
|
if (array_key_exists('groups', $options)) {
|
|
$user_group_names = array_column($options['groups'], 'display');
|
|
}
|
|
else {
|
|
$user_groupids = DB::select('user_scim_group', [
|
|
'output' => ['scim_groupid'],
|
|
'filter' => ['userid' => $options['id']]
|
|
]);
|
|
|
|
if ($user_groupids) {
|
|
$user_group_names = DB::select('scim_group', [
|
|
'output' => ['name'],
|
|
'scim_groupids' => array_column($user_groupids, 'scim_groupid')
|
|
]);
|
|
$user_group_names = array_column($user_group_names, 'name');
|
|
}
|
|
}
|
|
|
|
// In case some IdPs do not send attribute 'active'.
|
|
$options += [
|
|
'active' => true
|
|
];
|
|
|
|
$user_data = [
|
|
'userid' => $db_user['userid'],
|
|
'username' => $options['userName']
|
|
];
|
|
$user_data += $provisioning->getUserAttributes($options);
|
|
$user_data += $provisioning->getUserGroupsAndRole($user_group_names);
|
|
$user_data['medias'] = $provisioning->getUserMedias($options);
|
|
|
|
if ($options['active'] == false) {
|
|
$user_data['usrgrps'] = [];
|
|
}
|
|
|
|
$user = APIRPC::User()->updateProvisionedUser($user_data);
|
|
|
|
if ($options['active'] == false) {
|
|
$user['userid'] = $db_user['userid'];
|
|
}
|
|
|
|
$this->addScimUserAttributes($user, $options);
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* @param array $options
|
|
* @param array $db_user
|
|
*
|
|
* @throws APIException
|
|
*/
|
|
private function validatePut(array &$options, ?array &$db_user): void {
|
|
$api_input_rules = ['type' => API_OBJECT, 'flags' => API_REQUIRED | API_ALLOW_UNEXPECTED, 'fields' => [
|
|
'schemas' => ['type' => API_STRINGS_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY],
|
|
'id' => ['type' => API_ID, 'flags' => API_REQUIRED],
|
|
'userName' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('users', 'username')],
|
|
'active' => ['type' => API_BOOLEAN, 'flags' => API_NOT_EMPTY],
|
|
'groups' => ['type' => API_OBJECTS, 'fields' => [
|
|
'value' => ['type' => API_ID, 'flags' => API_REQUIRED],
|
|
'display' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY]
|
|
]]
|
|
]];
|
|
|
|
if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
|
|
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
|
|
}
|
|
|
|
if (!in_array(self::SCIM_SCHEMA, $options['schemas'], true)) {
|
|
self::exception(ZBX_API_ERROR_PARAMETERS, 'Incorrect schema was sent in the request.');
|
|
}
|
|
|
|
$userdirectoryid = CAuthenticationHelper::getSamlUserdirectoryidForScim();
|
|
$db_user = APIRPC::User()->get([
|
|
'output' => ['userid', 'userdirectoryid'],
|
|
'userids' => $options['id'],
|
|
'filter' => ['userdirectoryid' => $userdirectoryid]
|
|
]);
|
|
|
|
if (!$db_user) {
|
|
self::exception(ZBX_API_ERROR_NO_ENTITY, 'No permissions to referred object or it does not exist!');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates user in the database with newly received information.
|
|
*
|
|
* @param array $options
|
|
* @param string $options['id'] User id.
|
|
* @param array $options['Operations'] List of operations that need to be performed.
|
|
* @param string $options['Operations'][]['op'] Operation that needs to be performed -'add',
|
|
* 'replace', 'remove'.
|
|
* @param string $options['Operations'][]['path'] On what operation should be performed, filters are
|
|
* not supported, supported 'path' is only 'userName',
|
|
* 'active' and the one that matches custom user
|
|
* attributes .
|
|
* @param string $options['Operations'][]['value'] Value on which operation should be
|
|
* performed. If operation is 'remove' this can be
|
|
* omitted.
|
|
*
|
|
* @return array Array of updated user data.
|
|
*
|
|
* @throws APIException
|
|
*/
|
|
public function patch(array $options): array {
|
|
// In order to comply with Azure SCIM without flag "aadOptscim062020", attribute active value is transformed to
|
|
// boolean.
|
|
if (array_key_exists('Operations', $options)) {
|
|
foreach ($options['Operations'] as &$operation) {
|
|
if (array_key_exists('path', $operation) && $operation['path'] === 'active'
|
|
&& !is_bool($operation['value'])
|
|
) {
|
|
$operation['value'] = strtolower($operation['value']) === 'true';
|
|
}
|
|
}
|
|
unset($operation);
|
|
}
|
|
|
|
$this->validatePatch($options, $db_user);
|
|
|
|
$user_idp_data = [];
|
|
foreach ($options['Operations'] as $operation) {
|
|
if ($operation['op'] === 'remove') {
|
|
$user_idp_data[$operation['path']] = '';
|
|
}
|
|
else {
|
|
$user_idp_data[$operation['path']] = $operation['value'];
|
|
}
|
|
}
|
|
|
|
$provisioning = CProvisioning::forUserDirectoryId($db_user['userdirectoryid']);
|
|
$new_user_data = $provisioning->getUserAttributes($user_idp_data);
|
|
$new_user_data = array_merge($db_user, $new_user_data);
|
|
|
|
// If user status 'active' is changed to false, user needs to be added to disabled group.
|
|
if (array_key_exists('active', $user_idp_data) && strtolower($user_idp_data['active']) == false) {
|
|
$new_user_data['usrgrps'] = [];
|
|
$user_idp_data['active'] = false;
|
|
}
|
|
|
|
// If disabled user is activated again, need to return group mapping.
|
|
if ($db_user['roleid'] == 0 && array_key_exists('active', $user_idp_data)
|
|
&& strtolower($user_idp_data['active']) == true) {
|
|
|
|
$group_names = DBfetchColumn(DBselect(
|
|
'SELECT g.name'.
|
|
' FROM user_scim_group ug,scim_group g'.
|
|
' WHERE g.scim_groupid=ug.scim_groupid AND '.dbConditionId('ug.userid', [$options['id']])
|
|
), 'name');
|
|
|
|
$new_user_data = array_merge($new_user_data, $provisioning->getUserGroupsAndRole($group_names));
|
|
}
|
|
|
|
$user= APIRPC::User()->updateProvisionedUser($new_user_data);
|
|
$user = $user ?: $new_user_data;
|
|
|
|
$this->addScimUserAttributes($user, $user_idp_data);
|
|
|
|
return $user;
|
|
}
|
|
|
|
/**
|
|
* @param array $options
|
|
* @param array $db_user
|
|
*
|
|
* @throws APIException
|
|
*/
|
|
private function validatePatch(array &$options, array &$db_user = null): void {
|
|
$api_input_rules = ['type' => API_OBJECT, 'flags' => API_REQUIRED | API_ALLOW_UNEXPECTED, 'fields' => [
|
|
'id' => ['type' => API_ID, 'flags' => API_REQUIRED],
|
|
'schemas' => ['type' => API_STRINGS_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY],
|
|
'Operations' => ['type' => API_OBJECTS, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'fields' => [
|
|
'op' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'in' => implode(',', ['add', 'remove', 'replace', 'Add', 'Remove', 'Replace'])],
|
|
'path' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED],
|
|
'value' => ['type' => API_MULTIPLE, 'rules' => [
|
|
['if' => ['field' => 'path', 'in' => implode(',', ['active'])], 'type' => API_BOOLEAN, 'flags' => API_REQUIRED],
|
|
['if' => ['field' => 'op', 'in' => implode(',', ['remove', 'Remove'])], 'type' => API_STRING_UTF8],
|
|
['else' => true, 'type' => API_STRING_UTF8, 'flags' => API_REQUIRED]
|
|
]]
|
|
]]
|
|
]];
|
|
|
|
if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
|
|
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
|
|
}
|
|
|
|
if (!in_array(ScimApiService::SCIM_PATCH_SCHEMA, $options['schemas'], true)) {
|
|
self::exception(ZBX_API_ERROR_PARAMETERS, 'Incorrect schema was sent in the request.');
|
|
}
|
|
|
|
$userdirectoryid = CAuthenticationHelper::getSamlUserdirectoryidForScim();
|
|
$db_user = APIRPC::User()->get([
|
|
'output' => ['userid', 'name', 'surname', 'userdirectoryid', 'roleid'],
|
|
'userids' => $options['id'],
|
|
'filter' => ['userdirectoryid' => $userdirectoryid]
|
|
]);
|
|
|
|
if (!$db_user) {
|
|
self::exception(ZBX_API_ERROR_NO_ENTITY, 'No permissions to referred object or it does not exist!');
|
|
}
|
|
|
|
$db_user = $db_user[0];
|
|
|
|
foreach ($options['Operations'] as &$operation) {
|
|
$operation['op'] = strtolower($operation['op']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes requested user based on userid.
|
|
*
|
|
* @param array $options
|
|
* @param string $options['id'] Userid.
|
|
*
|
|
* @return array Returns only schema parameter, the rest of the parameters are not included.
|
|
*/
|
|
public function delete(array $options): array {
|
|
$this->validateDelete($options);
|
|
|
|
$provisioning = CProvisioning::forUserDirectoryId($options['userdirectoryid']);
|
|
$user_data = [
|
|
'userid' => $options['id']
|
|
];
|
|
$user_data += $provisioning->getUserAttributes($options);
|
|
$user_data['medias'] = $provisioning->getUserMedias($options);
|
|
$user_data['usrgrps'] = [];
|
|
|
|
DB::delete('user_scim_group', ['userid' => $user_data['userid']]);
|
|
|
|
return APIRPC::User()->updateProvisionedUser($user_data);
|
|
}
|
|
|
|
/**
|
|
* @param array $options
|
|
*
|
|
* @throws APIException if the input is invalid.
|
|
*/
|
|
private function validateDelete(array &$options) {
|
|
$api_input_rules = ['type' => API_OBJECT, 'flags' => API_REQUIRED, 'fields' => [
|
|
'id' => ['type' => API_ID, 'flags' => API_REQUIRED]
|
|
]];
|
|
|
|
if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) {
|
|
self::exception(ZBX_API_ERROR_PARAMETERS, $error);
|
|
}
|
|
|
|
$userdirectoryid = CAuthenticationHelper::getSamlUserdirectoryidForScim();
|
|
$options['userdirectoryid'] = $userdirectoryid;
|
|
|
|
$db_users = APIRPC::User()->get([
|
|
'output' => ['userid', 'userdirectoryid'],
|
|
'userids' => $options['id'],
|
|
'filter' => ['userdirectoryid' => $userdirectoryid]
|
|
]);
|
|
|
|
if (!$db_users) {
|
|
self::exception(ZBX_API_ERROR_NO_ENTITY, 'No permissions to referred object or it does not exist!');
|
|
}
|
|
}
|
|
|
|
private function addScimUserAttributes(array &$user, array $options = []): void {
|
|
$userdirectoryid = CAuthenticationHelper::getSamlUserdirectoryidForScim();
|
|
$user = array_intersect_key($user, array_flip(['userid', 'username', 'surname', 'name']));
|
|
|
|
if (!array_key_exists('username', $user)) {
|
|
if (array_key_exists('username', $options)) {
|
|
$user['username'] = $options['userName'];
|
|
}
|
|
else {
|
|
$usernames = APIRPC::User()->get([
|
|
'output' => ['username'],
|
|
'userids' => $user['userid'],
|
|
'filter' => ['userdirectoryid' => $userdirectoryid]
|
|
]);
|
|
$user['username'] = $usernames[0]['username'];
|
|
}
|
|
}
|
|
|
|
// Property 'name' in SCIM response is required for Okta SCIM to work correctly.
|
|
$user['name'] = array_key_exists('name', $options) ? $options['name'] : ['givenName' => '', 'familyName' => ''];
|
|
$user['active'] = array_key_exists('active', $options) ? $options['active'] : true;
|
|
|
|
$provisioning = CProvisioning::forUserDirectoryId($userdirectoryid);
|
|
$user_attributes = $provisioning->getUserAttributes($options);
|
|
$user += $user_attributes;
|
|
|
|
$media_attributes = $provisioning->getUserIdpMediaAttributes();
|
|
foreach ($media_attributes as $media_attribute) {
|
|
if (array_key_exists($media_attribute, $options)) {
|
|
$user[$media_attribute] = $options[$media_attribute];
|
|
}
|
|
}
|
|
}
|
|
}
|