TPermissionsManager::getLoadParameter()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * TPermissionsManager class file
5
 *
6
 * @author Brad Anderson <[email protected]>
7
 * @link https://github.com/pradosoft/prado
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 */
10
11
namespace Prado\Security\Permissions;
12
13
use Prado\Exceptions\TConfigurationException;
14
use Prado\Exceptions\TInvalidOperationException;
15
use Prado\Exceptions\TInvalidDataValueException;
16
use Prado\Prado;
17
use Prado\Security\TAuthorizationRule;
18
use Prado\Security\TAuthorizationRuleCollection;
19
use Prado\TApplication;
20
use Prado\TComponent;
21
use Prado\TPropertyValue;
22
use Prado\Util\TDbParameterModule;
23
use Prado\Xml\TXmlDocument;
24
use Prado\Xml\TXmlElement;
25
26
/**
27
 * TPermissionsManager class.
28
 *
29
 * TPermissionsManager handles Permissions authorization and Roll Based
30
 * Access Control (RBAC).  Each registered Permission is given a set of
31
 * {@see \Prado\Security\TAuthorizationRule}s.  The RBAC is based on roles
32
 * having children roles and permissions, with permissions being thought of
33
 * as special roles themselves.
34
 *
35
 * TPermissionsManager attaches {@see \Prado\Security\Permissions\TPermissionsBehavior} to all classes
36
 * that implement {@see \Prado\Security\Permissions\IPermissions}.  This is the main mechanism
37
 * by which application permissions are registered.
38
 *
39
 * The role hierarchy and permission rules are unique to each application.  The
40
 * permissions configuration is defined in the TPermissionsManager application
41
 * configuration or in a separate {@see PermissionsFile}. {@see \Prado\Security\Permissions\TPermissionsConfigurationBehavior}
42
 * enables a page configuration to have Permission Configurations as well.
43
 * A {@see \Prado\Util\TDbParameterModule} can be specified with {@see getDbParameter} for
44
 * loading dynamic roles and permissions.
45
 *
46
 * Module XML configurations (and similarly PermissionFile) follows the format, eg:
47
 * ```xml
48
 * <module id="permissions" class="Prado\Security\Permissions\TPermissionsManager" DefaultRoles="Default" SuperRoles="Administrator">
49
 *	<role name="Developer" children="all, param_shell_permission, cron" />
50
 *	<role name="Manager" children="editor, change_user_role_permission, cron_shell" />
51
 *	<role name="cron_shell" children="cron_add_task, cron_update_task, cron_remove_task" />
52
 *	<role name="cron" children="cron_shell, cron_manage_log, cron_add_task, cron_update_task, cron_remove_task" />
53
 *  <role name="Default" children="register_user, blog_read_posts, blog_comment">
54
 *	<permissionrule name="param_shell_permission" action="deny" users="*" roles="" verb="*" IPs="" />
55
 *	<permissionrule name="cron_shell" action="allow" users="*" roles="Developer,cron_shell,cron_manage_log" verb="*" IPs="" />
56
 *	<permissionrule name="register_user" action="allow" users="?" />
57
 *	<permissionrule name="register_user" action="allow" roles="Manager" />
58
 *	<permissionrule name="change_profile" action="deny" users="?" priority="0" />
59
 *	<permissionrule name="blog_update_posts" class="Prado\Security\Permissions\TUserOwnerRule" Priority="5" />
60
 *	<permissionrule name="cron" action="allow" users="admin, user1, user2" roles="*" verb="*" IPs="*"  />
61
 *	<permissionrule name="blog_*" action="allow" users="admin, user1, user2" roles="*" verb="*" IPs="*"  />
62
 *	<permissionrule name="*" action="deny" priority="1000" />
63
 * </module>
64
 * ```
65
 *
66
 * and in PHP the same file would follow the following format, eg:
67
 * ```php
68
 * 'modules' => [
69
 * 'permissions' => ['class' => 'Prado\Security\Permissions\TPermissionsManager',
70
 * 		'properties' => ['DefaultRoles' => 'Default', 'SuperRoles' => "Administrator"],
71
 *		'roles' => [
72
 *			'Developer' => ['all', 'param_shell_permission', 'cron'],
73
 *			'Manager' => ['editor', 'change_user_role_permission', 'cron_shell'],
74
 *			'cron_shell' => ['cron_add_task', 'cron_update_task', 'cron_remove_task'],
75
 *			'cron' => ['cron_shell', 'cron_manage_log', 'cron_add_task', 'cron_update_task', 'cron_remove_task'],
76
 *			'Default' => ['register_user', 'blog_read_posts', 'blog_comment'],
77
 *		],
78
 * 		'permissionRules' => [
79
 *			[name => 'param_shell_permission', 'action' => 'deny', 'users' => '*', roles => '*', 'verb' => '*', 'IPs' =>''],
80
 *			[name => 'cron_shell', 'action' => 'allow', 'users' => 'Developer,cron_shell,cron_manage_log', roles => 'cron_shell', 'verb' => '*', 'IPs' =>''],
81
 *			[name => 'register_user', 'action' => 'allow', 'users' => '?'],
82
 *			[name => 'register_user', 'action' => 'allow', 'roles' => 'Manager'],
83
 *			[name => 'change_profile', 'action' => 'deny', 'users' => '?', 'priority' => '0'],
84
 *			[name => 'blog_update_posts', 'class' => 'Prado\Security\Permissions\TUserOwnerRule', 'priority' => '5'],
85
 *			[name => 'cron', 'action' => 'allow', 'users' => 'admin, user1, user2'],
86
 *			[name => 'blog_*', 'action' => 'allow', 'users' => 'admin, user1, user2'],
87
 *			[name => '*', 'action' => 'deny', 'priority' => 1000]
88
 *		]
89
 * ]
90
 * ```
91
 *
92
 * In this example, "cron" is not a permission, but when used as a permission,
93
 * all children roles/permissions will receive the rule.  Permissions with children,
94
 * such as 'cron_shell' (above), will give all its children the rule as
95
 * well.
96
 *
97
 * A special role "All" is automatically created to contain all the permissions.
98
 * Specifying "all" as a child, is the same as specifying a role as a super role
99
 * via {@see setSuperRoles}.
100
 *
101
 * All users get the roles specified by {@see getDefaultRoles}.  This changes
102
 * the default Prado behavior for guest users having no roles.
103
 *
104
 * Intermediate roles, that are not defined in the user system, may be defined in
105
 * the hierarchy, in the above example the "cron" role is not defined by the system,
106
 * but is defined in the role hierarchy.
107
 *
108
 * Permission Rules can have multiple rules. they are
109
 * ordered by natural specified configuration order unless the rule property
110
 * {@see \Prado\Security\TAuthorizationRule::setPriority} is set.
111
 *
112
 * Permissions authorization rules may use the '*' or 'perm_*' to add the rules to all
113
 * matching permission names.  Anything before the * is matched as a permission.
114
 * This does not traverse the hierarchy roles matching the name, just the permissions
115
 * are matched for the TAuthorizationRule.
116
 *
117
 * A permission name must list itself as a role in TAuthorizationRule for the user to be
118
 * validated for that permission name for authorization.  This is handled automatically
119
 * by TPermissionManager with the {@see getAutoAllowWithPermission} property.
120
 * By default getAutoAllowWithPermission is true, and allows any user with
121
 * that permission in their hierarchy to allow access to the functionality.
122
 * This rule priority can be set with {@see getAutoRulePriority},
123
 * where the default is 5, and -thus- before user defined rules.
124
 *
125
 * The second automatic rules includes Modules have their own preset rules that can
126
 * be automatically added via {@see getAutoPresetRules}.  By default this
127
 * is true. These rules typically allow owners of the data to be permitted without having
128
 * a permission-role.  Preset rules can define their own priorities but those
129
 * without set priorities receive the priority from {@see getAutoRulePriority}.
130
 *
131
 * The third, and last, auto-Rule is the final {@see getAutoDenyAll DenyAll}
132
 * rule. This is the last rule that denies all by default.  The AutoDenyAll
133
 * gets its rule priority from {@see getAutoDenyAllPriority}.  By default,
134
 * deny all to all permissions is enabled and thus blocking all permissions.
135
 *
136
 * Recursive hierarchy is gracefully handled, in case of any loop structures.
137
 *
138
 * When TPermissionsManager is a module in your app, there are three permissions
139
 * to control user access to its function:
140
 *  - TPermissionsManager::PERM_PERMISSIONS_SHELL 'permissions_shell' enables the shell commands.
141
 *  - TPermissionsManager::PERM_PERMISSIONS_MANAGE_ROLES 'permissions_manage_roles' enables adding and removing roles and children.
142
 *  - TPermissionsManager::PERM_PERMISSIONS_MANAGE_RULES 'permissions_manage_rules' enables adding and removing rules for permissions and roles.
143
 *
144
 * The role and rule management functions only work when the TDbParameter Module is specified.
145
 * The following gives user "admin" and all users with "Administrators" role the
146
 * permission to access permissions shell and its full functionality:
147
 * ```php
148
 *	 <role name="permissions_shell" children="permissions_manage_roles, permissions_manage_rules" />
149
 *   <permissionrule name="permissions_shell" action="allow" users="admin" />
150
 *   <permissionrule name="permissions_shell" action="allow" roles="Administrators" />
151
 * ```php
152
 *
153
 * @author Brad Anderson <[email protected]>
154
 * @method bool dyRegisterShellAction($returnValue)
155
 * @method bool dyAddRoleChildren(bool $return, string $role, string[] $children)
156
 * @method bool dyRemoveRoleChildren(bool $return, string $role, string[] $children)
157
 * @method bool dyAddPermissionRule(bool $return, string $permission, \Prado\Security\TAuthorizationRule $rule)
158
 * @method bool dyRemovePermissionRule(bool $return, string $permission, \Prado\Security\TAuthorizationRule $rule)
159
 * @since 4.2.0
160
 */
161
class TPermissionsManager extends \Prado\TModule implements IPermissions
162
{
163
	public const PERMISSIONS_BEHAVIOR = 'permissions';
164
165
	public const USER_PERMISSIONS_BEHAVIOR = 'usercan';
166
167
	public const PERMISSIONS_CONFIG_BEHAVIOR = 'permissionsConfig';
168
169
	public const PERM_PERMISSIONS_SHELL = 'permissions_shell';
170
171
	public const PERM_PERMISSIONS_MANAGE_ROLES = 'permissions_manage_roles';
172
173
	public const PERM_PERMISSIONS_MANAGE_RULES = 'permissions_manage_rules';
174
175
	/** @var string[] roles that get all permissions, default [] */
176
	private $_superRoles;
177
178
	/** @var string[] Default roles to give all users, default [] */
179
	private $_defaultRoles;
180
181
	/** @var array<string, \Prado\Security\TAuthorizationRuleCollection> contains the rules for each permission */
182
	private $_permissionRules = [];
183
184
	/** @var array<string, string> contains the short descriptions for each permission */
185
	private $_descriptions = [];
186
187
	/** @var array<string, \Prado\Security\TAuthorizationRule[]> the rules to apply to newly registered Permissions */
188
	private $_autoRules = [];
189
190
	/** @var array<string, string[]> contains the hierarchy of roles and children roles/permissions */
191
	private $_hierarchy = [];
192
193
	/** @var bool is the module initialized */
194
	private $_initialized = false;
195
196
	/** @var string role hierarchy and permission rules information file */
197
	private $_permissionFile;
198
199
	/** @var numeric the priority of the Allow With Permission Rule, default 5 */
0 ignored issues
show
Bug introduced by
The type Prado\Security\Permissions\numeric was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
200
	private $_autoRulePriority = 5;
201
202
	/** @var bool add allow users with permission-role, default true  */
203
	private $_autoAllowWithPermission = true;
204
205
	/** @var bool add module rules, allows User's data, default true */
206
	private $_autoRulePresetRules = true;
207
208
	/** @var bool add Deny All rule to every permissions as the last rule, default true */
209
	private $_autoDenyAll = true;
210
211
	/** @var numeric the priority of the module Rule, usually these are Allow User As Owner, default 1000000 */
212
	private $_autoDenyAllPriority = 1000000;
213
214
	/** @var \Prado\Util\TDbParameterModule the database module providing runtime roles and rules */
215
	private $_dbParameter;
216
217
	/** @var numeric the priority of the module Rule, usually these are Allow User As Owner */
218
	private $_parameter = 'configuration:TPermissionsManager:runtime';
219
220
	/**
221
	 * @returtn ?TPermissionsManager The PermissionsManager for the application.
222
	 */
223
	public static function getManager(): ?TPermissionsManager
224
	{
225
		$app = Prado::getApplication();
226
		if ($app) {
227
			$modules = $app->getModulesByType(self::class);
228
			if (count($modules)) {
229
				return array_values($modules)[0];
230
			}
231
		}
232
		return null;
233
	}
234
235
	/**
236
	 * @param \Prado\Security\Permissions\TPermissionsManager $manager
237
	 * @return TPermissionEvent[] the dynamic events to have authorization
238
	 */
239
	public function getPermissions($manager)
240
	{
241
		return [
242
			new TPermissionEvent(static::PERM_PERMISSIONS_SHELL, 'Activates permissions shell commands.', 'dyRegisterShellAction'),
243
			new TPermissionEvent(static::PERM_PERMISSIONS_MANAGE_ROLES, 'Manages Db Permissions Role Children.', ['dyAddRoleChildren', 'dyRemoveRoleChildren']),
244
			new TPermissionEvent(static::PERM_PERMISSIONS_MANAGE_RULES, 'Manages Db Permissions Rules.', ['dyAddPermissionRule', 'dyRemovePermissionRule']),
245
		];
246
	}
247
248
	/**
249
	 * @param array|TXmlElement $config the application configuration
250
	 */
251
	public function init($config)
252
	{
253
		$app = $this->getApplication();
254
		if (is_string($this->_dbParameter)) {
0 ignored issues
show
introduced by
The condition is_string($this->_dbParameter) is always false.
Loading history...
255
			if (($dbParameter = $app->getModule($this->_dbParameter)) === null) {
256
				throw new TConfigurationException('permissions_dbparameter_nonexistent', $this->_dbParameter);
257
			}
258
			if (!($dbParameter instanceof TDbParameterModule)) {
259
				throw new TConfigurationException('permissions_dbparameter_invalid', $this->_dbParameter);
260
			}
261
			$this->_dbParameter = $dbParameter;
262
		}
263
264
		if ($this->_initialized) {
265
			throw new TInvalidOperationException('permissions_init_once');
266
		}
267
		$this->_initialized = true;
268
269
		$manager = \WeakReference::create($this);
270
		TComponent::attachClassBehavior(static::PERMISSIONS_BEHAVIOR, ['class' => TPermissionsBehavior::class, 'permissionsmanager' => $manager], IPermissions::class, -10);
0 ignored issues
show
Bug introduced by
array('class' => Prado\S...nsmanager' => $manager) of type array<string,WeakReference|string> is incompatible with the type object|string expected by parameter $behavior of Prado\TComponent::attachClassBehavior(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

270
		TComponent::attachClassBehavior(static::PERMISSIONS_BEHAVIOR, /** @scrutinizer ignore-type */ ['class' => TPermissionsBehavior::class, 'permissionsmanager' => $manager], IPermissions::class, -10);
Loading history...
Bug introduced by
-10 of type integer is incompatible with the type Prado\numeric|null expected by parameter $priority of Prado\TComponent::attachClassBehavior(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

270
		TComponent::attachClassBehavior(static::PERMISSIONS_BEHAVIOR, ['class' => TPermissionsBehavior::class, 'permissionsmanager' => $manager], IPermissions::class, /** @scrutinizer ignore-type */ -10);
Loading history...
271
		TComponent::attachClassBehavior(static::USER_PERMISSIONS_BEHAVIOR, ['class' => TUserPermissionsBehavior::class, 'permissionsmanager' => $manager], \Prado\Security\IUser::class, -10);
272
		TComponent::attachClassBehavior(static::PERMISSIONS_CONFIG_BEHAVIOR, ['class' => TPermissionsConfigurationBehavior::class, 'permissionsmanager' => $manager], \Prado\Web\Services\TPageConfiguration::class, -10);
273
274
		$this->loadPermissionsData($config);
275
		if ($this->_permissionFile !== null) {
276
			if ($this->getApplication()->getConfigurationType() == TApplication::CONFIG_TYPE_PHP) {
277
				$userFile = include $this->_permissionFile;
278
				$this->loadPermissionsData($userFile);
279
			} else {
280
				$dom = new TXmlDocument();
281
				$dom->loadFromFile($this->_permissionFile);
282
				$this->loadPermissionsData($dom);
283
			}
284
		}
285
		if ($this->_dbParameter) {
286
			$this->loadPermissionsData($this->_dbParameter->get($this->_parameter));
287
		}
288
289
		foreach (array_map('strtolower', $this->getSuperRoles() ?? []) as $role) {
290
			$this->_hierarchy[$role] = array_merge(['all'], $this->_hierarchy[$role] ?? []);
291
		}
292
293
		$app->attachEventHandler('onAuthenticationComplete', [$this, 'registerShellAction']);
294
295
		parent::init($config);
0 ignored issues
show
Bug introduced by
It seems like $config can also be of type array; however, parameter $config of Prado\TModule::init() does only seem to accept Prado\Xml\TXmlElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

295
		parent::init(/** @scrutinizer ignore-type */ $config);
Loading history...
296
	}
297
298
	/**
299
	 * Registers a permission name with description and preset rules.
300
	 * @param string $permissionName name of the permission
301
	 * @param string $description description of the permission
302
	 * @param null|\Prado\Security\TAuthorizationRule[] $rules
303
	 */
304
	public function registerPermission($permissionName, $description, $rules = null)
305
	{
306
		$permission = strtolower($permissionName);
307
		$this->_descriptions[$permission] = TPropertyValue::ensureString($description);
308
309
		if ($this->_autoDenyAll === true) {
310
			$this->_autoDenyAll = 2;
0 ignored issues
show
Documentation Bug introduced by
The property $_autoDenyAll was declared of type boolean, but 2 is of type integer. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
311
			$this->addPermissionRuleInternal('*', new TAuthorizationRule('deny', '*', '*', '*', '*', $this->_autoDenyAllPriority));
312
		}
313
314
		$this->_hierarchy['all'][] = $permission;
315
316
		if (!isset($this->_permissionRules[$permission])) {
317
			$this->_permissionRules[$permission] = new TAuthorizationRuleCollection();
318
		} else {
319
			throw new TInvalidOperationException('permissions_duplicate_permission', $permissionName);
320
		}
321
		if ($this->_autoAllowWithPermission) {
322
			$this->_permissionRules[$permission]->add(new TAuthorizationRule('allow', '*', $permission, '*', '*', $this->_autoRulePriority));
323
		}
324
		if ($this->_autoRulePresetRules && $rules) {
325
			if (!is_array($rules)) {
0 ignored issues
show
introduced by
The condition is_array($rules) is always true.
Loading history...
326
				$rules = [$rules];
327
			}
328
			foreach ($rules as $rule) {
329
				$this->_permissionRules[$permission]->add($rule, is_numeric($p = $rule->getPriority()) ? $p : $this->_autoRulePriority);
330
			}
331
		}
332
		foreach ($this->_autoRules as $rulePerm => $rules) {
333
			$pos = strpos($rulePerm, '*');
334
			if (($pos !== false && strncmp($permission, $rulePerm, $pos) === 0) || $this->isInHierarchy($rulePerm, $permission)) {
0 ignored issues
show
Bug introduced by
$rulePerm of type string is incompatible with the type string[] expected by parameter $roles of Prado\Security\Permissio...anager::isInHierarchy(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

334
			if (($pos !== false && strncmp($permission, $rulePerm, $pos) === 0) || $this->isInHierarchy(/** @scrutinizer ignore-type */ $rulePerm, $permission)) {
Loading history...
335
				$this->_permissionRules[$permission]->mergeWith($rules);
336
				if ($rulePerm === $permission) {
337
					unset($this->_autoRules[$rulePerm]);
338
				}
339
			}
340
		}
341
	}
342
343
	/**
344
	 * gets the short description of the permission
345
	 * @param string $permissionName name of the permission
346
	 * @return string short description of the permission
347
	 */
348
	public function getPermissionDescription($permissionName)
349
	{
350
		return $this->_descriptions[strtolower($permissionName)];
351
	}
352
353
	/**
354
	 * Loads the roles, children, and permission rules.
355
	 * @param array|\Prado\Xml\TXmlElement $config configurations to parse
356
	 */
357
	public function loadPermissionsData($config)
358
	{
359
		$isXml = false;
360
		if (!$config) {
361
			return;
362
		}
363
		$permissions = $roles = [];
364
		if ($config instanceof TXmlElement) {
365
			$isXml = true;
366
			$roles = $config->getElementsByTagName('role');
367
			$permissions = $config->getElementsByTagName('permissionrule');
368
		} elseif (is_array($config)) {
0 ignored issues
show
introduced by
The condition is_array($config) is always true.
Loading history...
369
			$roles = $config['roles'] ?? [];
370
			$permissions = $config['permissionrules'] ?? [];
371
		}
372
		foreach ($roles as $role => $properties) {
373
			if ($isXml) {
374
				$properties = array_change_key_case($properties->getAttributes()->toArray());
375
				$role = $properties['name'] ?? '';
376
				$children = array_map('trim', explode(',', $properties['children'] ?? ''));
377
			} else {
378
				$children = $properties;
379
				if (is_string($children)) {
380
					$children = array_map('trim', explode(',', $children));
381
				}
382
				if (!is_array($children)) {
383
					throw new TConfigurationException('permissions_role_children_invalid', $role, is_object($children) ? $children::class : $children);
384
				}
385
			}
386
387
			$role = strtolower($role);
388
			$children = array_map('strtolower', array_filter($children));
389
390
			$this->_hierarchy[$role] = array_merge($this->_hierarchy[$role] ?? [], $children);
391
		}
392
		foreach ($permissions as $name => $properties) {
393
			if ($isXml) {
394
				$properties = array_change_key_case($properties->getAttributes()->toArray());
395
			} else {
396
				if (!is_array($properties)) {
397
					throw new TConfigurationException('permissions_rule_invalid', $name);
398
				}
399
			}
400
			if (is_numeric($name) && (!isset($properties[0]) || !$properties[0] instanceof TAuthorizationRule)) {
401
				$name = strtolower($properties['name'] ?? '');
402
				if (!$name) {
403
					throw new TConfigurationException('permissions_rules_require_name');
404
				}
405
				$class = $properties['class'] ?? TAuthorizationRule::class;
406
				$action = $properties['action'] ?? '';
407
				$users = $properties['users'] ?? '';
408
				$roles = $properties['roles'] ?? '';
409
				$verb = $properties['verb'] ?? '';
410
				$ips = $properties['ips'] ?? '';
411
				$priority = $properties['priority'] ?? '';
412
413
				$rule = new $class($action, $users, $roles, $verb, $ips, $priority);
414
			} else {
415
				$rule = $properties;
416
			}
417
			$this->addPermissionRuleInternal($name, $rule);
418
		}
419
	}
420
421
	/**
422
	 * Adds a permission rule to a permission name. Names can contain the '*' character
423
	 * and every permission with a matching name before the '*' will get the rule
424
	 * @param string $name Permission name
425
	 * @param \Prado\Security\TAuthorizationRule|\Prado\Security\TAuthorizationRule[] $rule
426
	 */
427
	protected function addPermissionRuleInternal($name, $rule)
428
	{
429
		if (!is_array($rule)) {
430
			$rule = [$rule];
431
		}
432
		if (($pos = strpos($name, '*')) !== false) {
433
			foreach ($this->_permissionRules as $perm => $rules) {
434
				if (strncmp($perm, $name, $pos) === 0) {
435
					$rules->mergeWith($rule);
436
				}
437
			}
438
			$this->_autoRules[$name] = array_merge($this->_autoRules[$name] ?? [], $rule);
439
		} elseif (isset($this->_permissionRules[$name])) {
440
			$this->_permissionRules[$name]->mergeWith($rule);
441
		} else {
442
			$this->_autoRules[$name] = array_merge($this->_autoRules[$name] ?? [], $rule);
443
		}
444
		if (isset($this->_hierarchy[$name])) {
445
			//Push the rule down the hierarchy to any children permissions.
446
			$set = [$name => true];
447
			$hierarchy = $this->_hierarchy[$name];
448
			while (count($hierarchy)) {
449
				$role = array_pop($hierarchy);
450
				if (!isset($set[$role])) { // stop recursive hierarchy and duplicate permissions
451
					$set[$role] = true;
452
					if (isset($this->_permissionRules[$role])) {
453
						$this->_permissionRules[$role]->mergeWith($rule);
454
					}
455
					if (isset($this->_hierarchy[$role])) {
456
						$hierarchy = array_merge($this->_hierarchy[$role], $hierarchy);
457
					}
458
				}
459
			}
460
		}
461
	}
462
463
	/**
464
	 * Removes a permission rule from a permission name.
465
	 * @param string $name
466
	 * @param \Prado\Security\TAuthorizationRule $rule
467
	 */
468
	protected function removePermissionRuleInternal($name, $rule)
469
	{
470
		if (($pos = strpos($name, '*')) !== false) {
471
			foreach ($this->_permissionRules as $perm => $rules) {
472
				if (strncmp($perm, $name, $pos) === 0) {
473
					$rules->remove($rule);
474
				}
475
			}
476
		} elseif (isset($this->_permissionRules[$name])) {
477
			$this->_permissionRules[$name]->remove($rule);
478
		}
479
		if (isset($this->_hierarchy[$name])) {
480
			//Push the rule down the hierarchy to any children permissions.
481
			$set = [$name => true];
482
			$hierarchy = $this->_hierarchy[$name];
483
			while (count($hierarchy)) {
484
				$role = array_pop($hierarchy);
485
				if (!isset($set[$role])) { // stop recursive hierarchy and duplicate permissions
486
					$set[$role] = true;
487
					if (isset($this->_permissionRules[$role])) {
488
						$this->_permissionRules[$role]->remove($rule);
489
					}
490
					if (isset($this->_hierarchy[$role])) {
491
						$hierarchy = array_merge($this->_hierarchy[$role], $hierarchy);
492
					}
493
				}
494
			}
495
		}
496
	}
497
498
	/**
499
	 * @param object $sender sender of this event handler
500
	 * @param null|mixed $param parameter for the event
501
	 */
502
	public function registerShellAction($sender, $param)
0 ignored issues
show
Unused Code introduced by
The parameter $param is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

502
	public function registerShellAction($sender, /** @scrutinizer ignore-unused */ $param)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $sender is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

502
	public function registerShellAction(/** @scrutinizer ignore-unused */ $sender, $param)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
503
	{
504
		if ($this->dyRegisterShellAction(false) !== true && ($app = $this->getApplication()) instanceof \Prado\Shell\TShellApplication) {
505
			$app->addShellActionClass(['class' => TPermissionsAction::class, 'PermissionsManager' => $this]);
0 ignored issues
show
Bug introduced by
array('class' => Prado\S...sionsManager' => $this) of type array<string,Prado\Secur...missionsManager|string> is incompatible with the type string expected by parameter $class of Prado\Shell\TShellApplic...::addShellActionClass(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

505
			$app->addShellActionClass(/** @scrutinizer ignore-type */ ['class' => TPermissionsAction::class, 'PermissionsManager' => $this]);
Loading history...
506
		}
507
	}
508
509
	/**
510
	 * checks if the $permission is in the $roles hierarchy.
511
	 * @param string[] $roles the roles to check the permission
512
	 * @param string $permission the permission-role being checked for in the hierarchy
513
	 * @param array<string, bool> &$checked the roles already checked
514
	 */
515
	public function isInHierarchy($roles, $permission, &$checked = [])
516
	{
517
		if (!$roles) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $roles of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
518
			return false;
519
		}
520
		if (!$checked) {
521
			if (!is_array($roles)) {
0 ignored issues
show
introduced by
The condition is_array($roles) is always true.
Loading history...
522
				$roles = array_filter(array_map('trim', explode(',', $roles)));
523
			}
524
			$roles = array_map('strtolower', $roles);
525
			$permission = strtolower($permission);
526
		}
527
		if (in_array($permission, $roles)) {
528
			return true;
529
		}
530
		foreach ($roles as $role) {
531
			if (!isset($checked[$role])) {
532
				$checked[$role] = true;
533
				if (isset($this->_hierarchy[$role]) && $this->isInHierarchy($this->_hierarchy[$role], $permission, $checked)) {
534
					return true;
535
				}
536
			}
537
		}
538
		return false;
539
	}
540
541
	/**
542
	 * Get the roles that are runtime from the database
543
	 * @return array<string, string[]> roles and children from the database
544
	 */
545
	public function getDbConfigRoles()
546
	{
547
		if (!$this->_dbParameter || !$this->_parameter) {
548
			return [];
549
		}
550
		$runtimeData = $this->_dbParameter->get($this->_parameter) ?? [];
551
		return $runtimeData['roles'] ?? [];
552
	}
553
554
	/**
555
	 * Get the permission rules that are runtime from the database
556
	 * @return array<string, \Prado\Security\TAuthorizationRule[]>
557
	 */
558
	public function getDbConfigPermissionRules()
559
	{
560
		if (!$this->_dbParameter || !$this->_parameter) {
561
			return [];
562
		}
563
		$runtimeData = $this->_dbParameter->get($this->_parameter) ?? [];
564
		return $runtimeData['permissionrules'] ?? [];
565
	}
566
567
	/**
568
	 * This adds children to a role within the runtime context.  The children
569
	 * can be a single comma separated string.
570
	 * @param string $role the role to add children
571
	 * @param string|string[] $children the children to add to the role
572
	 * @throws TInvalidDataValueException when children is not an array
573
	 * @return bool was the method successful
574
	 */
575
	public function addRoleChildren($role, $children)
576
	{
577
		if ($this->dyAddRoleChildren(false, $role, $children) === true || !$this->_dbParameter) {
0 ignored issues
show
Bug introduced by
It seems like $children can also be of type string; however, parameter $children of Prado\Security\Permissio...er::dyAddRoleChildren() does only seem to accept string[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

577
		if ($this->dyAddRoleChildren(false, $role, /** @scrutinizer ignore-type */ $children) === true || !$this->_dbParameter) {
Loading history...
578
			return false;
579
		}
580
		if (is_string($children)) {
581
			$children = array_map('trim', explode(',', $children));
582
		} elseif (!is_array($children)) {
0 ignored issues
show
introduced by
The condition is_array($children) is always true.
Loading history...
583
			throw new TInvalidDataValueException('permissions_children_invalid', is_object($children) ? $children::class : $children);
584
		}
585
		$role = strtolower($role);
586
		$children = array_map('strtolower', array_filter($children));
587
		$this->_hierarchy[$role] = array_merge($this->_hierarchy[$role] ?? [], $children);
588
589
		$runtimeData = $this->_dbParameter->get($this->_parameter) ?? [];
590
		$runtimeData['roles'] ??= [];
591
		$runtimeData['roles'][$role] = array_unique(array_merge($runtimeData['roles'][$role] ?? [], $children));
592
		$this->_dbParameter->set($this->_parameter, $runtimeData);
593
594
		return true;
595
	}
596
597
	/**
598
	 * This removes children from a role within the runtime context.  The children
599
	 * can be a single comma separated string.
600
	 * @param string $role the role to add children
601
	 * @param string|string[] $children the children to add to the role
602
	 * @throws TInvalidDataValueException when children is not an array
603
	 * @return bool was the method successful
604
	 */
605
	public function removeRoleChildren($role, $children)
606
	{
607
		if ($this->dyRemoveRoleChildren(false, $role, $children) === true || !$this->_dbParameter) {
0 ignored issues
show
Bug introduced by
It seems like $children can also be of type string; however, parameter $children of Prado\Security\Permissio...:dyRemoveRoleChildren() does only seem to accept string[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

607
		if ($this->dyRemoveRoleChildren(false, $role, /** @scrutinizer ignore-type */ $children) === true || !$this->_dbParameter) {
Loading history...
608
			return false;
609
		}
610
		if (is_string($children)) {
611
			$children = array_map('trim', explode(',', $children));
612
		} elseif (!is_array($children)) {
0 ignored issues
show
introduced by
The condition is_array($children) is always true.
Loading history...
613
			throw new TInvalidDataValueException('permissions_children_invalid', is_object($children) ? $children::class : $children);
614
		}
615
		$role = strtolower($role);
616
		$children = array_map('strtolower', array_filter($children));
617
		$this->_hierarchy[$role] = array_values(array_diff($this->_hierarchy[$role] ?? [], $children));
618
		if (!$this->_hierarchy[$role]) {
619
			unset($this->_hierarchy[$role]);
620
		}
621
622
		$runtimeData = $this->_dbParameter->get($this->_parameter) ?? [];
623
		$runtimeData['roles'][$role] = array_values(array_diff($runtimeData['roles'][$role] ?? [], $children));
624
		if (!$runtimeData['roles'][$role]) {
625
			unset($runtimeData['roles'][$role]);
626
		}
627
		$this->_dbParameter->set($this->_parameter, $runtimeData);
628
		return true;
629
	}
630
631
	/**
632
	 * This method adds permission rules with in the runtime context.
633
	 * @param string $permission
634
	 * @param \Prado\Security\TAuthorizationRule $rule
635
	 * @return bool was the method successful
636
	 */
637
	public function addPermissionRule($permission, $rule)
638
	{
639
		$permission = strtolower($permission);
640
641
		if ($this->dyAddPermissionRule(false, $permission, $rule) === true || !$this->_dbParameter) {
642
			return false;
643
		}
644
		$this->addPermissionRuleInternal($permission, $rule);
645
646
		$runtimeData = $this->_dbParameter->get($this->_parameter) ?? [];
647
		$runtimeData['permissionrules'] ??= [];
648
		$runtimeData['permissionrules'][$permission][] = $rule;
649
		$this->_dbParameter->set($this->_parameter, $runtimeData);
650
651
		return true;
652
	}
653
654
	/**
655
	 * This method removes permission rules with in the runtime context.
656
	 * @param string $permission a permission or role to remove the rule from
657
	 * @param \Prado\Security\TAuthorizationRule $rule
658
	 * @return bool was the method successful
659
	 */
660
	public function removePermissionRule($permission, $rule)
661
	{
662
		$permission = strtolower($permission);
663
664
		if ($this->dyRemovePermissionRule(false, $permission, $rule) === true || !$this->_dbParameter) {
665
			return false;
666
		}
667
668
		$this->removePermissionRuleInternal($permission, $rule);
669
670
		$runtimeData = $this->_dbParameter->get($this->_parameter) ?? [];
671
		$runtimeData['permissionrules'] ??= [];
672
673
		if (($index = array_search($rule, $runtimeData['permissionrules'][$permission] ?? [], true)) === false) {
674
			return false;
675
		}
676
		unset($runtimeData['permissionrules'][$permission][$index]);
677
		if (!$runtimeData['permissionrules'][$permission]) {
678
			unset($runtimeData['permissionrules'][$permission]);
679
		} else {
680
			$runtimeData['permissionrules'][$permission] = array_values($runtimeData['permissionrules'][$permission]);
681
		}
682
		$this->_dbParameter->set($this->_parameter, $runtimeData);
683
684
		return true;
685
	}
686
687
	/**
688
	 * Gets all the roles in the hierarchy, though may not be valid roles in the application.
689
	 * @return string[] the roles in the hierarchy.
690
	 */
691
	public function getHierarchyRoles()
692
	{
693
		return array_keys($this->_hierarchy);
694
	}
695
696
	/**
697
	 * Gets the children for a specific role in the hierarchy.
698
	 * @param string $role the role to return its children
699
	 * @return null|string[] the children of a specific role.
700
	 */
701
	public function getHierarchyRoleChildren($role)
702
	{
703
		if (!$role) {
704
			return $this->_hierarchy;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_hierarchy returns the type array<string,string[]> which is incompatible with the documented return type null|string[].
Loading history...
705
		}
706
		return $this->_hierarchy[strtolower(TPropertyValue::ensureString($role))] ?? null;
707
	}
708
709
	/**
710
	 * @param null|string $permission
711
	 * @return null|array<string, TAuthorizationRuleCollection>|TAuthorizationRuleCollection
712
	 */
713
	public function getPermissionRules($permission)
714
	{
715
		if (is_string($permission)) {
716
			return $this->_permissionRules[strtolower($permission)] ?? null;
717
		} else {
718
			return $this->_permissionRules;
719
		}
720
	}
721
722
	/**
723
	 * All super roles will get "all" roles and thus all permissions on module init.
724
	 * @return null|string[] array of rolls that get all permissions
725
	 */
726
	public function getSuperRoles()
727
	{
728
		return $this->_superRoles;
729
	}
730
731
	/**
732
	 * sets the super roles to get all permissions.
733
	 * @param string|string[] $roles  of rolls that get all permissions
734
	 * @throws \Prado\Exceptions\TInvalidOperationException when the module is initialized
735
	 */
736
	public function setSuperRoles($roles)
737
	{
738
		if ($this->_initialized) {
739
			throw new TInvalidOperationException('permissions_property_unchangeable', 'SuperRoles');
740
		}
741
		if (!is_array($roles)) {
742
			$roles = array_map('trim', explode(',', $roles));
743
		}
744
		$this->_superRoles = array_filter($roles);
745
		;
746
	}
747
748
	/**
749
	 * Gets the default roles of all users.
750
	 * @return null|string[] the default roles of all users
751
	 */
752
	public function getDefaultRoles()
753
	{
754
		return $this->_defaultRoles;
755
	}
756
757
	/**
758
	 * @param string|string[] $roles the default roles of all users
759
	 * @throws \Prado\Exceptions\TInvalidOperationException when the module is initialized
760
	 */
761
	public function setDefaultRoles($roles)
762
	{
763
		if ($this->_initialized) {
764
			throw new TInvalidOperationException('permissions_property_unchangeable', 'DefaultRoles');
765
		}
766
		if (!is_array($roles)) {
767
			$roles = array_filter(array_map('trim', explode(',', $roles)));
768
		}
769
		$this->_defaultRoles = $roles;
770
	}
771
772
	/**
773
	 * @return string the full path to the file storing role/rule information
774
	 */
775
	public function getPermissionFile()
776
	{
777
		return $this->_permissionFile;
778
	}
779
780
	/**
781
	 * @param string $value role/rule data file path (in namespace form). The file format is configuration format
782
	 * whose content is similar to that role/rule block in the module configuration.
783
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
784
	 * @throws \Prado\Exceptions\TConfigurationException if the file is not in proper namespace format
785
	 */
786
	public function setPermissionFile($value)
787
	{
788
		if ($this->_initialized) {
789
			throw new TInvalidOperationException('permissions_property_unchangeable', 'PermissionFile');
790
		} elseif (($this->_permissionFile = Prado::getPathOfNamespace($value, $this->getApplication()->getConfigurationFileExt())) === null || !is_file($this->_permissionFile)) {
791
			throw new TConfigurationException('permissions_permissionfile_invalid', $value);
792
		}
793
	}
794
795
	/**
796
	 * @return numeric the priority of Allow With Permission and Preset Rules, default 5
797
	 */
798
	public function getAutoRulePriority()
799
	{
800
		return $this->_autoRulePriority;
801
	}
802
803
	/**
804
	 * @param numeric $priority the priority of Allow With Permission and Preset Rules
805
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
806
	 */
807
	public function setAutoRulePriority($priority)
808
	{
809
		if ($this->_initialized) {
810
			throw new TInvalidOperationException('permissions_property_unchangeable', 'AutoRulePriority');
811
		}
812
		$this->_autoRulePriority = is_numeric($priority) ? $priority : (float) $priority;
0 ignored issues
show
Documentation Bug introduced by
It seems like is_numeric($priority) ? ...ity : (double)$priority of type double is incompatible with the declared type Prado\Security\Permissions\numeric of property $_autoRulePriority.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
introduced by
The condition is_numeric($priority) is always false.
Loading history...
813
	}
814
815
	/**
816
	 * @return bool enable Allow With Permission rule, default true
817
	 */
818
	public function getAutoAllowWithPermission()
819
	{
820
		return $this->_autoAllowWithPermission;
821
	}
822
823
	/**
824
	 * @param bool $enable enable Allow With Permission rule
825
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
826
	 */
827
	public function setAutoAllowWithPermission($enable)
828
	{
829
		if ($this->_initialized) {
830
			throw new TInvalidOperationException('permissions_property_unchangeable', 'AutoAllowWithPermission');
831
		}
832
		$this->_autoAllowWithPermission = TPropertyValue::ensureBoolean($enable);
833
	}
834
835
	/**
836
	 * @return bool enable Module Rules, default true
837
	 */
838
	public function getAutoPresetRules()
839
	{
840
		return $this->_autoRulePresetRules;
841
	}
842
843
	/**
844
	 * @param bool $enable the priority of Allow With Permission
845
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
846
	 */
847
	public function setAutoPresetRules($enable)
848
	{
849
		if ($this->_initialized) {
850
			throw new TInvalidOperationException('permissions_property_unchangeable', 'AutoPresetRules');
851
		}
852
		$this->_autoRulePresetRules = TPropertyValue::ensureBoolean($enable);
853
	}
854
855
	/**
856
	 * @return bool the priority of Allow With Permission, default true
857
	 */
858
	public function getAutoDenyAll()
859
	{
860
		return $this->_autoDenyAll > 0;
861
	}
862
863
	/**
864
	 * @param bool $enable the priority of Allow With Permission
865
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
866
	 */
867
	public function setAutoDenyAll($enable)
868
	{
869
		if ($this->_initialized) {
870
			throw new TInvalidOperationException('permissions_property_unchangeable', 'AutoDenyAll');
871
		}
872
		$this->_autoDenyAll = TPropertyValue::ensureBoolean($enable);
873
	}
874
875
	/**
876
	 * @return numeric the priority of Deny All rule, default 999999
877
	 */
878
	public function getAutoDenyAllPriority()
879
	{
880
		return $this->_autoDenyAllPriority;
881
	}
882
883
	/**
884
	 * @param numeric $priority the priority of Deny All rule
885
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
886
	 */
887
	public function setAutoDenyAllPriority($priority)
888
	{
889
		if ($this->_initialized) {
890
			throw new TInvalidOperationException('permissions_property_unchangeable', 'AutoDenyAllPriority');
891
		}
892
		$this->_autoDenyAllPriority = is_numeric($priority) ? $priority : (float) $priority;
0 ignored issues
show
introduced by
The condition is_numeric($priority) is always false.
Loading history...
Documentation Bug introduced by
It seems like is_numeric($priority) ? ...ity : (double)$priority of type double is incompatible with the declared type Prado\Security\Permissions\numeric of property $_autoDenyAllPriority.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
893
	}
894
895
	/**
896
	 * @return \Prado\Util\TDbParameterModule DbParameter instance
897
	 */
898
	public function getDbParameter()
899
	{
900
		return $this->_dbParameter;
901
	}
902
903
	/**
904
	 * @param \Prado\Security\IUserManager|string $provider the user manager module ID or the DbParameter object
905
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
906
	 * @throws \Prado\Exceptions\TConfigurationException if the $provider is not a TDbParameterModule
907
	 */
908
	public function setDbParameter($provider)
909
	{
910
		if ($this->_initialized) {
911
			throw new TInvalidOperationException('permissions_property_unchangeable', 'DbParameter');
912
		}
913
		if ($provider !== null && !is_string($provider) && !($provider instanceof TDbParameterModule)) {
914
			throw new TConfigurationException('permissions_dbparameter_invalid', is_object($provider) ? $provider::class : $provider);
915
		}
916
		$this->_dbParameter = $provider;
0 ignored issues
show
Documentation Bug introduced by
It seems like $provider of type string is incompatible with the declared type Prado\Util\TDbParameterModule of property $_dbParameter.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
917
	}
918
919
	/**
920
	 * @return string name of the parameter to load
921
	 */
922
	public function getLoadParameter()
923
	{
924
		return $this->_parameter;
925
	}
926
927
	/**
928
	 * @param string $value name of the parameter to load
929
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
930
	 */
931
	public function setLoadParameter($value)
932
	{
933
		if ($this->_initialized) {
934
			throw new TInvalidOperationException('permissions_property_unchangeable', 'LoadParameter');
935
		}
936
		$this->_parameter = $value;
0 ignored issues
show
Documentation Bug introduced by
It seems like $value of type string is incompatible with the declared type Prado\Security\Permissions\numeric of property $_parameter.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
937
	}
938
939
	/**
940
	 * detaches the automatic class behaviors
941
	 */
942
	public function __destruct()
943
	{
944
		TComponent::detachClassBehavior(static::PERMISSIONS_BEHAVIOR, IPermissions::class);
945
		TComponent::detachClassBehavior(static::USER_PERMISSIONS_BEHAVIOR, \Prado\Security\IUser::class);
946
		TComponent::detachClassBehavior(static::PERMISSIONS_CONFIG_BEHAVIOR, \Prado\Web\Services\TPageConfiguration::class);
947
		parent::__destruct();
948
	}
949
}
950