Passed
Pull Request — master (#955)
by
unknown
06:07
created

TPermissionsManager::setPermissionFile()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 4
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 6
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
	private static $_singleton;
220
221
	public static function getManager()
222
	{
223
		return self::$_singleton;
224
	}
225
226
	/**
227
	 * @param \Prado\Security\Permissions\TPermissionsManager $manager
228
	 * @return TPermissionEvent[] the dynamic events to have authorization
229
	 */
230
	public function getPermissions($manager)
231
	{
232
		return [
233
			new TPermissionEvent(static::PERM_PERMISSIONS_SHELL, 'Activates permissions shell commands.', 'dyRegisterShellAction'),
234
			new TPermissionEvent(static::PERM_PERMISSIONS_MANAGE_ROLES, 'Manages Db Permissions Role Children.', ['dyAddRoleChildren', 'dyRemoveRoleChildren']),
235
			new TPermissionEvent(static::PERM_PERMISSIONS_MANAGE_RULES, 'Manages Db Permissions Rules.', ['dyAddPermissionRule', 'dyRemovePermissionRule']),
236
		];
237
	}
238
239
	/**
240
	 * @param array|TXmlElement $config the application configuration
241
	 */
242
	public function init($config)
243
	{
244
		$app = $this->getApplication();
245
		if (is_string($this->_dbParameter)) {
0 ignored issues
show
introduced by
The condition is_string($this->_dbParameter) is always false.
Loading history...
246
			if (($dbParameter = $app->getModule($this->_dbParameter)) === null) {
247
				throw new TConfigurationException('permissions_dbparameter_nonexistent', $this->_dbParameter);
248
			}
249
			if (!($dbParameter instanceof TDbParameterModule)) {
250
				throw new TConfigurationException('permissions_dbparameter_invalid', $this->_dbParameter);
251
			}
252
			$this->_dbParameter = $dbParameter;
253
		}
254
255
		if ($this->_initialized) {
256
			throw new TInvalidOperationException('permissions_init_once');
257
		}
258
		$this->_initialized = true;
259
		self::$_singleton = $this;
260
261
		$manager = \WeakReference::create($this);
262
		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

262
		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

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

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

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

494
	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

494
	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...
495
	{
496
		if ($this->dyRegisterShellAction(false) !== true && ($app = $this->getApplication()) instanceof \Prado\Shell\TShellApplication) {
497
			$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

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

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

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