Auth: Add support for denied permissions
This commit is contained in:
parent
b2f7c3788d
commit
87d741265e
|
@ -23,6 +23,11 @@ class RoleForm extends RepositoryForm
|
|||
*/
|
||||
const WILDCARD_NAME = 'allAndEverything';
|
||||
|
||||
/**
|
||||
* The prefix used to deny a permission
|
||||
*/
|
||||
const DENY_PREFIX = 'no-';
|
||||
|
||||
/**
|
||||
* Provided permissions by currently installed modules
|
||||
*
|
||||
|
@ -196,8 +201,15 @@ class RoleForm extends RepositoryForm
|
|||
|
||||
$elements[] = 'permission_header';
|
||||
$this->addElement('note', 'permission_header', [
|
||||
'value' => '<h4>' . $this->translate('Permissions') . '</h4>',
|
||||
'decorators' => ['ViewHelper']
|
||||
'decorators' => [['Callback', ['callback' => function () {
|
||||
return '<h4>' . $this->translate('Permissions') . '</h4>'
|
||||
. $this->getView()->icon('ok', $this->translate(
|
||||
'Grant access by toggling a switch below'
|
||||
))
|
||||
. $this->getView()->icon('cancel', $this->translate(
|
||||
'Deny access by toggling a switch below'
|
||||
));
|
||||
}]], ['HtmlTag', ['tag' => 'div']]]
|
||||
]);
|
||||
|
||||
$hasFullPerm = false;
|
||||
|
@ -207,6 +219,17 @@ class RoleForm extends RepositoryForm
|
|||
$elementName .= '_fake';
|
||||
}
|
||||
|
||||
$denyCheckbox = null;
|
||||
if (! isset($spec['isFullPerm'])
|
||||
&& substr($spec['name'], 0, strlen(self::DENY_PREFIX)) !== self::DENY_PREFIX
|
||||
) {
|
||||
$denyCheckbox = $this->createElement('checkbox', self::DENY_PREFIX . $name, [
|
||||
'decorators' => ['ViewHelper']
|
||||
]);
|
||||
$this->addElement($denyCheckbox);
|
||||
$this->removeFromIteration($denyCheckbox->getName());
|
||||
}
|
||||
|
||||
$elements[] = $elementName;
|
||||
$this->addElement(
|
||||
'checkbox',
|
||||
|
@ -222,7 +245,14 @@ class RoleForm extends RepositoryForm
|
|||
'/​',
|
||||
isset($spec['label']) ? $spec['label'] : $spec['name']
|
||||
),
|
||||
'description' => isset($spec['description']) ? $spec['description'] : $spec['name']
|
||||
'description' => isset($spec['description']) ? $spec['description'] : $spec['name'],
|
||||
'decorators' => array_merge(
|
||||
array_slice(self::$defaultElementDecorators, 0, 3),
|
||||
[['Callback', ['callback' => function () use ($denyCheckbox) {
|
||||
return $denyCheckbox ? $denyCheckbox->render() : '';
|
||||
}]]],
|
||||
array_slice(self::$defaultElementDecorators, 3)
|
||||
)
|
||||
]
|
||||
)
|
||||
->getElement($elementName)
|
||||
|
@ -298,13 +328,18 @@ class RoleForm extends RepositoryForm
|
|||
self::WILDCARD_NAME => (bool) preg_match('~(?<!/)\*~', $role->permissions)
|
||||
];
|
||||
|
||||
if (! empty($role->permissions) && $role->permissions !== '*') {
|
||||
if (! empty($role->permissions) || ! empty($role->refusals)) {
|
||||
$permissions = StringHelper::trimSplit($role->permissions);
|
||||
$refusals = StringHelper::trimSplit($role->refusals);
|
||||
foreach ($this->providedPermissions as $moduleName => $permissionList) {
|
||||
foreach ($permissionList as $name => $spec) {
|
||||
if (in_array($spec['name'], $permissions, true)) {
|
||||
$values[$name] = 1;
|
||||
}
|
||||
|
||||
if (in_array($spec['name'], $refusals, true)) {
|
||||
$values[$this->filterName(self::DENY_PREFIX . $name)] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -338,17 +373,24 @@ class RoleForm extends RepositoryForm
|
|||
$permissions[] = '*';
|
||||
}
|
||||
|
||||
$refusals = [];
|
||||
foreach ($this->providedPermissions as $moduleName => $permissionList) {
|
||||
foreach ($permissionList as $name => $spec) {
|
||||
if (isset($values[$name]) && $values[$name]) {
|
||||
$permissions[] = $spec['name'];
|
||||
}
|
||||
|
||||
unset($values[$name]);
|
||||
$denyName = $this->filterName(self::DENY_PREFIX . $name);
|
||||
if (isset($values[$denyName]) && $values[$denyName]) {
|
||||
$refusals[] = $spec['name'];
|
||||
}
|
||||
|
||||
unset($values[$name], $values[$denyName]);
|
||||
}
|
||||
}
|
||||
|
||||
unset($values[self::WILDCARD_NAME]);
|
||||
$values['refusals'] = join(',', $refusals);
|
||||
$values['permissions'] = join(',', $permissions);
|
||||
return ConfigForm::transformEmptyValuesToNull($values);
|
||||
}
|
||||
|
|
|
@ -71,6 +71,7 @@ class AdmissionLoader
|
|||
foreach ($roles as $roleName => $role) {
|
||||
if ($this->match($username, $userGroups, $role)) {
|
||||
$permissionsFromRole = StringHelper::trimSplit($role->permissions);
|
||||
$refusals = StringHelper::trimSplit($role->refusals);
|
||||
$permissions = array_merge(
|
||||
$permissions,
|
||||
array_diff($permissionsFromRole, $permissions)
|
||||
|
@ -78,6 +79,7 @@ class AdmissionLoader
|
|||
$restrictionsFromRole = $role->toArray();
|
||||
unset($restrictionsFromRole['users']);
|
||||
unset($restrictionsFromRole['groups']);
|
||||
unset($restrictionsFromRole['refusals']);
|
||||
unset($restrictionsFromRole['permissions']);
|
||||
foreach ($restrictionsFromRole as $name => $restriction) {
|
||||
if (! isset($restrictions[$name])) {
|
||||
|
@ -89,6 +91,7 @@ class AdmissionLoader
|
|||
$roleObj = new Role();
|
||||
$roleObjs[] = $roleObj
|
||||
->setName($roleName)
|
||||
->setRefusals($refusals)
|
||||
->setPermissions($permissionsFromRole)
|
||||
->setRestrictions($restrictionsFromRole);
|
||||
}
|
||||
|
|
|
@ -17,14 +17,21 @@ class Role
|
|||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $permissions = array();
|
||||
protected $permissions = [];
|
||||
|
||||
/**
|
||||
* Refusals of the role
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $refusals = [];
|
||||
|
||||
/**
|
||||
* Restrictions of the role
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $restrictions = array();
|
||||
protected $restrictions = [];
|
||||
|
||||
/**
|
||||
* Get the name of the role
|
||||
|
@ -46,6 +53,7 @@ class Role
|
|||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -69,6 +77,31 @@ class Role
|
|||
public function setPermissions(array $permissions)
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the refusals of the role
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getRefusals()
|
||||
{
|
||||
return $this->refusals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the refusals of the role
|
||||
*
|
||||
* @param array $refusals
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setRefusals(array $refusals)
|
||||
{
|
||||
$this->refusals = $refusals;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -104,6 +137,7 @@ class Role
|
|||
public function setRestrictions(array $restrictions)
|
||||
{
|
||||
$this->restrictions = $restrictions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -116,31 +150,67 @@ class Role
|
|||
*/
|
||||
public function grants($permission)
|
||||
{
|
||||
$requiredWildcard = strpos($permission, '*');
|
||||
foreach ($this->permissions as $grantedPermission) {
|
||||
if ($grantedPermission === '*' || $grantedPermission === $permission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($requiredWildcard !== false) {
|
||||
if (($grantedWildcard = strpos($grantedPermission, '*')) !== false) {
|
||||
$wildcard = min($requiredWildcard, $grantedWildcard);
|
||||
} else {
|
||||
$wildcard = $requiredWildcard;
|
||||
}
|
||||
} else {
|
||||
$wildcard = strpos($grantedPermission, '*');
|
||||
}
|
||||
|
||||
if ($wildcard !== false && $wildcard > 0) {
|
||||
if (substr($permission, 0, $wildcard) === substr($grantedPermission, 0, $wildcard)) {
|
||||
return true;
|
||||
}
|
||||
} elseif ($permission === $grantedPermission) {
|
||||
if ($this->match($grantedPermission, $permission)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this role denies the given permission
|
||||
*
|
||||
* @param string $permission
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function denies($permission)
|
||||
{
|
||||
foreach ($this->refusals as $refusedPermission) {
|
||||
if ($this->match($refusedPermission, $permission, false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the role expression matches the required permission
|
||||
*
|
||||
* @param string $roleExpression
|
||||
* @param string $requiredPermission
|
||||
* @param bool $cascadeUpwards `false` if `foo/bar/*` and `foo/bar/raboof` should not match `foo/*`
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function match($roleExpression, $requiredPermission, $cascadeUpwards = true)
|
||||
{
|
||||
if ($roleExpression === '*' || $roleExpression === $requiredPermission) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$requiredWildcard = strpos($requiredPermission, '*');
|
||||
if ($requiredWildcard !== false) {
|
||||
if (($grantedWildcard = strpos($roleExpression, '*')) !== false) {
|
||||
$wildcard = $cascadeUpwards ? min($requiredWildcard, $grantedWildcard) : $grantedWildcard;
|
||||
} else {
|
||||
$wildcard = $cascadeUpwards ? $requiredWildcard : false;
|
||||
}
|
||||
} else {
|
||||
$wildcard = strpos($roleExpression, '*');
|
||||
}
|
||||
|
||||
if ($wildcard !== false && $wildcard > 0) {
|
||||
if (substr($requiredPermission, 0, $wildcard) === substr($roleExpression, 0, $wildcard)) {
|
||||
return true;
|
||||
}
|
||||
} elseif ($requiredPermission === $roleExpression) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ class RolesConfig extends IniRepository
|
|||
'name',
|
||||
'users',
|
||||
'groups',
|
||||
'refusals',
|
||||
'permissions',
|
||||
'application/share/users',
|
||||
'application/share/groups'
|
||||
|
|
|
@ -563,13 +563,18 @@ class User
|
|||
*/
|
||||
public function can($requiredPermission)
|
||||
{
|
||||
$granted = false;
|
||||
foreach ($this->getRoles() as $role) {
|
||||
if ($role->grants($requiredPermission)) {
|
||||
return true;
|
||||
if ($role->denies($requiredPermission)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $granted && $role->grants($requiredPermission)) {
|
||||
$granted = true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return $granted;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -242,7 +242,26 @@ form.role-form {
|
|||
}
|
||||
|
||||
h4 {
|
||||
display: inline-block;
|
||||
width: 20em;
|
||||
margin-top: 1.5em;
|
||||
padding-right: .5625em;
|
||||
text-align: right;
|
||||
|
||||
& ~ i {
|
||||
display: inline-block;
|
||||
width: 2.625em;
|
||||
margin-right: 1em;
|
||||
text-align: center;
|
||||
|
||||
&.icon-ok {
|
||||
color: @color-ok;
|
||||
}
|
||||
|
||||
&.icon-cancel {
|
||||
color: @color-critical;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapsible-control {
|
||||
|
|
|
@ -76,6 +76,7 @@ class UserTest extends BaseTestCase
|
|||
$user->setRoles([$role]);
|
||||
|
||||
$this->assertTrue($user->can('test'));
|
||||
$this->assertTrue($user->can('test/some/*'));
|
||||
$this->assertTrue($user->can('test/some/specific'));
|
||||
$this->assertTrue($user->can('test/more/everything'));
|
||||
$this->assertTrue($user->can('test/wildcard-with-wildcard/*'));
|
||||
|
@ -85,4 +86,48 @@ class UserTest extends BaseTestCase
|
|||
$this->assertFalse($user->can('test/some/not/so/specific'));
|
||||
$this->assertFalse($user->can('test/wildcard2/*'));
|
||||
}
|
||||
|
||||
public function testRefusals()
|
||||
{
|
||||
$role = new Role();
|
||||
$role->setPermissions([
|
||||
'a',
|
||||
'a/b/*',
|
||||
'a/b/c/d',
|
||||
'c/*',
|
||||
'd/*'
|
||||
]);
|
||||
$role->setRefusals([
|
||||
'a/b/c',
|
||||
'a/b/e',
|
||||
'c/b/a',
|
||||
'c/d/*',
|
||||
'd/f',
|
||||
'e/g'
|
||||
]);
|
||||
|
||||
$user = new User('test');
|
||||
$user->setRoles([$role]);
|
||||
|
||||
$this->assertFalse($user->can('a/b/c'));
|
||||
$this->assertFalse($user->can('a/b/e'));
|
||||
$this->assertTrue($user->can('a/b/d'));
|
||||
$this->assertTrue($user->can('a/b/c/d'));
|
||||
$this->assertFalse($user->can('c/b/a'));
|
||||
$this->assertTrue($user->can('c/b/d'));
|
||||
$this->assertFalse($user->can('c/d/u'));
|
||||
$this->assertFalse($user->can('c/d/*'));
|
||||
$this->assertTrue($user->can('c/*'));
|
||||
$this->assertTrue($user->can('d/*'));
|
||||
$this->assertFalse($user->can('e/*'));
|
||||
|
||||
$secondRole = new Role();
|
||||
$role->setRefusals(['a/b/*']);
|
||||
|
||||
$user->setRoles([$role, $secondRole]);
|
||||
|
||||
$this->assertFalse($user->can('a/b/d'));
|
||||
$this->assertFalse($user->can('a/b/c/d'));
|
||||
$this->assertTrue($user->can('c/b/d'));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue