<?php declare(strict_types = 0); /* ** Copyright (C) 2001-2025 Zabbix SIA ** ** This program is free software: you can redistribute it and/or modify it under the terms of ** the GNU Affero General Public License as published by the Free Software Foundation, version 3. ** ** 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 Affero General Public License for more details. ** ** You should have received a copy of the GNU Affero General Public License along with this program. ** If not, see <https://www.gnu.org/licenses/>. **/ /** * Class containing methods for operations with authorization tokens. */ class CToken extends CApiService { public const ACCESS_RULES = [ 'create' => ['min_user_type' => USER_TYPE_ZABBIX_USER, 'action' => CRoleHelper::ACTIONS_MANAGE_API_TOKENS], 'delete' => ['min_user_type' => USER_TYPE_ZABBIX_USER, 'action' => CRoleHelper::ACTIONS_MANAGE_API_TOKENS], 'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER, 'action' => CRoleHelper::ACTIONS_MANAGE_API_TOKENS], 'update' => ['min_user_type' => USER_TYPE_ZABBIX_USER, 'action' => CRoleHelper::ACTIONS_MANAGE_API_TOKENS], 'generate' => ['min_user_type' => USER_TYPE_ZABBIX_USER, 'action' => CRoleHelper::ACTIONS_MANAGE_API_TOKENS] ]; protected $tableName = 'token'; protected $tableAlias = 't'; protected $sortColumns = ['tokenid', 'name', 'lastaccess', 'status', 'expires_at', 'created_at']; /** * @param array $options * * @throws APIException if the input is invalid. * * @return array|int */ public function get(array $options = []) { $token_fields = ['tokenid', 'name', 'description', 'userid', 'lastaccess', 'status', 'expires_at', 'created_at', 'creator_userid' ]; $api_input_rules = ['type' => API_OBJECT, 'fields' => [ // filter 'tokenids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null], 'userids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null], 'token' => ['type' => API_STRING_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'length' => 64, 'default' => null], 'valid_at' => ['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'default' => null], 'expired_at' => ['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'default' => null], 'filter' => ['type' => API_FILTER, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => ['tokenid', 'name', 'userid', 'lastaccess', 'status', 'expires_at', 'created_at', 'creator_userid']], 'search' => ['type' => API_FILTER, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => ['name', 'description']], 'searchByAny' => ['type' => API_BOOLEAN, 'default' => false], 'startSearch' => ['type' => API_FLAG, 'default' => false], 'excludeSearch' => ['type' => API_FLAG, 'default' => false], 'searchWildcardsEnabled' => ['type' => API_BOOLEAN, 'default' => false], // output 'output' => ['type' => API_OUTPUT, 'in' => implode(',', $token_fields), 'default' => API_OUTPUT_EXTEND], 'countOutput' => ['type' => API_FLAG, 'default' => false], // sort and limit 'sortfield' => ['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', $this->sortColumns), 'uniq' => true, 'default' => []], 'sortorder' => ['type' => API_SORTORDER, 'default' => []], 'limit' => ['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null], // flags 'preservekeys' => ['type' => API_BOOLEAN, 'default' => false] ]]; if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $sql_parts = [ 'select' => [], 'from' => [$this->tableName() => $this->tableName().' '.$this->tableAlias()], 'where' => [], 'order' => [], 'group' => [] ]; // Fix incorrect postgres query when sort is used together with count. if ($options['countOutput'] && $options['sortfield']) { $options['sortfield'] = []; } // Hides token field value from being shown. if (!$options['countOutput'] && $options['output'] === API_OUTPUT_EXTEND) { $options['output'] = $this->getTableSchema()['fields']; unset($options['output']['token']); $options['output'] = array_keys($options['output']); } // permissions if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) { $sql_parts['where'][] = dbConditionInt($this->tableAlias().'.userid', (array) self::$userData['userid']); } // tokenids if ($options['tokenids'] !== null) { $sql_parts['where'][] = dbConditionInt($this->tableAlias().'.tokenid', $options['tokenids']); } // userids if ($options['userids'] !== null) { $sql_parts['where'][] = dbConditionInt($this->tableAlias().'.userid', $options['userids']); } // token if ($options['token'] !== null) { $token = hash('sha512', $options['token']); $sql_parts['where'][] = dbConditionString($this->tableAlias().'.token', (array) $token); } // valid_at if ($options['valid_at'] !== null) { $sql_parts['where'][] = '('.$this->tableAlias().'.expires_at=0 OR '. $this->tableAlias().'.expires_at>'.$options['valid_at'].')'; } // expired_at if ($options['expired_at'] !== null) { $sql_parts['where'][] = '('.$this->tableAlias().'.expires_at!=0 AND '. $this->tableAlias().'.expires_at<='.$options['expired_at'].')'; } // filter if ($options['filter'] !== null) { $this->dbFilter($this->tableName().' '.$this->tableAlias(), $options, $sql_parts); } // search if ($options['search'] !== null) { zbx_db_search($this->tableName().' '.$this->tableAlias(), $options, $sql_parts); } $sql_parts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts); $sql_parts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts); $result = DBselect(self::createSelectQueryFromParts($sql_parts), $options['limit']); $db_tokens = []; while ($row = DBfetch($result)) { if ($options['countOutput']) { return $row['rowscount']; } $db_tokens[$row['tokenid']] = $row; } if (!$db_tokens) { return []; } if (!$options['preservekeys']) { $db_tokens = array_values($db_tokens); } return $this->unsetExtraFields($db_tokens, ['tokenid'], $options['output']); } /** * @param array $tokens * * @return array */ public function create(array $tokens): array { if ($this::$userData['username'] === ZBX_GUEST_USER) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.')); } $this->validateCreate($tokens); array_walk($tokens, function (&$token) { $token['created_at'] = time(); $token['creator_userid'] = static::$userData['userid']; }); $tokenids = DB::insert('token', $tokens); array_walk($tokens, function (&$token, $index) use ($tokenids) { $token['tokenid'] = $tokenids[$index]; }); self::addAuditLog(CAudit::ACTION_ADD, CAudit::RESOURCE_AUTH_TOKEN, $tokens); return ['tokenids' => $tokenids]; } /** * @param array $tokens * * @throws APIException if the input is invalid */ protected function validateCreate(array &$tokens): void { $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['userid', 'name']], 'fields' => [ 'name' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('token', 'name')], 'description' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('token', 'description')], 'userid' => ['type' => API_ID, 'default' => self::$userData['userid']], 'status' => ['type' => API_INT32, 'in' => implode(',', [ZBX_AUTH_TOKEN_ENABLED, ZBX_AUTH_TOKEN_DISABLED])], 'expires_at' => ['type' => API_INT32] ]]; if (!CApiInputValidator::validate($api_input_rules, $tokens, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $this->checkUsers($tokens); $this->checkDuplicateNames($tokens); } /** * Check if token objects contain correct userids. * * @param array $tokens * @param string $tokens['userid'] * * @throws APIException if user is not valid. */ protected function checkUsers(array $tokens): void { $userids = array_column($tokens, 'userid', 'userid'); if (array_key_exists(self::$userData['userid'], $userids)) { unset($userids[self::$userData['userid']]); } if ($userids) { if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('User with ID "%1$s" is not available.', key($userids))); } $db_users = API::User()->get([ 'output' => [], 'userids' => $userids, 'preservekeys' => true ]); foreach ($userids as $userid) { if (!array_key_exists($userid, $db_users)) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('User with ID "%1$s" is not available.', $userid)); } } } } /** * Check uniqueness of token name per user. * * @param array $tokens * @param string $tokens[]['tokenid'] (optional) Required when $db_tokens is not null. * @param string $tokens[]['userid'] * @param string $tokens[]['name'] (optional) * @param array|null $db_tokens * @param string $db_tokens[<tokenid>]['name'] * * * @throws APIException if token already exists. */ protected function checkDuplicateNames(array $tokens, ?array $db_tokens = null): void { $names_by_userid = []; foreach ($tokens as $token) { if (!array_key_exists('name', $token)) { continue; } if ($db_tokens === null || $token['name'] !== $db_tokens[$token['tokenid']]['name']) { $names_by_userid[$token['userid']][] = $token['name']; } } foreach ($names_by_userid as $userid => $names) { $duplicate = DBfetch(DBselect( 'SELECT t.userid,t.name'. ' FROM token t'. ' WHERE '.dbConditionId('t.userid', [$userid]). ' AND '.dbConditionString('t.name', $names), 1 )); if ($duplicate) { self::exception(ZBX_API_ERROR_PARAMETERS, _s('API token "%1$s" already exists for userid "%2$s".', $duplicate['name'], $userid )); } } } /** * @param array $tokens * * @return array */ public function update(array $tokens): array { if ($this::$userData['username'] === ZBX_GUEST_USER) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.')); } $this->validateUpdate($tokens, $db_tokens); $upd_tokens = []; foreach ($tokens as $token) { $db_token = $db_tokens[$token['tokenid']]; $upd_token = DB::getUpdatedValues('token', $token, $db_token); if ($upd_token) { $upd_tokens[] = [ 'values' => $upd_token, 'where' => ['tokenid' => $token['tokenid']] ]; } } if ($upd_tokens) { DB::update('token', $upd_tokens); self::addAuditLog(CAudit::ACTION_UPDATE, CAudit::RESOURCE_AUTH_TOKEN, $tokens, $db_tokens); } return ['tokenids' => array_column($tokens, 'tokenid')]; } /** * @param array $tokens * @param array $db_tokens * * @throws APIException if the input is invalid */ protected function validateUpdate(array &$tokens, ?array &$db_tokens = null): void { $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['tokenid']], 'fields' => [ 'tokenid' => ['type' => API_ID, 'flags' => API_REQUIRED], 'name' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('token', 'name')], 'description' => ['type' => API_STRING_UTF8, 'length' => DB::getFieldLength('token', 'description')], 'status' => ['type' => API_INT32, 'in' => implode(',', [ZBX_AUTH_TOKEN_ENABLED, ZBX_AUTH_TOKEN_DISABLED])], 'expires_at' => ['type' => API_INT32] ]]; if (!CApiInputValidator::validate($api_input_rules, $tokens, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $db_tokens = $this->get([ 'output' => ['tokenid', 'userid', 'name', 'description', 'status', 'expires_at'], 'tokenids' => array_column($tokens, 'tokenid', 'tokenid'), 'preservekeys' => true ]); if (count($db_tokens) != count($tokens)) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); } $tokens = $this->extendObjectsByKey($tokens, $db_tokens, 'tokenid', ['userid']); $api_input_rules = ['type' => API_OBJECTS, 'uniq' => [['userid', 'name']], 'fields' => [ 'userid' => ['type' => API_ID], 'name' => ['type' => API_STRING_UTF8] ]]; if (!CApiInputValidator::validateUniqueness($api_input_rules, $tokens, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $this->checkDuplicateNames($tokens, $db_tokens); } /** * @param array $tokenids * * @param array $tokenids */ public static function deleteForce(array $tokenids): void { if (!$tokenids) { return; } $db_tokens = DB::select('token', [ 'output' => ['tokenid', 'userid', 'name'], 'tokenids' => $tokenids ]); DB::delete('token', ['tokenid' => $tokenids]); self::addAuditLog(CAudit::ACTION_DELETE, CAudit::RESOURCE_AUTH_TOKEN, $db_tokens); } /** * @param array $tokenids * * @throws APIException if the input is invalid * * @return array */ public function delete(array $tokenids): array { if ($this::$userData['username'] === ZBX_GUEST_USER) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.')); } $api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true]; if (!CApiInputValidator::validate($api_input_rules, $tokenids, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $db_count = $this->get([ 'countOutput' => true, 'tokenids' => $tokenids ]); if ($db_count != count($tokenids)) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); } self::deleteForce($tokenids); return ['tokenids' => $tokenids]; } /** * Generates authentication token string for given API tokens. * * @param array $tokenids * * @throws APIException if the input is invalid * * @return array */ public function generate(array $tokenids): array { if ($this::$userData['username'] === ZBX_GUEST_USER) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('You do not have permission to perform this operation.')); } $api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true]; if (!CApiInputValidator::validate($api_input_rules, $tokenids, '/', $error)) { self::exception(ZBX_API_ERROR_PARAMETERS, $error); } $db_count = $this->get([ 'countOutput' => true, 'tokenids' => $tokenids ]); if ($db_count != count($tokenids)) { self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); } $db_tokens = DB::select('token', [ 'output' => ['tokenid', 'name', 'token', 'creator_userid'], 'tokenids' => $tokenids, 'preservekeys' => true ]); $tokens = []; $response = []; $upd_tokens = []; foreach ($tokenids as $tokenid) { $new_token = bin2hex(random_bytes(32)); $token = [ 'tokenid' => $tokenid, 'token' => hash('sha512', $new_token), 'creator_userid' => self::$userData['userid'] ]; $tokens[] = $token; $response[] = [ 'tokenid' => $tokenid, 'token' => $new_token ]; $upd_tokens[] = [ 'values' => DB::getUpdatedValues('token', $token, $db_tokens[$tokenid]), 'where' => ['tokenid' => $tokenid] ]; } DB::update('token', $upd_tokens); self::addAuditLog(CAudit::ACTION_UPDATE, CAudit::RESOURCE_AUTH_TOKEN, $tokens, $db_tokens); return $response; } }