1<?php declare(strict_types = 1); 2/* 3** Zabbix 4** Copyright (C) 2001-2021 Zabbix SIA 5** 6** This program is free software; you can redistribute it and/or modify 7** it under the terms of the GNU General Public License as published by 8** the Free Software Foundation; either version 2 of the License, or 9** (at your option) any later version. 10** 11** This program is distributed in the hope that it will be useful, 12** but WITHOUT ANY WARRANTY; without even the implied warranty of 13** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14** GNU General Public License for more details. 15** 16** You should have received a copy of the GNU General Public License 17** along with this program; if not, write to the Free Software 18** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19**/ 20 21 22/** 23 * Class containing methods for operations with user roles. 24 */ 25class CRole extends CApiService { 26 27 public const ACCESS_RULES = [ 28 'get' => ['min_user_type' => USER_TYPE_ZABBIX_USER], 29 'create' => ['min_user_type' => USER_TYPE_SUPER_ADMIN], 30 'update' => ['min_user_type' => USER_TYPE_SUPER_ADMIN], 31 'delete' => ['min_user_type' => USER_TYPE_SUPER_ADMIN] 32 ]; 33 34 /** 35 * @var string 36 */ 37 protected $tableName = 'role'; 38 39 /** 40 * @var string 41 */ 42 protected $tableAlias = 'r'; 43 44 /** 45 * @var array 46 */ 47 protected $sortColumns = ['roleid', 'name']; 48 49 /** 50 * List of rules output parameters. 51 * 52 * @var array 53 */ 54 protected $rules_params = [CRoleHelper::SECTION_UI, CRoleHelper::UI_DEFAULT_ACCESS, CRoleHelper::SECTION_MODULES, 55 CRoleHelper::MODULES_DEFAULT_ACCESS, CRoleHelper::API_ACCESS, CRoleHelper::API_MODE, CRoleHelper::SECTION_API, 56 CRoleHelper::SECTION_ACTIONS, CRoleHelper::ACTIONS_DEFAULT_ACCESS 57 ]; 58 59 /** 60 * List of user output parameters. 61 * 62 * @var array 63 */ 64 protected $user_params = ['userid', 'username', 'name', 'surname', 'url', 'autologin', 'autologout', 'lang', 65 'refresh', 'theme', 'attempt_failed', 'attempt_ip', 'attempt_clock', 'rows_per_page', 'timezone', 'roleid' 66 ]; 67 68 /** 69 * Rule value types. 70 */ 71 private const RULE_VALUE_TYPE_INT32 = 0; 72 private const RULE_VALUE_TYPE_STR = 1; 73 private const RULE_VALUE_TYPE_MODULE = 2; 74 75 /** 76 * Set of rule value types and database field names that store their values. 77 */ 78 public const RULE_VALUE_TYPES = [ 79 self::RULE_VALUE_TYPE_INT32 => 'value_int', 80 self::RULE_VALUE_TYPE_STR => 'value_str', 81 self::RULE_VALUE_TYPE_MODULE => 'value_moduleid' 82 ]; 83 84 /** 85 * @param array $options 86 * 87 * @throws APIException 88 * 89 * @return array|int 90 */ 91 public function get(array $options) { 92 $result = []; 93 94 $api_input_rules = ['type' => API_OBJECT, 'fields' => [ 95 // filter 96 'roleids' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'default' => null], 97 'filter' => ['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [ 98 'roleid' => ['type' => API_IDS, 'flags' => API_ALLOW_NULL | API_NORMALIZE], 99 'name' => ['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE], 100 'type' => ['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => implode(',', [USER_TYPE_ZABBIX_USER, USER_TYPE_ZABBIX_ADMIN, USER_TYPE_SUPER_ADMIN])], 101 'readonly' => ['type' => API_INTS32, 'flags' => API_ALLOW_NULL | API_NORMALIZE, 'in' => '0,1'] 102 ]], 103 'search' => ['type' => API_OBJECT, 'flags' => API_ALLOW_NULL, 'default' => null, 'fields' => [ 104 'name' => ['type' => API_STRINGS_UTF8, 'flags' => API_ALLOW_NULL | API_NORMALIZE] 105 ]], 106 'searchByAny' => ['type' => API_BOOLEAN, 'default' => false], 107 'startSearch' => ['type' => API_FLAG, 'default' => false], 108 'excludeSearch' => ['type' => API_FLAG, 'default' => false], 109 'searchWildcardsEnabled' => ['type' => API_BOOLEAN, 'default' => false], 110 // output 111 'output' => ['type' => API_OUTPUT, 'in' => implode(',', ['roleid', 'name', 'type', 'readonly']), 'default' => API_OUTPUT_EXTEND], 112 'countOutput' => ['type' => API_FLAG, 'default' => false], 113 'selectRules' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $this->rules_params), 'default' => null], 114 'selectUsers' => ['type' => API_OUTPUT, 'flags' => API_ALLOW_NULL, 'in' => implode(',', $this->user_params), 'default' => null], 115 // sort and limit 116 'sortfield' => ['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'in' => implode(',', $this->sortColumns), 'uniq' => true, 'default' => []], 117 'sortorder' => ['type' => API_SORTORDER, 'default' => []], 118 'limit' => ['type' => API_INT32, 'flags' => API_ALLOW_NULL, 'in' => '1:'.ZBX_MAX_INT32, 'default' => null], 119 // flags 120 'editable' => ['type' => API_BOOLEAN, 'default' => false], 121 'preservekeys' => ['type' => API_BOOLEAN, 'default' => false] 122 ]]; 123 if (!CApiInputValidator::validate($api_input_rules, $options, '/', $error)) { 124 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 125 } 126 127 $sql_parts = [ 128 'select' => ['role' => 'r.roleid'], 129 'from' => ['role' => 'role r'], 130 'where' => [], 131 'order' => [], 132 'limit' => null 133 ]; 134 135 // permission check + editable 136 if (self::$userData['type'] != USER_TYPE_SUPER_ADMIN) { 137 if ($options['editable']) { 138 return $options['countOutput'] ? 0 : []; 139 } 140 141 $sql_parts['from']['users'] = 'users u'; 142 $sql_parts['where']['u'] = 'r.roleid=u.roleid'; 143 $sql_parts['where'][] = 'u.userid='.self::$userData['userid']; 144 } 145 146 $output = $options['output']; 147 148 if ($options['selectRules'] !== null && is_array($options['output']) && !in_array('type', $options['output'])) { 149 $options['output'][] = 'type'; 150 } 151 152 // roleids 153 if ($options['roleids'] !== null) { 154 $sql_parts['where'][] = dbConditionInt('r.roleid', $options['roleids']); 155 } 156 157 // filter 158 if ($options['filter'] !== null) { 159 $this->dbFilter('role r', $options, $sql_parts); 160 } 161 162 // search 163 if ($options['search'] !== null) { 164 zbx_db_search('role r', $options, $sql_parts); 165 } 166 167 $sql_parts = $this->applyQueryOutputOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts); 168 $sql_parts = $this->applyQuerySortOptions($this->tableName(), $this->tableAlias(), $options, $sql_parts); 169 170 $res = DBselect(self::createSelectQueryFromParts($sql_parts), $options['limit']); 171 172 while ($db_role = DBfetch($res)) { 173 if ($options['countOutput']) { 174 return $db_role['rowscount']; 175 } 176 177 $result[$db_role['roleid']] = $db_role; 178 } 179 180 if ($result) { 181 $result = $this->addRelatedObjects($options, $result); 182 $result = $this->unsetExtraFields($result, ['roleid', 'type'], $output); 183 184 if (!$options['preservekeys']) { 185 $result = array_values($result); 186 } 187 } 188 189 return $result; 190 } 191 192 /** 193 * @param array $roles 194 * 195 * @return array 196 */ 197 public function create(array $roles): array { 198 $this->validateCreate($roles); 199 200 $ins_roles = []; 201 202 foreach ($roles as $role) { 203 unset($role['rules']); 204 $ins_roles[] = $role; 205 } 206 207 $roleids = DB::insert('role', $ins_roles); 208 209 foreach ($roles as $index => $role) { 210 $roles[$index]['roleid'] = $roleids[$index]; 211 } 212 213 $this->updateRules($roles, __FUNCTION__); 214 215 $this->addAuditBulk(AUDIT_ACTION_ADD, AUDIT_RESOURCE_USER_ROLE, $roles); 216 217 return ['roleids' => $roleids]; 218 } 219 220 /** 221 * @param array $roles 222 * 223 * @throws APIException if no permissions or the input is invalid. 224 */ 225 protected function validateCreate(array &$roles) { 226 $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [ 227 'name' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED | API_NOT_EMPTY, 'length' => DB::getFieldLength('role', 'name')], 228 'type' => ['type' => API_INT32, 'flags' => API_REQUIRED, 'in' => implode(',', [USER_TYPE_ZABBIX_USER, USER_TYPE_ZABBIX_ADMIN, USER_TYPE_SUPER_ADMIN])], 229 'rules' => ['type' => API_OBJECT, 'default' => [], 'fields' => [ 230 'ui' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [ 231 'name' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('role_rule', 'value_str')], 232 'status' => ['type' => API_INT32, 'in' => '0,1', 'default' => '1'] 233 ]], 234 'ui.default_access' => ['type' => API_INT32, 'in' => CRoleHelper::DEFAULT_ACCESS_DISABLED.','.CRoleHelper::DEFAULT_ACCESS_ENABLED, 'default' => CRoleHelper::DEFAULT_ACCESS_ENABLED], 235 'modules' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [ 236 'moduleid' => ['type' => API_ID, 'flags' => API_REQUIRED], 237 'status' => ['type' => API_INT32, 'in' => '0,1', 'default' => '1'] 238 ]], 239 'modules.default_access' => ['type' => API_INT32, 'in' => CRoleHelper::DEFAULT_ACCESS_DISABLED.','.CRoleHelper::DEFAULT_ACCESS_ENABLED, 'default' => CRoleHelper::DEFAULT_ACCESS_ENABLED], 240 'api.access' => ['type' => API_INT32, 'in' => CRoleHelper::API_ACCESS_DISABLED.','.CRoleHelper::API_ACCESS_ENABLED], 241 'api.mode' => ['type' => API_INT32, 'in' => CRoleHelper::API_MODE_DENY.','.CRoleHelper::API_MODE_ALLOW], 242 'api' => ['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'uniq' => true], 243 'actions' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [ 244 'name' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('role_rule', 'value_str')], 245 'status' => ['type' => API_INT32, 'in' => '0,1', 'default' => '1'] 246 ]], 247 'actions.default_access' => ['type' => API_INT32, 'in' => CRoleHelper::DEFAULT_ACCESS_DISABLED.','.CRoleHelper::DEFAULT_ACCESS_ENABLED, 'default' => CRoleHelper::DEFAULT_ACCESS_ENABLED] 248 ]] 249 ]]; 250 if (!CApiInputValidator::validate($api_input_rules, $roles, '/', $error)) { 251 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 252 } 253 254 $this->checkDuplicates(array_keys(array_flip(array_column($roles, 'name')))); 255 256 $db_modules = DBfetchArray(DBselect( 257 'SELECT moduleid'. 258 ' FROM module'. 259 ' WHERE status='.MODULE_STATUS_ENABLED 260 ), 'moduleid'); 261 $default_modules = []; 262 263 foreach ($db_modules as $db_module) { 264 $default_modules[] = ['moduleid' => $db_module['moduleid'], 'status' => 1]; 265 } 266 267 foreach ($roles as &$role) { 268 $role += ['rules' => []]; 269 $role['rules'] += [ 270 CRoleHelper::UI_DEFAULT_ACCESS => CRoleHelper::DEFAULT_ACCESS_ENABLED, 271 CRoleHelper::API_ACCESS => CRoleHelper::API_ACCESS_ENABLED, 272 CRoleHelper::API_MODE => CRoleHelper::API_MODE_DENY, 273 CRoleHelper::MODULES_DEFAULT_ACCESS => CRoleHelper::DEFAULT_ACCESS_ENABLED, 274 CRoleHelper::ACTIONS_DEFAULT_ACCESS => CRoleHelper::DEFAULT_ACCESS_ENABLED, 275 CRoleHelper::SECTION_MODULES => $default_modules 276 ]; 277 278 if (!array_key_exists(CRoleHelper::SECTION_UI, $role['rules'])) { 279 $skip = strlen(CRoleHelper::SECTION_UI.'.'); 280 281 foreach (CRoleHelper::getAllUiElements($role['type']) as $ui_element) { 282 $role['rules'][CRoleHelper::SECTION_UI][] = ['name' => substr($ui_element, $skip), 'status' => 1]; 283 } 284 } 285 286 if (!array_key_exists(CRoleHelper::SECTION_ACTIONS, $role['rules'])) { 287 $skip = strlen(CRoleHelper::SECTION_ACTIONS.'.'); 288 289 foreach (CRoleHelper::getAllActions($role['type']) as $action) { 290 $role['rules'][CRoleHelper::SECTION_ACTIONS][] = ['name' => substr($action, $skip), 'status' => 1]; 291 } 292 } 293 } 294 295 $this->checkRules($roles); 296 } 297 298 /** 299 * @param array $roles 300 * 301 * @return array 302 */ 303 public function update(array $roles): array { 304 $this->validateUpdate($roles, $db_roles); 305 306 $upd_roles = []; 307 308 foreach ($roles as $role) { 309 $db_role = $db_roles[$role['roleid']]; 310 311 $upd_role = []; 312 313 if (array_key_exists('name', $role) && $role['name'] !== $db_role['name']) { 314 $upd_role['name'] = $role['name']; 315 } 316 if (array_key_exists('type', $role) && $role['type'] !== $db_role['type']) { 317 $upd_role['type'] = $role['type']; 318 } 319 320 if ($upd_role) { 321 $upd_roles[] = [ 322 'values' => $upd_role, 323 'where' => ['roleid' => $role['roleid']] 324 ]; 325 } 326 } 327 328 if ($upd_roles) { 329 DB::update('role', $upd_roles); 330 } 331 332 $this->updateRules($roles, __FUNCTION__); 333 334 foreach ($db_roles as $db_roleid => $db_role) { 335 unset($db_roles[$db_roleid]['rules']); 336 } 337 338 $this->addAuditBulk(AUDIT_ACTION_UPDATE, AUDIT_RESOURCE_USER_ROLE, $roles, $db_roles); 339 340 return ['roleids' => array_column($roles, 'roleid')]; 341 } 342 343 /** 344 * @param array $roles 345 * @param array $db_roles 346 * 347 * @throws APIException if input is invalid. 348 */ 349 private function validateUpdate(array &$roles, ?array &$db_roles) { 350 $api_input_rules = ['type' => API_OBJECTS, 'flags' => API_NOT_EMPTY | API_NORMALIZE, 'uniq' => [['name']], 'fields' => [ 351 'roleid' => ['type' => API_ID, 'flags' => API_REQUIRED], 352 'name' => ['type' => API_STRING_UTF8, 'flags' => API_NOT_EMPTY, 'length' => DB::getFieldLength('role', 'name')], 353 'type' => ['type' => API_INT32, 'in' => implode(',', [USER_TYPE_ZABBIX_USER, USER_TYPE_ZABBIX_ADMIN, USER_TYPE_SUPER_ADMIN])], 354 'rules' => ['type' => API_OBJECT, 'fields' => [ 355 'ui' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [ 356 'name' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('role_rule', 'value_str')], 357 'status' => ['type' => API_INT32, 'in' => '0,1', 'default' => '1'] 358 ]], 359 'ui.default_access' => ['type' => API_INT32, 'in' => CRoleHelper::DEFAULT_ACCESS_DISABLED.','.CRoleHelper::DEFAULT_ACCESS_ENABLED], 360 'modules' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [ 361 'moduleid' => ['type' => API_ID, 'flags' => API_REQUIRED], 362 'status' => ['type' => API_INT32, 'in' => '0,1', 'default' => '1'] 363 ]], 364 'modules.default_access' => ['type' => API_INT32, 'in' => CRoleHelper::DEFAULT_ACCESS_DISABLED.','.CRoleHelper::DEFAULT_ACCESS_ENABLED], 365 'api.access' => ['type' => API_INT32, 'in' => CRoleHelper::API_ACCESS_DISABLED.','.CRoleHelper::API_ACCESS_ENABLED], 366 'api.mode' => ['type' => API_INT32, 'in' => CRoleHelper::API_MODE_DENY.','.CRoleHelper::API_MODE_ALLOW], 367 'api' => ['type' => API_STRINGS_UTF8, 'flags' => API_NORMALIZE, 'uniq' => true], 368 'actions' => ['type' => API_OBJECTS, 'flags' => API_NORMALIZE, 'fields' => [ 369 'name' => ['type' => API_STRING_UTF8, 'flags' => API_REQUIRED, 'length' => DB::getFieldLength('role_rule', 'value_str')], 370 'status' => ['type' => API_INT32, 'in' => '0,1', 'default' => '1'] 371 ]], 372 'actions.default_access' => ['type' => API_INT32, 'in' => CRoleHelper::DEFAULT_ACCESS_DISABLED.','.CRoleHelper::DEFAULT_ACCESS_ENABLED] 373 ]] 374 ]]; 375 if (!CApiInputValidator::validate($api_input_rules, $roles, '/', $error)) { 376 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 377 } 378 379 $db_roles = $this->get([ 380 'output' => ['roleid', 'name', 'type', 'readonly'], 381 'roleids' => array_column($roles, 'roleid'), 382 'selectRules' => [CRoleHelper::UI_DEFAULT_ACCESS], 383 'preservekeys' => true 384 ]); 385 $roles = $this->extendObjectsByKey($roles, $db_roles, 'roleid', ['name', 'type']); 386 387 if (array_diff(array_column($roles, 'roleid'), array_column($db_roles, 'roleid'))) { 388 self::exception(ZBX_API_ERROR_PERMISSIONS, _('No permissions to referred object or it does not exist!')); 389 } 390 391 $readonly = array_search(1, array_column($db_roles, 'readonly', 'name')); 392 393 if ($readonly !== false) { 394 self::exception(ZBX_API_ERROR_PERMISSIONS, _s('Cannot update readonly user role "%1$s".', $readonly)); 395 } 396 397 $role_type = array_column($roles, 'type', 'roleid'); 398 399 if (array_key_exists(self::$userData['roleid'], $role_type) 400 && $role_type[self::$userData['roleid']] != self::$userData['type']) { 401 self::exception(ZBX_API_ERROR_PERMISSIONS, _('User cannot change the user type of own role.')); 402 } 403 404 $names = array_diff(array_column($roles, 'name'), array_column($db_roles, 'name')); 405 406 if ($names) { 407 $this->checkDuplicates($names); 408 } 409 410 $this->checkRules($roles, $db_roles); 411 } 412 413 /** 414 * Check for duplicated user roles. 415 * 416 * @param array $names 417 * 418 * @throws APIException if user role already exists. 419 */ 420 private function checkDuplicates(array $names): void { 421 $db_roles = DB::select('role', [ 422 'output' => ['name'], 423 'filter' => ['name' => $names], 424 'limit' => 1 425 ]); 426 427 if ($db_roles) { 428 self::exception(ZBX_API_ERROR_PARAMETERS, 429 _s('User role with name "%1$s" already exists.', $db_roles[0]['name']) 430 ); 431 } 432 } 433 434 /** 435 * Check user role rules. 436 * 437 * @param array $roles 438 * @param array $db_roles 439 * 440 * @throws APIException if input is invalid. 441 */ 442 private function checkRules(array $roles, array $db_roles = []): void { 443 $moduleids = []; 444 445 foreach ($roles as $role) { 446 if (!array_key_exists('rules', $role)) { 447 continue; 448 } 449 450 if (array_key_exists(CRoleHelper::UI_DEFAULT_ACCESS, $role['rules']) 451 || array_key_exists(CRoleHelper::SECTION_UI, $role['rules'])) { 452 $ui_rules = []; 453 $default_access = CRoleHelper::DEFAULT_ACCESS_ENABLED; 454 455 if (array_key_exists(CRoleHelper::UI_DEFAULT_ACCESS, $role['rules'])) { 456 $default_access = $role['rules'][CRoleHelper::UI_DEFAULT_ACCESS]; 457 } 458 elseif (array_key_exists('roleid', $role)) { 459 $default_access = $db_roles[$role['roleid']]['rules'][CRoleHelper::UI_DEFAULT_ACCESS]; 460 } 461 462 $skip = strlen(CRoleHelper::SECTION_UI.'.'); 463 464 foreach (CRoleHelper::getAllUiElements((int) $role['type']) as $rule) { 465 $index = substr($rule, $skip); 466 $ui_rules[$index] = $default_access; 467 } 468 469 if (array_key_exists('rules', $role) && array_key_exists(CRoleHelper::SECTION_UI, $role['rules'])) { 470 foreach ($role['rules'][CRoleHelper::SECTION_UI] as $ui_rule) { 471 if (!array_key_exists($ui_rule['name'], $ui_rules)) { 472 self::exception(ZBX_API_ERROR_PARAMETERS, 473 _s('UI element "%1$s" is not available.', $ui_rule['name']) 474 ); 475 } 476 477 $ui_rules[$ui_rule['name']] = $ui_rule['status']; 478 } 479 } 480 481 if (!in_array(1, $ui_rules)) { 482 self::exception(ZBX_API_ERROR_PARAMETERS, _('At least one UI element must be checked.')); 483 } 484 } 485 486 if (array_key_exists(CRoleHelper::SECTION_MODULES, $role['rules'])) { 487 foreach ($role['rules'][CRoleHelper::SECTION_MODULES] as $module) { 488 $moduleids[$module['moduleid']] = true; 489 } 490 } 491 492 if (array_key_exists(CRoleHelper::SECTION_API, $role['rules'])) { 493 foreach ($role['rules'][CRoleHelper::SECTION_API] as $api_method) { 494 $this->validateApiMethod($api_method); 495 } 496 } 497 498 if (array_key_exists(CRoleHelper::SECTION_ACTIONS, $role['rules'])) { 499 foreach ($role['rules'][CRoleHelper::SECTION_ACTIONS] as $action) { 500 if (!in_array(sprintf('%s.%s', CRoleHelper::SECTION_ACTIONS, $action['name']), 501 CRoleHelper::getAllActions((int) $role['type']))) { 502 self::exception(ZBX_API_ERROR_PARAMETERS, 503 _s('Action "%1$s" is not available.', $action['name']) 504 ); 505 } 506 } 507 } 508 } 509 510 if ($moduleids) { 511 $moduleids = array_keys($moduleids); 512 513 $db_modules = DBfetchArrayAssoc(DBselect( 514 'SELECT moduleid'. 515 ' FROM module'. 516 ' WHERE '.dbConditionInt('moduleid', $moduleids). 517 ' AND status='.MODULE_STATUS_ENABLED 518 ), 'moduleid'); 519 520 foreach ($moduleids as $moduleid) { 521 if (!array_key_exists($moduleid, $db_modules)) { 522 self::exception(ZBX_API_ERROR_PARAMETERS, 523 _s('Module with ID "%1$s" is not available.', $moduleid) 524 ); 525 } 526 } 527 } 528 } 529 530 /** 531 * Checks if the given API method is valid. 532 * 533 * @param string $api_method 534 * 535 * @throws APIException if the input is invalid. 536 */ 537 private function validateApiMethod(string $api_method): void { 538 if ($api_method === CRoleHelper::API_WILDCARD || $api_method === CRoleHelper::API_WILDCARD_ALIAS) { 539 return; 540 } 541 542 if (!preg_match('/([a-z]+|\*)\.([a-z]+|\*)/', $api_method) 543 || (!in_array($api_method, CRoleHelper::getApiMethodMasks(USER_TYPE_SUPER_ADMIN)) 544 && !in_array($api_method, CRoleHelper::getApiMethods(USER_TYPE_SUPER_ADMIN)))) { 545 self::exception(ZBX_API_ERROR_PARAMETERS, _s('Invalid API method "%1$s".', $api_method)); 546 } 547 } 548 549 /** 550 * Update table "role_rule". Additionally check UI section for update operation. 551 * 552 * @param array $roles Array of roles. 553 * @param int $roles[<role>]['roleid'] Role id. 554 * @param int $roles[<role>]['type'] Role type. 555 * @param array $roles[<role>]['rules'] Array or role rules to be updated. 556 * @param string $method 557 */ 558 private function updateRules(array $roles, string $method): void { 559 $insert = []; 560 $update = []; 561 $delete = []; 562 $roles = array_column($roles, null, 'roleid'); 563 $db_roles_rules = []; 564 $is_update = ($method === 'update'); 565 566 if ($is_update) { 567 $db_rows = DB::select('role_rule', [ 568 'output' => ['role_ruleid', 'roleid', 'type', 'name', 'value_int', 'value_str', 'value_moduleid'], 569 'filter' => ['roleid' => array_keys($roles)] 570 ]); 571 572 // Move rules in database to $delete if it is not accessible anymore by role type. 573 foreach ($db_rows as $db_row) { 574 $role_type = (int) $roles[$db_row['roleid']]['type']; 575 $rule_name = $db_row['name']; 576 $section = CRoleHelper::getRuleSection($rule_name); 577 578 if ($section === CRoleHelper::SECTION_API && $rule_name !== CRoleHelper::API_ACCESS 579 && $rule_name !== CRoleHelper::API_MODE) { 580 $rule_name = (strpos($db_row['value_str'], CRoleHelper::API_WILDCARD) === false) 581 ? CRoleHelper::API_METHOD.$db_row['value_str'] 582 : $rule_name; 583 } 584 585 if (CRoleHelper::checkRuleAllowedByType($rule_name, $role_type)) { 586 $db_roles_rules[$db_row['roleid']][$db_row['role_ruleid']] = $db_row; 587 } 588 else { 589 $delete[] = $db_row['role_ruleid']; 590 } 591 } 592 } 593 594 $roles_rules = []; 595 $processed_sections = []; 596 597 foreach ($roles as $role) { 598 if (!array_key_exists('rules', $role) && $is_update) { 599 continue; 600 } 601 602 $default = [ 603 CRoleHelper::UI_DEFAULT_ACCESS => CRoleHelper::DEFAULT_ACCESS_ENABLED, 604 CRoleHelper::API_ACCESS => CRoleHelper::API_ACCESS_ENABLED, 605 CRoleHelper::API_MODE => CRoleHelper::API_MODE_DENY, 606 CRoleHelper::MODULES_DEFAULT_ACCESS => CRoleHelper::DEFAULT_ACCESS_ENABLED, 607 CRoleHelper::ACTIONS_DEFAULT_ACCESS => CRoleHelper::DEFAULT_ACCESS_ENABLED 608 ]; 609 610 if ($is_update) { 611 $db_role_rules = array_column($db_roles_rules[$role['roleid']], 'value_int', 'name'); 612 $default = array_intersect_key($db_role_rules, $default) + $default; 613 } 614 615 $rules = $role['rules'] + $default + [ 616 CRoleHelper::SECTION_UI => [], 617 CRoleHelper::SECTION_API => [], 618 CRoleHelper::SECTION_MODULES => [], 619 CRoleHelper::SECTION_ACTIONS => [] 620 ]; 621 $roleid = $role['roleid']; 622 623 // UI rules. 624 $default_access = $rules[CRoleHelper::UI_DEFAULT_ACCESS]; 625 $processed_sections[$roleid][CRoleHelper::SECTION_UI] = (bool) array_intersect_key($role['rules'], [ 626 CRoleHelper::UI_DEFAULT_ACCESS => '', 627 CRoleHelper::SECTION_UI => '' 628 ]); 629 $roles_rules[$roleid][] = [ 630 'type' => self::RULE_VALUE_TYPE_INT32, 631 'name' => CRoleHelper::UI_DEFAULT_ACCESS, 632 'value_int' => $default_access 633 ]; 634 635 foreach ($rules[CRoleHelper::SECTION_UI] as $rule) { 636 if ($rule['status'] != $default_access) { 637 $roles_rules[$roleid][] = [ 638 'type' => self::RULE_VALUE_TYPE_INT32, 639 'name' => sprintf('%s.%s', CRoleHelper::SECTION_UI, $rule['name']), 640 'value_int' => $rule['status'] 641 ]; 642 } 643 } 644 645 // API rules. 646 $api_access = $rules[CRoleHelper::API_ACCESS]; 647 $processed_sections[$roleid][CRoleHelper::SECTION_API] = (bool) array_intersect_key($role['rules'], [ 648 CRoleHelper::API_ACCESS => '', 649 CRoleHelper::SECTION_API => '' 650 ]); 651 $roles_rules[$roleid][] = [ 652 'type' => self::RULE_VALUE_TYPE_INT32, 653 'name' => CRoleHelper::API_ACCESS, 654 'value_int' => $api_access 655 ]; 656 657 if ($api_access) { 658 $status = $rules[CRoleHelper::API_MODE]; 659 660 $index = 0; 661 foreach ($rules[CRoleHelper::SECTION_API] as $method) { 662 $roles_rules[$roleid][] = [ 663 'type' => self::RULE_VALUE_TYPE_STR, 664 'name' => CRoleHelper::API_METHOD.$index, 665 'value_str' => $method 666 ]; 667 $index++; 668 } 669 670 if ($index) { 671 $roles_rules[$roleid][] = [ 672 'type' => self::RULE_VALUE_TYPE_INT32, 673 'name' => CRoleHelper::API_MODE, 674 'value_int' => $status 675 ]; 676 } 677 } 678 679 // Module rules. 680 $default_access = $rules[CRoleHelper::MODULES_DEFAULT_ACCESS]; 681 $processed_sections[$roleid][CRoleHelper::SECTION_MODULES] = (bool) array_intersect_key($role['rules'], [ 682 CRoleHelper::MODULES_DEFAULT_ACCESS => '', 683 CRoleHelper::SECTION_MODULES => '' 684 ]); 685 $roles_rules[$roleid][] = [ 686 'type' => self::RULE_VALUE_TYPE_INT32, 687 'name' => CRoleHelper::MODULES_DEFAULT_ACCESS, 688 'value_int' => $default_access 689 ]; 690 691 $index = 0; 692 foreach ($rules[CRoleHelper::SECTION_MODULES] as $module) { 693 if ($module['status'] != $default_access) { 694 $roles_rules[$roleid][] = [ 695 'type' => self::RULE_VALUE_TYPE_MODULE, 696 'name' => CRoleHelper::MODULES_MODULE.$index, 697 'value_moduleid' => $module['moduleid'] 698 ]; 699 $index++; 700 } 701 } 702 703 // Action rules. 704 $default_access = $rules[CRoleHelper::ACTIONS_DEFAULT_ACCESS]; 705 $processed_sections[$roleid][CRoleHelper::SECTION_ACTIONS] = (bool) array_intersect_key($role['rules'], [ 706 CRoleHelper::ACTIONS_DEFAULT_ACCESS => '', 707 CRoleHelper::SECTION_ACTIONS => '' 708 ]); 709 $roles_rules[$roleid][] = [ 710 'name' => CRoleHelper::ACTIONS_DEFAULT_ACCESS, 711 'value_int' => $default_access 712 ]; 713 714 foreach ($rules[CRoleHelper::SECTION_ACTIONS] as $rule) { 715 if ($rule['status'] != $default_access) { 716 $roles_rules[$roleid][] = [ 717 'name' => sprintf('%s.%s', CRoleHelper::SECTION_ACTIONS, $rule['name']), 718 'value_int' => $rule['status'] 719 ]; 720 } 721 } 722 } 723 724 // Fill rules to be inserted, updated or deleted. 725 foreach ($roles_rules as $roleid => $rules) { 726 if (!array_key_exists($roleid, $db_roles_rules)) { 727 foreach ($rules as $rule) { 728 $insert[] = $rule + ['roleid' => $roleid]; 729 } 730 731 continue; 732 } 733 734 $db_role_rules = array_column($db_roles_rules[$roleid], null, 'name'); 735 736 foreach ($rules as $rule) { 737 if (!array_key_exists($rule['name'], $db_role_rules)) { 738 $insert[] = $rule + ['roleid' => $roleid]; 739 740 continue; 741 } 742 743 $role_ruleid = $db_role_rules[$rule['name']]['role_ruleid']; 744 $type_index = self::RULE_VALUE_TYPES[$db_role_rules[$rule['name']]['type']]; 745 746 if (strval($db_role_rules[$rule['name']][$type_index]) != strval($rule[$type_index])) { 747 $update[] = [ 748 'values' => $rule, 749 'where' => ['role_ruleid' => $role_ruleid] 750 ]; 751 } 752 753 unset($db_roles_rules[$roleid][$role_ruleid]); 754 } 755 } 756 757 foreach ($db_roles_rules as $roleid => $db_role_rules) { 758 if (!array_key_exists($roleid, $processed_sections)) { 759 continue; 760 } 761 762 foreach ($db_role_rules as $db_rule) { 763 $section = substr($db_rule['name'], 0, strpos($db_rule['name'], '.')); 764 765 if ($processed_sections[$roleid][$section]) { 766 $delete[] = $db_rule['role_ruleid']; 767 } 768 } 769 } 770 771 if ($insert) { 772 DB::insert('role_rule', $insert); 773 } 774 775 if ($update) { 776 DB::update('role_rule', $update); 777 } 778 779 if ($delete) { 780 DB::delete('role_rule', ['role_ruleid' => $delete]); 781 } 782 } 783 784 /** 785 * @param array $roleids 786 * 787 * @return array 788 */ 789 public function delete(array $roleids): array { 790 $api_input_rules = ['type' => API_IDS, 'flags' => API_NOT_EMPTY, 'uniq' => true]; 791 if (!CApiInputValidator::validate($api_input_rules, $roleids, '/', $error)) { 792 self::exception(ZBX_API_ERROR_PARAMETERS, $error); 793 } 794 795 $db_roles = $this->get([ 796 'output' => ['roleid', 'name', 'readonly'], 797 'selectUsers' => ['userid'], 798 'roleids' => $roleids, 799 'preservekeys' => true 800 ]); 801 802 foreach ($roleids as $roleid) { 803 if (!array_key_exists($roleid, $db_roles)) { 804 self::exception(ZBX_API_ERROR_PERMISSIONS, 805 _('No permissions to referred object or it does not exist!') 806 ); 807 } 808 809 $db_role = $db_roles[$roleid]; 810 811 if ($db_role['readonly'] == 1) { 812 self::exception(ZBX_API_ERROR_PERMISSIONS, 813 _s('Cannot delete readonly user role "%1$s".', $db_role['name']) 814 ); 815 } 816 817 if ($db_role['users']) { 818 self::exception(ZBX_API_ERROR_PERMISSIONS, 819 _s('The role "%1$s" is assigned to at least one user and cannot be deleted.', $db_role['name']) 820 ); 821 } 822 } 823 824 DB::delete('role', ['roleid' => $roleids]); 825 826 $this->addAuditBulk(AUDIT_ACTION_DELETE, AUDIT_RESOURCE_USER_ROLE, $db_roles); 827 828 return ['roleids' => $roleids]; 829 } 830 831 protected function addRelatedObjects(array $options, array $result): array { 832 $roleids = array_keys($result); 833 834 // adding role rules 835 if ($options['selectRules'] !== null && $options['selectRules'] !== API_OUTPUT_COUNT) { 836 $options['selectRules'] = ($options['selectRules'] === API_OUTPUT_EXTEND) 837 ? $this->rules_params 838 : array_intersect($this->rules_params, $options['selectRules']); 839 840 $enabled_modules = in_array('modules', $options['selectRules']) 841 ? DBfetchArray(DBselect('SELECT moduleid FROM module WHERE status='.MODULE_STATUS_ENABLED)) 842 : []; 843 844 $db_rules = DBselect( 845 'SELECT roleid,type,name,value_int,value_str,value_moduleid'. 846 ' FROM role_rule'. 847 ' WHERE '.dbConditionInt('roleid', $roleids) 848 ); 849 850 $rules = []; 851 while ($db_rule = DBfetch($db_rules)) { 852 if (!array_key_exists($db_rule['roleid'], $rules)) { 853 $rules[$db_rule['roleid']] = [ 854 CRoleHelper::SECTION_UI => [], 855 CRoleHelper::UI_DEFAULT_ACCESS => (string) CRoleHelper::DEFAULT_ACCESS_ENABLED, 856 CRoleHelper::SECTION_MODULES => [], 857 CRoleHelper::MODULES_DEFAULT_ACCESS => (string) CRoleHelper::DEFAULT_ACCESS_ENABLED, 858 CRoleHelper::API_ACCESS => (string) CRoleHelper::API_ACCESS_ENABLED, 859 CRoleHelper::API_MODE => (string) CRoleHelper::API_MODE_DENY, 860 CRoleHelper::SECTION_API => [], 861 CRoleHelper::SECTION_ACTIONS => [], 862 CRoleHelper::ACTIONS_DEFAULT_ACCESS => (string) CRoleHelper::DEFAULT_ACCESS_ENABLED 863 ]; 864 } 865 866 $value = $db_rule[self::RULE_VALUE_TYPES[$db_rule['type']]]; 867 868 if (in_array($db_rule['name'], [CRoleHelper::UI_DEFAULT_ACCESS, CRoleHelper::MODULES_DEFAULT_ACCESS, 869 CRoleHelper::API_ACCESS, CRoleHelper::API_MODE, CRoleHelper::ACTIONS_DEFAULT_ACCESS])) { 870 $rules[$db_rule['roleid']][$db_rule['name']] = $value; 871 } 872 else { 873 [$key, $name] = explode('.', $db_rule['name'], 2); 874 $rules[$db_rule['roleid']][$key][$name] = $value; 875 } 876 } 877 878 foreach ($result as $roleid => $role) { 879 $role_rules = []; 880 881 foreach ($options['selectRules'] as $param) { 882 $role_rules[$param] = []; 883 884 switch ($param) { 885 case CRoleHelper::SECTION_UI: 886 foreach (CRoleHelper::getAllUiElements((int) $role['type']) as $ui_element) { 887 $ui_element = explode('.', $ui_element, 2)[1]; 888 $role_rules[$param][] = [ 889 'name' => $ui_element, 890 'status' => array_key_exists($ui_element, $rules[$roleid][$param]) 891 ? $rules[$roleid][$param][$ui_element] 892 : $rules[$roleid][CRoleHelper::UI_DEFAULT_ACCESS] 893 ]; 894 } 895 break; 896 897 case CRoleHelper::UI_DEFAULT_ACCESS: 898 case CRoleHelper::MODULES_DEFAULT_ACCESS: 899 case CRoleHelper::API_ACCESS: 900 case CRoleHelper::API_MODE: 901 case CRoleHelper::ACTIONS_DEFAULT_ACCESS: 902 $role_rules[$param] = $rules[$roleid][$param]; 903 break; 904 905 case CRoleHelper::SECTION_MODULES: 906 $modules = array_flip($rules[$roleid][$param]); 907 foreach ($enabled_modules as $module) { 908 $role_rules[$param][] = [ 909 'moduleid' => $module['moduleid'], 910 'status' => array_key_exists($module['moduleid'], $modules) 911 ? (string) (int) !$rules[$roleid][CRoleHelper::MODULES_DEFAULT_ACCESS] 912 : $rules[$roleid][CRoleHelper::MODULES_DEFAULT_ACCESS] 913 ]; 914 } 915 break; 916 917 case CRoleHelper::SECTION_API: 918 $role_rules[$param] = array_values($rules[$roleid][$param]); 919 break; 920 921 case CRoleHelper::SECTION_ACTIONS: 922 foreach (CRoleHelper::getAllActions((int) $role['type']) as $action) { 923 $action = explode('.', $action, 2)[1]; 924 $role_rules[$param][] = [ 925 'name' => $action, 926 'status' => array_key_exists($action, $rules[$roleid][$param]) 927 ? $rules[$roleid][$param][$action] 928 : $rules[$roleid][CRoleHelper::ACTIONS_DEFAULT_ACCESS] 929 ]; 930 } 931 } 932 } 933 934 $result[$roleid]['rules'] = $role_rules; 935 } 936 } 937 938 // adding users 939 if ($options['selectUsers'] !== null && $options['selectRules'] !== API_OUTPUT_COUNT) { 940 if ($options['selectUsers'] === API_OUTPUT_EXTEND) { 941 $options['selectUsers'] = $this->user_params; 942 } 943 944 if (in_array('roleid', $options['selectUsers'])) { 945 $roleid_requested = true; 946 } 947 else { 948 $roleid_requested = false; 949 $options['selectUsers'][] = 'roleid'; 950 } 951 952 $db_users = DBselect( 953 'SELECT '.implode(',', $options['selectUsers']). 954 ' FROM users'. 955 ' WHERE '.dbConditionInt('roleid', $roleids) 956 ); 957 958 foreach ($result as $roleid => $role) { 959 $result[$roleid]['users'] = []; 960 } 961 962 while ($db_user = DBfetch($db_users)) { 963 $roleid = $db_user['roleid']; 964 if (!$roleid_requested) { 965 unset($db_user['roleid']); 966 } 967 968 $result[$roleid]['users'][] = $db_user; 969 } 970 } 971 972 return $result; 973 } 974} 975