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