%PDF- %PDF-
| Direktori : /www/varak.net/losik.varak.net/vendor/nette/security/src/Security/ |
| Current File : //www/varak.net/losik.varak.net/vendor/nette/security/src/Security/Permission.php |
<?php
/**
* This file is part of the Nette Framework (https://nette.org)
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
*/
declare(strict_types=1);
namespace Nette\Security;
use Nette;
/**
* Access control list (ACL) functionality and privileges management.
*
* This solution is mostly based on Zend_Acl (c) Zend Technologies USA Inc. (https://www.zend.com), new BSD license
*/
class Permission implements Authorizator
{
use Nette\SmartObject;
/** @var array Role storage */
private $roles = [];
/** @var array Resource storage */
private $resources = [];
/** @var array Access Control List rules; whitelist (deny everything to all) by default */
private $rules = [
'allResources' => [
'allRoles' => [
'allPrivileges' => [
'type' => self::DENY,
'assert' => null,
],
'byPrivilege' => [],
],
'byRole' => [],
],
'byResource' => [],
];
/** @var mixed */
private $queriedRole;
private $queriedResource;
/********************* roles ****************d*g**/
/**
* Adds a Role to the list. The most recently added parent
* takes precedence over parents that were previously added.
* @param string|array $parents
* @throws Nette\InvalidArgumentException
* @throws Nette\InvalidStateException
* @return static
*/
public function addRole(string $role, $parents = null)
{
$this->checkRole($role, false);
if (isset($this->roles[$role])) {
throw new Nette\InvalidStateException("Role '$role' already exists in the list.");
}
$roleParents = [];
if ($parents !== null) {
if (!is_array($parents)) {
$parents = [$parents];
}
foreach ($parents as $parent) {
$this->checkRole($parent);
$roleParents[$parent] = true;
$this->roles[$parent]['children'][$role] = true;
}
}
$this->roles[$role] = [
'parents' => $roleParents,
'children' => [],
];
return $this;
}
/**
* Returns true if the Role exists in the list.
*/
public function hasRole(string $role): bool
{
$this->checkRole($role, false);
return isset($this->roles[$role]);
}
/**
* Checks whether Role is valid and exists in the list.
* @throws Nette\InvalidStateException
*/
private function checkRole(string $role, bool $exists = true): void
{
if ($role === '') {
throw new Nette\InvalidArgumentException('Role must be a non-empty string.');
} elseif ($exists && !isset($this->roles[$role])) {
throw new Nette\InvalidStateException("Role '$role' does not exist.");
}
}
/**
* Returns all Roles.
*/
public function getRoles(): array
{
return array_keys($this->roles);
}
/**
* Returns existing Role's parents ordered by ascending priority.
*/
public function getRoleParents(string $role): array
{
$this->checkRole($role);
return array_keys($this->roles[$role]['parents']);
}
/**
* Returns true if $role inherits from $inherit. If $onlyParents is true,
* then $role must inherit directly from $inherit.
* @throws Nette\InvalidStateException
*/
public function roleInheritsFrom(string $role, string $inherit, bool $onlyParents = false): bool
{
$this->checkRole($role);
$this->checkRole($inherit);
$inherits = isset($this->roles[$role]['parents'][$inherit]);
if ($inherits || $onlyParents) {
return $inherits;
}
foreach ($this->roles[$role]['parents'] as $parent => $foo) {
if ($this->roleInheritsFrom($parent, $inherit)) {
return true;
}
}
return false;
}
/**
* Removes the Role from the list.
*
* @throws Nette\InvalidStateException
* @return static
*/
public function removeRole(string $role)
{
$this->checkRole($role);
foreach ($this->roles[$role]['children'] as $child => $foo) {
unset($this->roles[$child]['parents'][$role]);
}
foreach ($this->roles[$role]['parents'] as $parent => $foo) {
unset($this->roles[$parent]['children'][$role]);
}
unset($this->roles[$role]);
foreach ($this->rules['allResources']['byRole'] as $roleCurrent => $rules) {
if ($role === $roleCurrent) {
unset($this->rules['allResources']['byRole'][$roleCurrent]);
}
}
foreach ($this->rules['byResource'] as $resourceCurrent => $visitor) {
if (isset($visitor['byRole'])) {
foreach ($visitor['byRole'] as $roleCurrent => $rules) {
if ($role === $roleCurrent) {
unset($this->rules['byResource'][$resourceCurrent]['byRole'][$roleCurrent]);
}
}
}
}
return $this;
}
/**
* Removes all Roles from the list.
*
* @return static
*/
public function removeAllRoles()
{
$this->roles = [];
foreach ($this->rules['allResources']['byRole'] as $roleCurrent => $rules) {
unset($this->rules['allResources']['byRole'][$roleCurrent]);
}
foreach ($this->rules['byResource'] as $resourceCurrent => $visitor) {
foreach ($visitor['byRole'] as $roleCurrent => $rules) {
unset($this->rules['byResource'][$resourceCurrent]['byRole'][$roleCurrent]);
}
}
return $this;
}
/********************* resources ****************d*g**/
/**
* Adds a Resource having an identifier unique to the list.
*
* @throws Nette\InvalidArgumentException
* @throws Nette\InvalidStateException
* @return static
*/
public function addResource(string $resource, ?string $parent = null)
{
$this->checkResource($resource, false);
if (isset($this->resources[$resource])) {
throw new Nette\InvalidStateException("Resource '$resource' already exists in the list.");
}
if ($parent !== null) {
$this->checkResource($parent);
$this->resources[$parent]['children'][$resource] = true;
}
$this->resources[$resource] = [
'parent' => $parent,
'children' => [],
];
return $this;
}
/**
* Returns true if the Resource exists in the list.
*/
public function hasResource(string $resource): bool
{
$this->checkResource($resource, false);
return isset($this->resources[$resource]);
}
/**
* Checks whether Resource is valid and exists in the list.
* @throws Nette\InvalidStateException
*/
private function checkResource(string $resource, bool $exists = true): void
{
if ($resource === '') {
throw new Nette\InvalidArgumentException('Resource must be a non-empty string.');
} elseif ($exists && !isset($this->resources[$resource])) {
throw new Nette\InvalidStateException("Resource '$resource' does not exist.");
}
}
/**
* Returns all Resources.
*/
public function getResources(): array
{
return array_keys($this->resources);
}
/**
* Returns true if $resource inherits from $inherit. If $onlyParents is true,
* then $resource must inherit directly from $inherit.
*
* @throws Nette\InvalidStateException
*/
public function resourceInheritsFrom(string $resource, string $inherit, bool $onlyParent = false): bool
{
$this->checkResource($resource);
$this->checkResource($inherit);
if ($this->resources[$resource]['parent'] === null) {
return false;
}
$parent = $this->resources[$resource]['parent'];
if ($inherit === $parent) {
return true;
} elseif ($onlyParent) {
return false;
}
while ($this->resources[$parent]['parent'] !== null) {
$parent = $this->resources[$parent]['parent'];
if ($inherit === $parent) {
return true;
}
}
return false;
}
/**
* Removes a Resource and all of its children.
*
* @throws Nette\InvalidStateException
* @return static
*/
public function removeResource(string $resource)
{
$this->checkResource($resource);
$parent = $this->resources[$resource]['parent'];
if ($parent !== null) {
unset($this->resources[$parent]['children'][$resource]);
}
$removed = [$resource];
foreach ($this->resources[$resource]['children'] as $child => $foo) {
$this->removeResource($child);
$removed[] = $child;
}
foreach ($removed as $resourceRemoved) {
foreach ($this->rules['byResource'] as $resourceCurrent => $rules) {
if ($resourceRemoved === $resourceCurrent) {
unset($this->rules['byResource'][$resourceCurrent]);
}
}
}
unset($this->resources[$resource]);
return $this;
}
/**
* Removes all Resources.
* @return static
*/
public function removeAllResources()
{
foreach ($this->resources as $resource => $foo) {
foreach ($this->rules['byResource'] as $resourceCurrent => $rules) {
if ($resource === $resourceCurrent) {
unset($this->rules['byResource'][$resourceCurrent]);
}
}
}
$this->resources = [];
return $this;
}
/********************* defining rules ****************d*g**/
/**
* Allows one or more Roles access to [certain $privileges upon] the specified Resource(s).
* If $assertion is provided, then it must return true in order for rule to apply.
*
* @param string|string[]|null $roles
* @param string|string[]|null $resources
* @param string|string[]|null $privileges
* @return static
*/
public function allow(
$roles = self::ALL,
$resources = self::ALL,
$privileges = self::ALL,
?callable $assertion = null
) {
$this->setRule(true, self::ALLOW, $roles, $resources, $privileges, $assertion);
return $this;
}
/**
* Denies one or more Roles access to [certain $privileges upon] the specified Resource(s).
* If $assertion is provided, then it must return true in order for rule to apply.
*
* @param string|string[]|null $roles
* @param string|string[]|null $resources
* @param string|string[]|null $privileges
* @return static
*/
public function deny(
$roles = self::ALL,
$resources = self::ALL,
$privileges = self::ALL,
?callable $assertion = null
) {
$this->setRule(true, self::DENY, $roles, $resources, $privileges, $assertion);
return $this;
}
/**
* Removes "allow" permissions from the list in the context of the given Roles, Resources, and privileges.
*
* @param string|string[]|null $roles
* @param string|string[]|null $resources
* @param string|string[]|null $privileges
* @return static
*/
public function removeAllow($roles = self::ALL, $resources = self::ALL, $privileges = self::ALL)
{
$this->setRule(false, self::ALLOW, $roles, $resources, $privileges);
return $this;
}
/**
* Removes "deny" restrictions from the list in the context of the given Roles, Resources, and privileges.
*
* @param string|string[]|null $roles
* @param string|string[]|null $resources
* @param string|string[]|null $privileges
* @return static
*/
public function removeDeny($roles = self::ALL, $resources = self::ALL, $privileges = self::ALL)
{
$this->setRule(false, self::DENY, $roles, $resources, $privileges);
return $this;
}
/**
* Performs operations on Access Control List rules.
* @param string|string[]|null $roles
* @param string|string[]|null $resources
* @param string|string[]|null $privileges
* @throws Nette\InvalidStateException
* @return static
*/
protected function setRule(bool $toAdd, bool $type, $roles, $resources, $privileges, ?callable $assertion = null)
{
// ensure that all specified Roles exist; normalize input to array of Roles or null
if ($roles === self::ALL) {
$roles = [self::ALL];
} else {
if (!is_array($roles)) {
$roles = [$roles];
}
foreach ($roles as $role) {
$this->checkRole($role);
}
}
// ensure that all specified Resources exist; normalize input to array of Resources or null
if ($resources === self::ALL) {
$resources = [self::ALL];
} else {
if (!is_array($resources)) {
$resources = [$resources];
}
foreach ($resources as $resource) {
$this->checkResource($resource);
}
}
// normalize privileges to array
if ($privileges === self::ALL) {
$privileges = [];
} elseif (!is_array($privileges)) {
$privileges = [$privileges];
}
if ($toAdd) { // add to the rules
foreach ($resources as $resource) {
foreach ($roles as $role) {
$rules = &$this->getRules($resource, $role, true);
if (count($privileges) === 0) {
$rules['allPrivileges']['type'] = $type;
$rules['allPrivileges']['assert'] = $assertion;
if (!isset($rules['byPrivilege'])) {
$rules['byPrivilege'] = [];
}
} else {
foreach ($privileges as $privilege) {
$rules['byPrivilege'][$privilege]['type'] = $type;
$rules['byPrivilege'][$privilege]['assert'] = $assertion;
}
}
}
}
} else { // remove from the rules
foreach ($resources as $resource) {
foreach ($roles as $role) {
$rules = &$this->getRules($resource, $role);
if ($rules === null) {
continue;
}
if (count($privileges) === 0) {
if ($resource === self::ALL && $role === self::ALL) {
if ($type === $rules['allPrivileges']['type']) {
$rules = [
'allPrivileges' => [
'type' => self::DENY,
'assert' => null,
],
'byPrivilege' => [],
];
}
continue;
}
if ($type === $rules['allPrivileges']['type']) {
unset($rules['allPrivileges']);
}
} else {
foreach ($privileges as $privilege) {
if (isset($rules['byPrivilege'][$privilege]) &&
$type === $rules['byPrivilege'][$privilege]['type']
) {
unset($rules['byPrivilege'][$privilege]);
}
}
}
}
}
}
return $this;
}
/********************* querying the ACL ****************d*g**/
/**
* Returns true if and only if the Role has access to [certain $privileges upon] the Resource.
*
* This method checks Role inheritance using a depth-first traversal of the Role list.
* The highest priority parent (i.e., the parent most recently added) is checked first,
* and its respective parents are checked similarly before the lower-priority parents of
* the Role are checked.
*
* @param string|Role|null $role
* @param string|Nette\Security\Resource|null $resource
* @param string|null $privilege
* @throws Nette\InvalidStateException
*/
public function isAllowed($role = self::ALL, $resource = self::ALL, $privilege = self::ALL): bool
{
$this->queriedRole = $role;
if ($role !== self::ALL) {
if ($role instanceof Role) {
$role = $role->getRoleId();
}
$this->checkRole($role);
}
$this->queriedResource = $resource;
if ($resource !== self::ALL) {
if ($resource instanceof Resource) {
$resource = $resource->getResourceId();
}
$this->checkResource($resource);
}
do {
// depth-first search on $role if it is not 'allRoles' pseudo-parent
if (
$role !== null
&& ($result = $this->searchRolePrivileges($privilege === self::ALL, $role, $resource, $privilege)) !== null
) {
break;
}
if ($privilege === self::ALL) {
if ($rules = $this->getRules($resource, self::ALL)) { // look for rule on 'allRoles' psuedo-parent
foreach ($rules['byPrivilege'] as $privilege => $rule) {
if (($result = $this->getRuleType($resource, null, $privilege)) === self::DENY) {
break 2;
}
}
if (($result = $this->getRuleType($resource, null, null)) !== null) {
break;
}
}
} elseif (($result = $this->getRuleType($resource, null, $privilege)) !== null) { // look for rule on 'allRoles' pseudo-parent
break;
} elseif (($result = $this->getRuleType($resource, null, null)) !== null) {
break;
}
$resource = $this->resources[$resource]['parent']; // try next Resource
} while (true);
$this->queriedRole = $this->queriedResource = null;
return $result ?? false;
}
/**
* Returns real currently queried Role. Use by assertion.
* @return mixed
*/
public function getQueriedRole()
{
return $this->queriedRole;
}
/**
* Returns real currently queried Resource. Use by assertion.
* @return mixed
*/
public function getQueriedResource()
{
return $this->queriedResource;
}
/********************* internals ****************d*g**/
/**
* Performs a depth-first search of the Role DAG, starting at $role, in order to find a rule
* allowing/denying $role access to a/all $privilege upon $resource.
* @param bool $all (true) or one?
* @return mixed null if no applicable rule is found, otherwise returns ALLOW or DENY
*/
private function searchRolePrivileges(bool $all, $role, $resource, $privilege)
{
$dfs = [
'visited' => [],
'stack' => [$role],
];
while (($role = array_pop($dfs['stack'])) !== null) {
if (isset($dfs['visited'][$role])) {
continue;
}
if ($all) {
if ($rules = $this->getRules($resource, $role)) {
foreach ($rules['byPrivilege'] as $privilege2 => $rule) {
if ($this->getRuleType($resource, $role, $privilege2) === self::DENY) {
return self::DENY;
}
}
if (($type = $this->getRuleType($resource, $role, null)) !== null) {
return $type;
}
}
} else {
if (($type = $this->getRuleType($resource, $role, $privilege)) !== null) {
return $type;
} elseif (($type = $this->getRuleType($resource, $role, null)) !== null) {
return $type;
}
}
$dfs['visited'][$role] = true;
foreach ($this->roles[$role]['parents'] as $roleParent => $foo) {
$dfs['stack'][] = $roleParent;
}
}
return null;
}
/**
* Returns the rule type associated with the specified Resource, Role, and privilege.
* @param string|null $resource
* @param string|null $role
* @param string|null $privilege
* @return bool|null null if a rule does not exist or assertion fails, otherwise returns ALLOW or DENY
*/
private function getRuleType($resource, $role, $privilege): ?bool
{
if (!$rules = $this->getRules($resource, $role)) {
return null;
}
if ($privilege === self::ALL) {
if (isset($rules['allPrivileges'])) {
$rule = $rules['allPrivileges'];
} else {
return null;
}
} elseif (!isset($rules['byPrivilege'][$privilege])) {
return null;
} else {
$rule = $rules['byPrivilege'][$privilege];
}
if ($rule['assert'] === null || $rule['assert']($this, $role, $resource, $privilege)) {
return $rule['type'];
} elseif ($resource !== self::ALL || $role !== self::ALL || $privilege !== self::ALL) {
return null;
} elseif ($rule['type'] === self::ALLOW) {
return self::DENY;
} else {
return self::ALLOW;
}
}
/**
* Returns the rules associated with a Resource and a Role, or null if no such rules exist.
* If the $create parameter is true, then a rule set is first created and then returned to the caller.
* @param string|null $resource
* @param string|null $role
*/
private function &getRules($resource, $role, bool $create = false): ?array
{
$null = null;
if ($resource === self::ALL) {
$visitor = &$this->rules['allResources'];
} else {
if (!isset($this->rules['byResource'][$resource])) {
if (!$create) {
return $null;
}
$this->rules['byResource'][$resource] = [];
}
$visitor = &$this->rules['byResource'][$resource];
}
if ($role === self::ALL) {
if (!isset($visitor['allRoles'])) {
if (!$create) {
return $null;
}
$visitor['allRoles']['byPrivilege'] = [];
}
return $visitor['allRoles'];
}
if (!isset($visitor['byRole'][$role])) {
if (!$create) {
return $null;
}
$visitor['byRole'][$role]['byPrivilege'] = [];
}
return $visitor['byRole'][$role];
}
}