Passed
Push — master ( f5ec54...94ecb9 )
by Fabio
05:41
created

TPermissionsManager::getDefaultRoles()   A

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
 * TPermissionsManager class file
4
 *
5
 * @author Brad Anderson <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Security\Permissions;
11
12
use Prado\Exceptions\TConfigurationException;
13
use Prado\Exceptions\TInvalidOperationException;
14
use Prado\Exceptions\TInvalidDataValueException;
15
use Prado\Prado;
16
use Prado\Security\TAuthorizationRule;
17
use Prado\Security\TAuthorizationRuleCollection;
18
use Prado\TApplication;
19
use Prado\TComponent;
20
use Prado\TPropertyValue;
21
use Prado\Util\TDbParameterModule;
22
use Prado\Xml\TXmlDocument;
23
use Prado\Xml\TXmlElement;
24
25
/**
26
 * TPermissionsManager class.
27
 *
28
 * TPermissionsManager handles Permissions authorization and Roll Based
29
 * Access Control (RBAC).  Each registered Permission is given a set of
30
 * {@link \Prado\Security\TAuthorizationRule}s.  The RBAC is based on roles
31
 * having children roles and permissions, with permissions being thought of
32
 * as special roles themselves.
33
 *
34
 * TPermissionsManager attaches {@link TPermissionsBehavior} to all classes
35
 * that implement {@link IPermissions}.  This is the main mechanism
36
 * by which application permissions are registered.
37
 *
38
 * The role hierarchy and permission rules are unique to each application.  The
39
 * permissions configuration is defined in the TPermissionsManager application
40
 * configuration or in a separate {@link PermissionsFile}. {@link TPermissionsConfigurationBehavior}
41
 * enables a page configuration to have Permission Configurations as well.
42
 * A {@link TDbParameterModule} can be specified with {@link getDbParameter} for
43
 * loading dynamic roles and permissions.
44
 *
45
 * Module XML configurations (and similarly PermissionFile) follows the format, eg:
46
 * <code>
47
 * <module id="permissions" class="Prado\Security\Permissions\TPermissionsManager" DefaultRoles="Default" SuperRoles="Administrator">
48
 *	<role name="Developer" children="all, param_shell_permission, cron" />
49
 *	<role name="Manager" children="editor, change_user_role_permission, cron_shell" />
50
 *	<role name="cron_shell" children="cron_add_task, cron_update_task, cron_remove_task" />
51
 *	<role name="cron" children="cron_shell, cron_manage_log, cron_add_task, cron_update_task, cron_remove_task" />
52
 *  <role name="Default" children="register_user, blog_read_posts, blog_comment">
53
 *	<permissionrule name="param_shell_permission" action="deny" users="*" roles="" verb="*" IPs="" />
54
 *	<permissionrule name="cron_shell" action="allow" users="*" roles="Developer,cron_shell,cron_manage_log" verb="*" IPs="" />
55
 *	<permissionrule name="register_user" action="allow" users="?" />
56
 *	<permissionrule name="register_user" action="allow" roles="Manager" />
57
 *	<permissionrule name="change_profile" action="deny" users="?" priority="0" />
58
 *	<permissionrule name="blog_update_posts" class="Prado\Security\Permissions\TUserOwnerRule" Priority="5" />
59
 *	<permissionrule name="cron" action="allow" users="admin, user1, user2" roles="*" verb="*" IPs="*"  />
60
 *	<permissionrule name="blog_*" action="allow" users="admin, user1, user2" roles="*" verb="*" IPs="*"  />
61
 *	<permissionrule name="*" action="deny" priority="1000" />
62
 * </module>
63
 * </code>
64
 *
65
 * and in PHP the same file would follow the following format, eg:
66
 * <code>
67
 * 'modules' => [
68
 * 'permissions' => ['class' => 'Prado\Security\Permissions\TPermissionsManager',
69
 * 		'properties' => ['DefaultRoles' => 'Default', 'SuperRoles' => "Administrator"],
70
 *		'roles' => [
71
 *			'Developer' => ['all', 'param_shell_permission', 'cron'],
72
 *			'Manager' => ['editor', 'change_user_role_permission', 'cron_shell'],
73
 *			'cron_shell' => ['cron_add_task', 'cron_update_task', 'cron_remove_task'],
74
 *			'cron' => ['cron_shell', 'cron_manage_log', 'cron_add_task', 'cron_update_task', 'cron_remove_task'],
75
 *			'Default' => ['register_user', 'blog_read_posts', 'blog_comment'],
76
 *		],
77
 * 		'permissionRules' => [
78
 *			[name => 'param_shell_permission', 'action' => 'deny', 'users' => '*', roles => '*', 'verb' => '*', 'IPs' =>''],
79
 *			[name => 'cron_shell', 'action' => 'allow', 'users' => 'Developer,cron_shell,cron_manage_log', roles => 'cron_shell', 'verb' => '*', 'IPs' =>''],
80
 *			[name => 'register_user', 'action' => 'allow', 'users' => '?'],
81
 *			[name => 'register_user', 'action' => 'allow', 'roles' => 'Manager'],
82
 *			[name => 'change_profile', 'action' => 'deny', 'users' => '?', 'priority' => '0'],
83
 *			[name => 'blog_update_posts', 'class' => 'Prado\Security\Permissions\TUserOwnerRule', 'priority' => '5'],
84
 *			[name => 'cron', 'action' => 'allow', 'users' => 'admin, user1, user2'],
85
 *			[name => 'blog_*', 'action' => 'allow', 'users' => 'admin, user1, user2'],
86
 *			[name => '*', 'action' => 'deny', 'priority' => 1000]
87
 *		]
88
 * ]
89
 * </code>
90
 *
91
 * In this example, "cron" is not a permission, but when used as a permission,
92
 * all children roles/permissions will receive the rule.  Permissions with children,
93
 * such as 'cron_shell' (above), will give all its children the rule as
94
 * well.
95
 *
96
 * A special role "All" is automatically created to contain all the permissions.
97
 * Specifying "all" as a child, is the same as specifying a role as a super role
98
 * via {@link setSuperRoles}.
99
 *
100
 * All users get the roles specified by {@link getDefaultRoles}.  This changes
101
 * the default Prado behavior for guest users having no roles.
102
 *
103
 * Intermediate roles, that are not defined in the user system, may be defined in
104
 * the hierarchy, in the above example the "cron" role is not defined by the system,
105
 * but is defined in the role hierarchy.
106
 *
107
 * Permission Rules can have multiple rules. they are
108
 * ordered by natural specified configuration order unless the rule property
109
 * {@link TAuthorizationRule::setPriority} is set.
110
 *
111
 * Permissions authorization rules may use the '*' or 'perm_*' to add the rules to all
112
 * matching permission names.  Anything before the * is matched as a permission.
113
 * This does not traverse the hierarchy roles matching the name, just the permissions
114
 * are matched for the TAuthorizationRule.
115
 *
116
 * A permission name must list itself as a role in TAuthorizationRule for the user to be
117
 * validated for that permission name for authorization.  This is handled automatically
118
 * by TPermissionManager with the {@link getAutoAllowWithPermission} property.
119
 * By default getAutoAllowWithPermission is true, and allows any user with
120
 * that permission in their hierarchy to allow access to the functionality.
121
 * This rule priority can be set with {@link getAutoRulePriority},
122
 * where the default is 5, and -thus- before user defined rules.
123
 *
124
 * The second automatic rules includes Modules have their own preset rules that can
125
 * be automatically added via {@link getAutoPresetRules}.  By default this
126
 * is true. These rules typically allow owners of the data to be permitted without having
127
 * a permission-role.  Preset rules can define their own priorities but those
128
 * without set priorities receive the priority from {@link getAutoRulePriority}.
129
 *
130
 * The third, and last, auto-Rule is the final {@link getAutoDenyAll DenyAll}
131
 * rule. This is the last rule that denies all by default.  The AutoDenyAll
132
 * gets its rule priority from {@link getAutoDenyAllPriority}.  By default,
133
 * deny all to all permissions is enabled and thus blocking all permissions.
134
 *
135
 * Recursive hierarchy is gracefully handled, in case of any loop structures.
136
 *
137
 * When TPermissionsManager is a module in your app, there are three permissions
138
 * to control user access to its function:
139
 *  - TPermissionsManager::PERM_PERMISSIONS_SHELL 'permissions_shell' enables the shell commands.
140
 *  - TPermissionsManager::PERM_PERMISSIONS_MANAGE_ROLES 'permissions_manage_roles' enables adding and removing roles and children.
141
 *  - TPermissionsManager::PERM_PERMISSIONS_MANAGE_RULES 'permissions_manage_rules' enables adding and removing rules for permissions and roles.
142
 *
143
 * The role and rule management functions only work when the TDbParameter Module is specified.
144
 * The following gives user "admin" and all users with "Administrators" role the
145
 * permission to access permissions shell and its full functionality:
146
 * <code>
147
 *	 <role name="permissions_shell" children="permissions_manage_roles, permissions_manage_rules" />
148
 *   <permissionrule name="permissions_shell" action="allow" users="admin" />
149
 *   <permissionrule name="permissions_shell" action="allow" roles="Administrators" />
150
 * <code>
151
 *
152
 * @author Brad Anderson <[email protected]>
153
 * @method bool dyRegisterShellAction($returnValue)
154
 * @method bool dyAddRoleChildren(bool $return, string $role, string[] $children)
155
 * @method bool dyRemoveRoleChildren(bool $return, string $role, string[] $children)
156
 * @method bool dyAddPermissionRule(bool $return, string $permission, \Prado\Security\TAuthorizationRule $rule)
157
 * @method bool dyRemovePermissionRule(bool $return, string $permission, \Prado\Security\TAuthorizationRule $rule)
158
 * @since 4.2.0
159
 */
160
class TPermissionsManager extends \Prado\TModule implements IPermissions
161
{
162
	public const PERMISSIONS_BEHAVIOR = 'permissions';
163
164
	public const USER_PERMISSIONS_BEHAVIOR = 'usercan';
165
166
	public const PERMISSIONS_CONFIG_BEHAVIOR = 'permissionsConfig';
167
168
	public const PERM_PERMISSIONS_SHELL = 'permissions_shell';
169
170
	public const PERM_PERMISSIONS_MANAGE_ROLES = 'permissions_manage_roles';
171
172
	public const PERM_PERMISSIONS_MANAGE_RULES = 'permissions_manage_rules';
173
174
	/** @var string[] roles that get all permissions, default [] */
175
	private $_superRoles;
176
177
	/** @var string[] Default roles to give all users, default [] */
178
	private $_defaultRoles;
179
180
	/** @var array<string, \Prado\Security\TAuthorizationRuleCollection> contains the rules for each permission */
181
	private $_permissionRules = [];
182
183
	/** @var array<string, string> contains the short descriptions for each permission */
184
	private $_descriptions = [];
185
186
	/** @var array<string, \Prado\Security\TAuthorizationRule[]> the rules to apply to newly registered Permissions */
187
	private $_autoRules = [];
188
189
	/** @var array<string, string[]> contains the hierarchy of roles and children roles/permissions */
190
	private $_hierarchy = [];
191
192
	/** @var bool is the module initialized */
193
	private $_initialized = false;
194
195
	/** @var string role hierarchy and permission rules information file */
196
	private $_permissionFile;
197
198
	/** @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...
199
	private $_autoRulePriority = 5;
200
201
	/** @var bool add allow users with permission-role, default true  */
202
	private $_autoAllowWithPermission = true;
203
204
	/** @var bool add module rules, allows User's data, default true */
205
	private $_autoRulePresetRules = true;
206
207
	/** @var bool add Deny All rule to every permissions as the last rule, default true */
208
	private $_autoDenyAll = true;
209
210
	/** @var numeric the priority of the module Rule, usually these are Allow User As Owner, default 1000000 */
211
	private $_autoDenyAllPriority = 1000000;
212
213
	/** @var \Prado\Util\TDbParameterModule the database module providing runtime roles and rules */
214
	private $_dbParameter;
215
216
	/** @var numeric the priority of the module Rule, usually these are Allow User As Owner */
217
	private $_parameter = 'configuration:TPermissionsManager:runtime';
218
219
	/**
220
	 * @returtn ?TPermissionsManager The PermissionsManager for the application.
221
	 */
222
	public static function getManager(): ?TPermissionsManager
223
	{
224
		$app = Prado::getApplication();
225
		if ($app) {
226
			$modules = $app->getModulesByType(self::class);
227
			if (count($modules)) {
228
				return array_values($modules)[0];
229
			}
230
		}
231
		return null;
232
	}
233
234
	/**
235
	 * @param \Prado\Security\Permissions\TPermissionsManager $manager
236
	 * @return TPermissionEvent[] the dynamic events to have authorization
237
	 */
238
	public function getPermissions($manager)
239
	{
240
		return [
241
			new TPermissionEvent(static::PERM_PERMISSIONS_SHELL, 'Activates permissions shell commands.', 'dyRegisterShellAction'),
242
			new TPermissionEvent(static::PERM_PERMISSIONS_MANAGE_ROLES, 'Manages Db Permissions Role Children.', ['dyAddRoleChildren', 'dyRemoveRoleChildren']),
243
			new TPermissionEvent(static::PERM_PERMISSIONS_MANAGE_RULES, 'Manages Db Permissions Rules.', ['dyAddPermissionRule', 'dyRemovePermissionRule']),
244
		];
245
	}
246
247
	/**
248
	 * @param array|TXmlElement $config the application configuration
249
	 */
250
	public function init($config)
251
	{
252
		$app = $this->getApplication();
253
		if (is_string($this->_dbParameter)) {
0 ignored issues
show
introduced by
The condition is_string($this->_dbParameter) is always false.
Loading history...
254
			if (($dbParameter = $app->getModule($this->_dbParameter)) === null) {
255
				throw new TConfigurationException('permissions_dbparameter_nonexistent', $this->_dbParameter);
256
			}
257
			if (!($dbParameter instanceof TDbParameterModule)) {
258
				throw new TConfigurationException('permissions_dbparameter_invalid', $this->_dbParameter);
259
			}
260
			$this->_dbParameter = $dbParameter;
261
		}
262
263
		if ($this->_initialized) {
264
			throw new TInvalidOperationException('permissions_init_once');
265
		}
266
		$this->_initialized = true;
267
268
		$manager = \WeakReference::create($this);
269
		TComponent::attachClassBehavior(static::PERMISSIONS_BEHAVIOR, ['class' => TPermissionsBehavior::class, 'permissionsmanager' => $manager], IPermissions::class, -10);
0 ignored issues
show
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

269
		TComponent::attachClassBehavior(static::PERMISSIONS_BEHAVIOR, ['class' => TPermissionsBehavior::class, 'permissionsmanager' => $manager], IPermissions::class, /** @scrutinizer ignore-type */ -10);
Loading history...
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

269
		TComponent::attachClassBehavior(static::PERMISSIONS_BEHAVIOR, /** @scrutinizer ignore-type */ ['class' => TPermissionsBehavior::class, 'permissionsmanager' => $manager], IPermissions::class, -10);
Loading history...
270
		TComponent::attachClassBehavior(static::USER_PERMISSIONS_BEHAVIOR, ['class' => TUserPermissionsBehavior::class, 'permissionsmanager' => $manager], \Prado\Security\IUser::class, -10);
271
		TComponent::attachClassBehavior(static::PERMISSIONS_CONFIG_BEHAVIOR, ['class' => TPermissionsConfigurationBehavior::class, 'permissionsmanager' => $manager], \Prado\Web\Services\TPageConfiguration::class, -10);
272
273
		$this->loadPermissionsData($config);
274
		if ($this->_permissionFile !== null) {
275
			if ($this->getApplication()->getConfigurationType() == TApplication::CONFIG_TYPE_PHP) {
276
				$userFile = include $this->_permissionFile;
277
				$this->loadPermissionsData($userFile);
278
			} else {
279
				$dom = new TXmlDocument();
280
				$dom->loadFromFile($this->_permissionFile);
281
				$this->loadPermissionsData($dom);
282
			}
283
		}
284
		if ($this->_dbParameter) {
285
			$this->loadPermissionsData($this->_dbParameter->get($this->_parameter));
286
		}
287
288
		foreach (array_map('strtolower', $this->getSuperRoles() ?? []) as $role) {
289
			$this->_hierarchy[$role] = array_merge(['all'], $this->_hierarchy[$role] ?? []);
290
		}
291
292
		$app->attachEventHandler('onAuthenticationComplete', [$this, 'registerShellAction']);
293
294
		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

294
		parent::init(/** @scrutinizer ignore-type */ $config);
Loading history...
295
	}
296
297
	/**
298
	 * Registers a permission name with description and preset rules.
299
	 * @param string $permissionName name of the permission
300
	 * @param string $description description of the permission
301
	 * @param null|\Prado\Security\TAuthorizationRule[] $rules
302
	 */
303
	public function registerPermission($permissionName, $description, $rules = null)
304
	{
305
		$permission = strtolower($permissionName);
306
		$this->_descriptions[$permission] = TPropertyValue::ensureString($description);
307
308
		if ($this->_autoDenyAll === true) {
309
			$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...
310
			$this->addPermissionRuleInternal('*', new TAuthorizationRule('deny', '*', '*', '*', '*', $this->_autoDenyAllPriority));
311
		}
312
313
		$this->_hierarchy['all'][] = $permission;
314
315
		if (!isset($this->_permissionRules[$permission])) {
316
			$this->_permissionRules[$permission] = new TAuthorizationRuleCollection();
317
		} else {
318
			throw new TInvalidOperationException('permissions_duplicate_permission', $permissionName);
319
		}
320
		if ($this->_autoAllowWithPermission) {
321
			$this->_permissionRules[$permission]->add(new TAuthorizationRule('allow', '*', $permission, '*', '*', $this->_autoRulePriority));
322
		}
323
		if ($this->_autoRulePresetRules && $rules) {
324
			if (!is_array($rules)) {
0 ignored issues
show
introduced by
The condition is_array($rules) is always true.
Loading history...
325
				$rules = [$rules];
326
			}
327
			foreach ($rules as $rule) {
328
				$this->_permissionRules[$permission]->add($rule, is_numeric($p = $rule->getPriority()) ? $p : $this->_autoRulePriority);
329
			}
330
		}
331
		foreach ($this->_autoRules as $rulePerm => $rules) {
332
			$pos = strpos($rulePerm, '*');
333
			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

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

501
	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...
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

501
	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...
502
	{
503
		if ($this->dyRegisterShellAction(false) !== true && ($app = $this->getApplication()) instanceof \Prado\Shell\TShellApplication) {
504
			$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

504
			$app->addShellActionClass(/** @scrutinizer ignore-type */ ['class' => TPermissionsAction::class, 'PermissionsManager' => $this]);
Loading history...
505
		}
506
	}
507
508
	/**
509
	 * checks if the $permission is in the $roles hierarchy.
510
	 * @param string[] $roles the roles to check the permission
511
	 * @param string $permission the permission-role being checked for in the hierarchy
512
	 * @param array<string, bool> &$checked the roles already checked
513
	 */
514
	public function isInHierarchy($roles, $permission, &$checked = [])
515
	{
516
		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...
517
			return false;
518
		}
519
		if (!$checked) {
520
			if (!is_array($roles)) {
0 ignored issues
show
introduced by
The condition is_array($roles) is always true.
Loading history...
521
				$roles = array_filter(array_map('trim', explode(',', $roles)));
522
			}
523
			$roles = array_map('strtolower', $roles);
524
			$permission = strtolower($permission);
525
		}
526
		if (in_array($permission, $roles)) {
527
			return true;
528
		}
529
		foreach ($roles as $role) {
530
			if (!isset($checked[$role])) {
531
				$checked[$role] = true;
532
				if (isset($this->_hierarchy[$role]) && $this->isInHierarchy($this->_hierarchy[$role], $permission, $checked)) {
533
					return true;
534
				}
535
			}
536
		}
537
		return false;
538
	}
539
540
	/**
541
	 * Get the roles that are runtime from the database
542
	 * @return array<string, string[]> roles and children from the database
543
	 */
544
	public function getDbConfigRoles()
545
	{
546
		if (!$this->_dbParameter || !$this->_parameter) {
547
			return [];
548
		}
549
		$runtimeData = $this->_dbParameter->get($this->_parameter) ?? [];
550
		return $runtimeData['roles'] ?? [];
551
	}
552
553
	/**
554
	 * Get the permission rules that are runtime from the database
555
	 * @return array<string, \Prado\Security\TAuthorizationRule[]>
556
	 */
557
	public function getDbConfigPermissionRules()
558
	{
559
		if (!$this->_dbParameter || !$this->_parameter) {
560
			return [];
561
		}
562
		$runtimeData = $this->_dbParameter->get($this->_parameter) ?? [];
563
		return $runtimeData['permissionrules'] ?? [];
564
	}
565
566
	/**
567
	 * This adds children to a role within the runtime context.  The children
568
	 * can be a single comma separated string.
569
	 * @param string $role the role to add children
570
	 * @param string|string[] $children the children to add to the role
571
	 * @throws TInvalidDataValueException when children is not an array
572
	 * @return bool was the method successful
573
	 */
574
	public function addRoleChildren($role, $children)
575
	{
576
		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

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

606
		if ($this->dyRemoveRoleChildren(false, $role, /** @scrutinizer ignore-type */ $children) === true || !$this->_dbParameter) {
Loading history...
607
			return false;
608
		}
609
		if (is_string($children)) {
610
			$children = array_map('trim', explode(',', $children));
611
		} elseif (!is_array($children)) {
0 ignored issues
show
introduced by
The condition is_array($children) is always true.
Loading history...
612
			throw new TInvalidDataValueException('permissions_children_invalid', is_object($children) ? $children::class : $children);
613
		}
614
		$role = strtolower($role);
615
		$children = array_map('strtolower', array_filter($children));
616
		$this->_hierarchy[$role] = array_values(array_diff($this->_hierarchy[$role] ?? [], $children));
617
		if (!$this->_hierarchy[$role]) {
618
			unset($this->_hierarchy[$role]);
619
		}
620
621
		$runtimeData = $this->_dbParameter->get($this->_parameter) ?? [];
622
		$runtimeData['roles'][$role] = array_values(array_diff($runtimeData['roles'][$role] ?? [], $children));
623
		if (!$runtimeData['roles'][$role]) {
624
			unset($runtimeData['roles'][$role]);
625
		}
626
		$this->_dbParameter->set($this->_parameter, $runtimeData);
627
		return true;
628
	}
629
630
	/**
631
	 * This method adds permission rules with in the runtime context.
632
	 * @param string $permission
633
	 * @param \Prado\Security\TAuthorizationRule $rule
634
	 * @return bool was the method successful
635
	 */
636
	public function addPermissionRule($permission, $rule)
637
	{
638
		$permission = strtolower($permission);
639
640
		if ($this->dyAddPermissionRule(false, $permission, $rule) === true || !$this->_dbParameter) {
641
			return false;
642
		}
643
		$this->addPermissionRuleInternal($permission, $rule);
644
645
		$runtimeData = $this->_dbParameter->get($this->_parameter) ?? [];
646
		$runtimeData['permissionrules'] ??= [];
647
		$runtimeData['permissionrules'][$permission][] = $rule;
648
		$this->_dbParameter->set($this->_parameter, $runtimeData);
649
650
		return true;
651
	}
652
653
	/**
654
	 * This method removes permission rules with in the runtime context.
655
	 * @param string $permission a permission or role to remove the rule from
656
	 * @param \Prado\Security\TAuthorizationRule $rule
657
	 * @return bool was the method successful
658
	 */
659
	public function removePermissionRule($permission, $rule)
660
	{
661
		$permission = strtolower($permission);
662
663
		if ($this->dyRemovePermissionRule(false, $permission, $rule) === true || !$this->_dbParameter) {
664
			return false;
665
		}
666
667
		$this->removePermissionRuleInternal($permission, $rule);
668
669
		$runtimeData = $this->_dbParameter->get($this->_parameter) ?? [];
670
		$runtimeData['permissionrules'] ??= [];
671
672
		if (($index = array_search($rule, $runtimeData['permissionrules'][$permission] ?? [], true)) === false) {
673
			return false;
674
		}
675
		unset($runtimeData['permissionrules'][$permission][$index]);
676
		if (!$runtimeData['permissionrules'][$permission]) {
677
			unset($runtimeData['permissionrules'][$permission]);
678
		} else {
679
			$runtimeData['permissionrules'][$permission] = array_values($runtimeData['permissionrules'][$permission]);
680
		}
681
		$this->_dbParameter->set($this->_parameter, $runtimeData);
682
683
		return true;
684
	}
685
686
	/**
687
	 * Gets all the roles in the hierarchy, though may not be valid roles in the application.
688
	 * @return string[] the roles in the hierarchy.
689
	 */
690
	public function getHierarchyRoles()
691
	{
692
		return array_keys($this->_hierarchy);
693
	}
694
695
	/**
696
	 * Gets the children for a specific role in the hierarchy.
697
	 * @param string $role the role to return its children
698
	 * @return null|string[] the children of a specific role.
699
	 */
700
	public function getHierarchyRoleChildren($role)
701
	{
702
		if (!$role) {
703
			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...
704
		}
705
		return $this->_hierarchy[strtolower(TPropertyValue::ensureString($role))] ?? null;
706
	}
707
708
	/**
709
	 * @param null|string $permission
710
	 * @return null|array<string, TAuthorizationRuleCollection>|TAuthorizationRuleCollection
711
	 */
712
	public function getPermissionRules($permission)
713
	{
714
		if (is_string($permission)) {
715
			return $this->_permissionRules[strtolower($permission)] ?? null;
716
		} else {
717
			return $this->_permissionRules;
718
		}
719
	}
720
721
	/**
722
	 * All super roles will get "all" roles and thus all permissions on module init.
723
	 * @return null|string[] array of rolls that get all permissions
724
	 */
725
	public function getSuperRoles()
726
	{
727
		return $this->_superRoles;
728
	}
729
730
	/**
731
	 * sets the super roles to get all permissions.
732
	 * @param string|string[] $roles  of rolls that get all permissions
733
	 * @throws \Prado\Exceptions\TInvalidOperationException when the module is initialized
734
	 */
735
	public function setSuperRoles($roles)
736
	{
737
		if ($this->_initialized) {
738
			throw new TInvalidOperationException('permissions_property_unchangeable', 'SuperRoles');
739
		}
740
		if (!is_array($roles)) {
741
			$roles = array_map('trim', explode(',', $roles));
742
		}
743
		$this->_superRoles = array_filter($roles);
744
		;
745
	}
746
747
	/**
748
	 * Gets the default roles of all users.
749
	 * @return null|string[] the default roles of all users
750
	 */
751
	public function getDefaultRoles()
752
	{
753
		return $this->_defaultRoles;
754
	}
755
756
	/**
757
	 * @param string|string[] $roles the default roles of all users
758
	 * @throws \Prado\Exceptions\TInvalidOperationException when the module is initialized
759
	 */
760
	public function setDefaultRoles($roles)
761
	{
762
		if ($this->_initialized) {
763
			throw new TInvalidOperationException('permissions_property_unchangeable', 'DefaultRoles');
764
		}
765
		if (!is_array($roles)) {
766
			$roles = array_filter(array_map('trim', explode(',', $roles)));
767
		}
768
		$this->_defaultRoles = $roles;
769
	}
770
771
	/**
772
	 * @return string the full path to the file storing role/rule information
773
	 */
774
	public function getPermissionFile()
775
	{
776
		return $this->_permissionFile;
777
	}
778
779
	/**
780
	 * @param string $value role/rule data file path (in namespace form). The file format is configuration format
781
	 * whose content is similar to that role/rule block in the module configuration.
782
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
783
	 * @throws \Prado\Exceptions\TConfigurationException if the file is not in proper namespace format
784
	 */
785
	public function setPermissionFile($value)
786
	{
787
		if ($this->_initialized) {
788
			throw new TInvalidOperationException('permissions_property_unchangeable', 'PermissionFile');
789
		} elseif (($this->_permissionFile = Prado::getPathOfNamespace($value, $this->getApplication()->getConfigurationFileExt())) === null || !is_file($this->_permissionFile)) {
790
			throw new TConfigurationException('permissions_permissionfile_invalid', $value);
791
		}
792
	}
793
794
	/**
795
	 * @return numeric the priority of Allow With Permission and Preset Rules, default 5
796
	 */
797
	public function getAutoRulePriority()
798
	{
799
		return $this->_autoRulePriority;
800
	}
801
802
	/**
803
	 * @param numeric $priority the priority of Allow With Permission and Preset Rules
804
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
805
	 */
806
	public function setAutoRulePriority($priority)
807
	{
808
		if ($this->_initialized) {
809
			throw new TInvalidOperationException('permissions_property_unchangeable', 'AutoRulePriority');
810
		}
811
		$this->_autoRulePriority = 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 $_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...
812
	}
813
814
	/**
815
	 * @return bool enable Allow With Permission rule, default true
816
	 */
817
	public function getAutoAllowWithPermission()
818
	{
819
		return $this->_autoAllowWithPermission;
820
	}
821
822
	/**
823
	 * @param bool $enable enable Allow With Permission rule
824
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
825
	 */
826
	public function setAutoAllowWithPermission($enable)
827
	{
828
		if ($this->_initialized) {
829
			throw new TInvalidOperationException('permissions_property_unchangeable', 'AutoAllowWithPermission');
830
		}
831
		$this->_autoAllowWithPermission = TPropertyValue::ensureBoolean($enable);
832
	}
833
834
	/**
835
	 * @return bool enable Module Rules, default true
836
	 */
837
	public function getAutoPresetRules()
838
	{
839
		return $this->_autoRulePresetRules;
840
	}
841
842
	/**
843
	 * @param bool $enable the priority of Allow With Permission
844
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
845
	 */
846
	public function setAutoPresetRules($enable)
847
	{
848
		if ($this->_initialized) {
849
			throw new TInvalidOperationException('permissions_property_unchangeable', 'AutoPresetRules');
850
		}
851
		$this->_autoRulePresetRules = TPropertyValue::ensureBoolean($enable);
852
	}
853
854
	/**
855
	 * @return bool the priority of Allow With Permission, default true
856
	 */
857
	public function getAutoDenyAll()
858
	{
859
		return $this->_autoDenyAll > 0;
860
	}
861
862
	/**
863
	 * @param bool $enable the priority of Allow With Permission
864
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
865
	 */
866
	public function setAutoDenyAll($enable)
867
	{
868
		if ($this->_initialized) {
869
			throw new TInvalidOperationException('permissions_property_unchangeable', 'AutoDenyAll');
870
		}
871
		$this->_autoDenyAll = TPropertyValue::ensureBoolean($enable);
872
	}
873
874
	/**
875
	 * @return numeric the priority of Deny All rule, default 999999
876
	 */
877
	public function getAutoDenyAllPriority()
878
	{
879
		return $this->_autoDenyAllPriority;
880
	}
881
882
	/**
883
	 * @param numeric $priority the priority of Deny All rule
884
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
885
	 */
886
	public function setAutoDenyAllPriority($priority)
887
	{
888
		if ($this->_initialized) {
889
			throw new TInvalidOperationException('permissions_property_unchangeable', 'AutoDenyAllPriority');
890
		}
891
		$this->_autoDenyAllPriority = 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 $_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...
introduced by
The condition is_numeric($priority) is always false.
Loading history...
892
	}
893
894
	/**
895
	 * @return \Prado\Util\TDbParameterModule DbParameter instance
896
	 */
897
	public function getDbParameter()
898
	{
899
		return $this->_dbParameter;
900
	}
901
902
	/**
903
	 * @param \Prado\Security\IUserManager|string $provider the user manager module ID or the DbParameter object
904
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
905
	 * @throws \Prado\Exceptions\TConfigurationException if the $provider is not a TDbParameterModule
906
	 */
907
	public function setDbParameter($provider)
908
	{
909
		if ($this->_initialized) {
910
			throw new TInvalidOperationException('permissions_property_unchangeable', 'DbParameter');
911
		}
912
		if ($provider !== null && !is_string($provider) && !($provider instanceof TDbParameterModule)) {
913
			throw new TConfigurationException('permissions_dbparameter_invalid', is_object($provider) ? $provider::class : $provider);
914
		}
915
		$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...
916
	}
917
918
	/**
919
	 * @return string name of the parameter to load
920
	 */
921
	public function getLoadParameter()
922
	{
923
		return $this->_parameter;
924
	}
925
926
	/**
927
	 * @param string $value name of the parameter to load
928
	 * @throws \Prado\Exceptions\TInvalidOperationException if the module is already initialized
929
	 */
930
	public function setLoadParameter($value)
931
	{
932
		if ($this->_initialized) {
933
			throw new TInvalidOperationException('permissions_property_unchangeable', 'LoadParameter');
934
		}
935
		$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...
936
	}
937
938
	/**
939
	 * detaches the automatic class behaviors
940
	 */
941
	public function __destruct()
942
	{
943
		TComponent::detachClassBehavior(static::PERMISSIONS_BEHAVIOR, IPermissions::class);
944
		TComponent::detachClassBehavior(static::USER_PERMISSIONS_BEHAVIOR, \Prado\Security\IUser::class);
945
		TComponent::detachClassBehavior(static::PERMISSIONS_CONFIG_BEHAVIOR, \Prado\Web\Services\TPageConfiguration::class);
946
		parent::__destruct();
947
	}
948
}
949