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]; } } } }