validateGet($options); if (array_key_exists('id', $options)) { $db_scim_group = DB::select('scim_group', [ 'output' => ['name'], 'scim_groupids' => $options['id'] ]); if (!$db_scim_group) { self::exception(ZBX_API_ERROR_NO_ENTITY, 'No permissions to referred object or it does not exist!'); } $users = $this->getUsersByGroupIds([$options['id']]); return [ 'id' => $options['id'], 'displayName' => $db_scim_group[0]['name'], 'users' => $users[$options['id']] ]; } if (array_key_exists('displayName', $options)) { $db_scim_group = DB::select('scim_group', [ 'output' => ['name', 'scim_groupid'], 'filter' => ['name' => $options['displayName']] ]); if (!$db_scim_group) { return []; } $users = $this->getUsersByGroupIds([$db_scim_group[0]['scim_groupid']]); return [ 'id' => $db_scim_group[0]['scim_groupid'], 'displayName' => $db_scim_group[0]['name'], 'users' => $users ]; } $db_scim_groups = DB::select('scim_group', [ 'output' => ['name'], 'preservekeys' => true ]); if ($db_scim_groups) { $groups_users = $this->getUsersByGroupIds(array_keys($db_scim_groups)); foreach ($groups_users as $groupid => $users) { $db_scim_groups[$groupid]['users'] = $users; } } return $db_scim_groups; } /** * @param array $options * * @throws APIException if input is invalid. */ private function validateGet(array &$options): void { $api_input_rules = ['type' => API_OBJECT, 'fields' => [ 'displayName' => ['type' => API_STRING_UTF8], 'id' => ['type' => API_ID], '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); } } /** * Create group, assign users to new group when 'members' is passed in request. * * @param array $options * @param string $options['displayName'] SCIM group name. * @param array $options['members'] Array with SCIM group members. * @param string $options['members'][]['value'] Userid. * * @return array Array with data for created group. */ public function post(array $options): array { $this->validatePost($options); $db_scim_groups = DB::select('scim_group', [ 'output' => ['scim_groupid'], 'filter' => ['name' => $options['displayName']] ]); if ($db_scim_groups) { $options['id'] = $db_scim_groups[0]['scim_groupid']; return $this->put($options); } [$scim_groupid] = DB::insert('scim_group', [['name' => $options['displayName']]]); if (!$scim_groupid) { self::exception(ZBX_API_ERROR_INTERNAL, 'Cannot create group '.$options['displayName'].'.'); } $group = [ 'id' => $scim_groupid, 'displayName' => $options['displayName'], 'users' => [] ]; if (array_key_exists('members', $options) && $options['members'] != []) { $userdirectoryid = CAuthenticationHelper::getSamlUserdirectoryidForScim(); $scim_group_members = array_column($options['members'], 'value'); $group['users'] = $this->verifyUserids($scim_group_members, $userdirectoryid); foreach ($scim_group_members as $memberid) { $user_group = DB::insert('user_scim_group', [[ 'userid' => $memberid, 'scim_groupid' => $scim_groupid ]]); if (!$user_group) { self::exception(ZBX_API_ERROR_INTERNAL, 'Cannot add user '.$memberid.' to group '.$options['displayName'].'.' ); } $this->updateProvisionedUserGroups($memberid, $userdirectoryid); } } return $group; } /** * @param array $options * * @throws APIException if input is invalid. */ 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], 'displayName' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY], 'members' => ['type' => API_OBJECTS, 'fields' => [ 'display' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY], 'value' => ['type' => API_ID, 'flags' => API_REQUIRED] ]] ]]; 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.'); } } /** * Receives new information on the SCIM group and its members. Updates 'user_scim_group' table, updates users' * user groups mapping based on the remaining SCIM groups and SAML settings. * * @param array $options * @param string $options['id'] SCIM group id. * @param array $options['members'] Array with SCIM group members. * @param string $options['members'][]['value'] Userid. * * @return array Array with data for updated group. */ public function put(array $options): array { $this->validatePut($options); $db_scim_groups = DB::select('scim_group', [ 'output' => ['name'], 'scim_groupids' => $options['id'] ]); if (!$db_scim_groups) { self::exception(ZBX_API_ERROR_NO_ENTITY, 'No permissions to referred object or it does not exist!'); } $db_scim_group = $db_scim_groups[0]; if ($options['displayName'] !== $db_scim_group['name']) { $scim_groupid = DB::update('scim_group', [ 'values' => ['name' => $options['displayName']], 'where' => ['scim_groupid' => $options['id']] ]); if (!$scim_groupid) { self::exception(ZBX_API_ERROR_INTERNAL, 'Cannot update group '.$db_scim_group['name'].' to group '.$options['displayName'].'.' ); } $db_scim_group['name'] = $options['displayName']; } $userdirectoryid = CAuthenticationHelper::getSamlUserdirectoryidForScim(); $scim_group_members = array_column($options['members'], 'value'); $this->verifyUserids($scim_group_members, $userdirectoryid); $db_scim_group_members = DB::select('user_scim_group', [ 'output' => ['userid'], 'filter' => ['scim_groupid' => $options['id']] ]); $users_to_add = array_diff($scim_group_members, array_column($db_scim_group_members, 'userid')); $users_to_remove = array_diff(array_column($db_scim_group_members, 'userid'), $scim_group_members); if($users_to_add) { foreach ($users_to_add as $userid) { $scim_user_group = DB::insert('user_scim_group', [[ 'userid' => $userid, 'scim_groupid' => $options['id'] ]]); if (!$scim_user_group) { self::exception(ZBX_API_ERROR_INTERNAL, 'Cannot add user '.$userid.' to group '.$options['displayName'].'.' ); } $this->updateProvisionedUserGroups($userid, $userdirectoryid); } } if ($users_to_remove) { DB::delete('user_scim_group', [ 'userid' => array_values($users_to_remove), 'scim_groupid' => $options['id'] ]); foreach ($users_to_remove as $userid) { $this->updateProvisionedUserGroups($userid, $userdirectoryid); } } $db_users = APIRPC::User()->get([ 'output' => ['userid', 'username'], 'userids' => $scim_group_members, 'filter' => ['userdirectoryid' => $userdirectoryid] ]); return [ 'id' => $options['id'], 'displayName' => $db_scim_group['name'], 'users' => $db_users ]; } /** * @param array $options * * @throws APIException if input is invalid. */ private function validatePut($options) { $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], 'displayName' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY], 'members' => ['type' => API_OBJECTS, 'flags' => API_REQUIRED, 'fields' => [ 'display' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY], 'value' => ['type' => API_ID, 'flags' => API_REQUIRED] ]] ]]; 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.'); } } /** * Receives new information on the SCIM group members. Updates 'user_scim_group' table, updates users' * user groups mapping based on the remaining SCIM groups and SAML settings. * * @param array $options * @param string $options['id'] SCIM group 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, only 'members' path is supported. * @param array $options['Operations'][]['value'] Array of values on which operation should be * performed. If operation is 'remove' this can be * omitted, in this case all members should be removed. * @param string $options['Operations'][]['value'][]['value'] User id on which operation should be performed. * * @return array Array with data for updated group. * * @throws APIException */ public function patch(array $options): array { $this->validatePatch($options); $db_scim_groups = DB::select('scim_group', [ 'output' => ['name'], 'scim_groupids' => $options['id'] ]); if (!$db_scim_groups) { self::exception(ZBX_API_ERROR_NO_ENTITY, 'No permissions to referred object or it does not exist!'); } $db_users = []; $db_users_delete = []; $new_userids = []; $del_userids = []; $do_replace = false; $userdirectoryid = CAuthenticationHelper::getSamlUserdirectoryidForScim(); foreach ($options['Operations'] as $operation) { if ($operation['path'] === 'displayName') { $scim_groupid = DB::update('scim_group', [ 'values' => ['name' => $operation['value']], 'where' => ['scim_groupid' => $options['id']] ]); if (!$scim_groupid) { self::exception(ZBX_API_ERROR_INTERNAL, 'Cannot update group '.$db_scim_groups[0]['name'].' to group '.$operation['value'].'.' ); } $db_scim_groups[0]['name'] = $operation['value']; } else if ($operation['path'] === 'members') { switch ($operation['op']) { case 'add': $new_userids = array_merge($new_userids, array_column($operation['value'], 'value')); break; case 'remove': if (!$do_replace) { if (array_key_exists('value', $operation)) { $del_userids = array_merge($del_userids, array_column($operation['value'], 'value')); } if (!$del_userids) { // Empty 'value' array for 'remove' operation should act as 'replace' operation. $do_replace = true; } } break; case 'replace': $new_userids = array_merge($new_userids, array_column($operation['value'], 'value')); $do_replace = true; break; } } } if ($new_userids || $del_userids) { $new_userids = array_diff($new_userids, $del_userids); if (!$do_replace && $new_userids) { $db_userids = DB::select('user_scim_group', [ 'output' => ['userid'], 'filter' => ['scim_groupid' => $options['id']] ]); $new_userids = array_diff($new_userids, array_column($db_userids, 'userid')); } $db_users = $this->verifyUserids($new_userids, $userdirectoryid); $db_users_delete = $this->verifyUserids($del_userids, $userdirectoryid); } if ($do_replace) { DB::delete('user_scim_group', ['scim_groupid' => $options['id']]); } else if ($del_userids) { DB::delete('user_scim_group', ['userid' => $del_userids, 'scim_groupid' => $options['id']]); } if ($new_userids) { $values = []; foreach ($new_userids as $userid) { $values[] = [ 'userid' => $userid, 'scim_groupid' => $options['id'] ]; } DB::insertBatch('user_scim_group', $values); } foreach (array_column(array_merge($db_users, $db_users_delete), 'userid') as $db_userid) { $this->updateProvisionedUserGroups($db_userid, $userdirectoryid); } return [ 'id' => $options['id'], 'displayName' => $db_scim_groups[0]['name'], 'users' => $db_users ]; } /** * @param array $options * * @throws APIException if input is invalid. */ private function validatePatch(array &$options): 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 | API_ALLOW_UNEXPECTED, 'fields' => [ 'path' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'in' => implode(',', ['members', 'externalId', 'displayName'])], 'op' => ['type' => API_MULTIPLE, 'rules' => [ ['if' => ['field' => 'path', 'in' => 'displayName'], 'type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'in' => implode(',', ['replace', 'Replace'])], ['else' => true, 'type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'in' => implode(',', ['add', 'remove', 'replace', 'Add', 'Remove', 'Replace'])] ]], 'value' => ['type' => API_MULTIPLE, 'rules' => [ ['if' => ['field' => 'path', 'in' => 'members'], 'type' => API_OBJECTS, 'flags' => API_NOT_EMPTY, 'fields' => [ 'value' => ['type' => API_ID] ]], ['else' => true, 'type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY] ]] ]] ]]; 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.'); } foreach ($options['Operations'] as &$operation) { $operation['op'] = strtolower($operation['op']); } } /** * Deletes SCIM group from 'scim_group' table. Deletes the users that belong to this group from 'user_scim_group' * table. Updates users' user groups mapping based on the remaining SCIM groups and SAML settings. * * @param array $options * @param string $options['id] SCIM group to be deleted. * * @return array Deleted group id. */ public function delete(array $options): array { $this->validateDelete($options); $db_scim_group_members = DB::select('user_scim_group', [ 'output' => ['userid'], 'filter' => ['scim_groupid' => $options['id']] ]); DB::delete('scim_group', ['scim_groupid' => $options['id']]); foreach (array_column($db_scim_group_members, 'userid') as $userid) { $this->updateProvisionedUserGroups($userid, CAuthenticationHelper::getSamlUserdirectoryidForScim()); } return [$options['id']]; } /** * @param array $options * * @throws APIException if the input is invalid. */ private function validateDelete(array $options): void { $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); } } /** * Based on SCIM group id, returns all the users that are included in this group. * * @param array $groupids SCIM groups' IDs. * * @return array Returns array where each group has its own array of users. Groupid is key, userid is key. * [][]['userid'] * [][]['username'] */ private function getUsersByGroupIds(array $groupids): array { $db_scim_groups_members = DB::select('user_scim_group', [ 'output' => ['userid', 'scim_groupid'], 'filter' => ['scim_groupid' => $groupids] ]); if (!$db_scim_groups_members) { return array_fill_keys($groupids, []); } $users = APIRPC::User()->get([ 'output' => ['userid', 'username'], 'userids' => array_column($db_scim_groups_members, 'userid'), 'preservekeys' => true ]); $users_groups = array_fill_keys($groupids, []); foreach ($groupids as $groupid) { foreach ($db_scim_groups_members as $scim_group_member) { if ($scim_group_member['scim_groupid'] == $groupid) { $users_groups[$groupid][$scim_group_member['userid']] = $users[$scim_group_member['userid']]; } } } return $users_groups; } /** * Checks what kind of SCIM groups user belongs to, checks the mapping of the groups in SAML settings and updates * user's user group and role based on this mapping. * * @param string $userid * @param array $userdirectoryid * * @return void */ private function updateProvisionedUserGroups(string $userid, string $userdirectoryid): void { $provisioning = CProvisioning::forUserDirectoryId($userdirectoryid); $user_scim_groupids = DB::select('user_scim_group', [ 'output' => ['scim_groupid'], 'filter' => ['userid' => $userid] ]); $user_scim_group_names = DB::select('scim_group', [ 'output' => ['name'], 'scim_groupids' => array_column($user_scim_groupids, 'scim_groupid') ]); $group_rights = $provisioning->getUserGroupsAndRole(array_column($user_scim_group_names, 'name')); APIRPC::User()->updateProvisionedUser([ 'userid' => $userid, 'roleid' => $group_rights['roleid'], 'usrgrps' => $group_rights['usrgrps'] ]); } /** * Verifies if provided users exist in the database. * * @param array $userids User ids. * @param string $userdirectoryid User directory id to which users belong to. * * @return array Returns array with users' id and username. * * @throws APIException */ private function verifyUserids(array $userids, string $userdirectoryid): array { $users = APIRPC::User()->get([ 'output' => ['userid', 'username'], 'userids' => $userids, 'filter' => ['userdirectoryid' => $userdirectoryid] ]); if (count($users) !== count($userids)) { self::exception(ZBX_API_ERROR_NO_ENTITY, 'No permissions to referred object or it does not exist!'); } return $users; } /** * Add user to specified list of SAML/SCIM groups. Creates new entries in 'scim_group' if group do not exists. * Remove user to scim group relation from 'user_scim_group' when no groups supplied but * do not removes scim_groups entry if user were last related to group user. * * @param array $saml_group_names Array of strings with SAML/SCIM groups names. * @param int $userid User id. */ public static function createScimGroupsFromSamlAttributes(array $saml_group_names, string $userid): void { if (!$saml_group_names) { DB::delete('user_scim_group', ['userid' => $userid]); return; } $db_scim_groups = DB::select('scim_group', [ 'output' => ['scim_groupid', 'name'], 'filter' => ['name' => $saml_group_names], 'preservekeys' => true ]); $groups_to_add = array_diff($saml_group_names, array_column($db_scim_groups, 'name')); $scim_groupids = array_column($db_scim_groups, 'scim_groupid'); if ($groups_to_add) { $insert = []; foreach ($groups_to_add as $group_to_add) { $insert[] = ['name' => $group_to_add]; } $db_newids = DB::insert('scim_group', $insert, true); $scim_groupids = array_merge($scim_groupids, $db_newids); } $db_users_scim_groupids = DB::select('user_scim_group', [ 'output' => ['scim_groupid'], 'filter' => ['userid' => $userid] ]); $db_users_scim_groupids = array_column($db_users_scim_groupids, 'scim_groupid'); $user_scim_groupids_delete = array_diff($db_users_scim_groupids, $scim_groupids); $user_scim_groupids_add = []; foreach(array_diff($scim_groupids, $db_users_scim_groupids) as $scim_groupid) { $user_scim_groupids_add[] = [ 'userid' => $userid, 'scim_groupid' => $scim_groupid ]; } if ($user_scim_groupids_add) { DB::insert('user_scim_group', $user_scim_groupids_add); } if ($user_scim_groupids_delete) { DB::delete('user_scim_group', [ 'userid' => $userid, 'scim_groupid' => $user_scim_groupids_delete ]); } } }