Хотелось бы услышать толковую критику и, может быть, примеры как эффективно реализовать RBAC на SQL.
<?php
/********************************************************************
* - tasks can have sub-tasks and so on. depth is unlimited
* - task unfolding pre-process
* - deny-allow order is hardcoded (always deny first)
********************************************************************/
// Dummy meta-data model
class CPermsModel
{
private function _unfoldTask($entity, &$tasks)
{
$result = array($entity);
foreach ($tasks[$entity] as $e)
if (isset($tasks[$e]))
$result += $this->unfoldTask($e);
else
$result[] = $e;
return $result;
}
public function getRoles()
{
return array('administrator', 'moderator', 'user', 'guest', 'post owner', 'banned');
}
public function getTasks()
{
return array(
'user management' => array('create user', 'modify user', 'delete user'),
'post management' => array('create post', 'modify post', 'delete post')
);
}
public function getGrants()
{
return array(
array('administrator', 'allow', 'user management'),
array('administrator', 'allow', 'post management'),
array('moderator', 'allow', 'post management'),
array('banned', 'deny', 'user management'),
array('banned', 'deny', 'post management'),
// array('user', 'allow', 'post management'),
array('user', 'allow', 'create post'),
array('post owner', 'allow', 'modify post'),
array('post owner', 'allow', 'delete post'),
);
}
public function getUnfoldedGrants()
{
$tasks = $this->getTasks();
$tmp = $this->getGrants();
// Reform grants as grants[role][entity] = permission
$grants = array();
foreach ($tmp as $g)
{
list($role, $perm, $entity) = $g;
if (isset($tasks[$entity]))
{
// Unfold tasks in grant list
foreach ($this->_unfoldTask($entity, $tasks) as $e)
$grants[$role][$e] = $perm;
}
else
$grants[$role][$entity] = $perm;
}
return $grants;
}
}
// Simple permission checker
class CPermsChecker
{
/* — private section – */
private $roles;
private $grants;
//
// Check one role for specified action or task
// returns allow|deny|NULL
private function findGrant($role, $entity)
{
if (isset($this->grants[$role][$entity]))
return $this->grants[$role][$entity];
return NULL;
}
/* — public section – */
function __construct(CPermsModel $model)
{
// Fetch meta-data from database
$this->roles = $model->getRoles();
$this->grants = $model->getUnfoldedGrants();
}
//
// Can it do the action?
// returns allow|deny
// first arg is object having roles or roles themleslf
// second arg is action or task
//
public function can($object, $entity)
{
// It is an object_with_roles_property OR array_of_role_ids OR role_id
if (is_object($object))
$roles = $object->roles;
else if (is_array($object))
$roles = $object;
else
$roles = array($object);
$result = NULL;
// Test every role…
foreach ($roles as $role)
{
$g = $this->findGrant($role, $entity);
// … until first deny rule
if (!is_null($g))
{
$result = $g;
if ($g == 'deny')
break;
}
}
if (is_null($result))
return 'deny';
return $result;
}
//
// Get role_ids array who can|cannot do the action
// first arg is action or task
// second arg is deny|allow
//
public function whoCan($entity, $value = 'allow')
{
$result = array();
foreach ($this->roles as $role)
{
$g = $this->findGrant($role, $entity);
if ($g == $value || (is_null($g) && $value == 'deny'))
$result[] = $role;
}
return $result;
}
}
// Tests:
$model = new CPermsModel;
$p = new CPermsChecker($model);
header('Content-type: text/plain');
echo "By default = deny";
echo "\n\n role - operation";
echo "\n1. ".$p->can('guest', 'delete post');
echo "\n2. ".$p->can('user', 'delete post');
echo "\n3. ".$p->can('post owner', 'delete post');
echo "\n4. ".$p->can('moderator', 'delete post');
echo "\n\n role - task";
echo "\n1. ".$p->can('moderator', 'post management');
echo "\n2. ".$p->can('moderator', 'user management');
echo "\n\n role set - operation";
echo "\n1. ".$p->can(array('user', 'post owner'), 'delete post');
echo "\n2. ".$p->can(array('user', 'banned'), 'delete post');
echo "\n3. ".$p->can(array('user', 'moderator', 'banned'), 'delete post');
echo "\n\n role set - task";
echo "\n1. ".$p->can(array('user'), 'post management');
echo "\n1. ".$p->can(array('user', 'moderator'), 'post management');
echo "\n2. ".$p->can(array('user', 'moderator', 'banned'), 'post management');
echo "\n\n who can";
echo "\ncreate post: ".implode(', ', $p->whoCan('create post'));
echo "\ndelete post: ".implode(', ', $p->whoCan('delete post'));
echo "\npost management: ".implode(', ', $p->whoCan('post management'));
echo "\n\n who cannot";
echo "\ncreate post: ".implode(', ', $p->whoCan('create post', 'deny'));
echo "\ndelete post: ".implode(', ', $p->whoCan('delete post', 'deny'));
echo "\npost management: ".implode(', ', $p->whoCan('post management', 'deny'));
edited: что делается
В моем примере реализуется такая версия RBAC:
- роли не могут быть вложены, но
- задачи могут иметь подзадачи
- что не разрешено, то запрещено
- пользователю могут быть назначены несколько ролей одновременно. класс "пользователь" я не создавал, тестировать можно через массив ролей или через любой объект со свойством roles
Метод can() проверяет доступно ли пользователю (набору ролей) какое-то действие или задача
Метод whoCan() возвращает список ролей, которые дают/запрещают указанное действие или задачу