Как вы знаете существуют различные системы контроля доступа.
Некторые из них простые, реализованные только на основе сессий, другие же сложные, такие как ACL. Каждая из них имеет свои плюсы и минусы. Простые системы легки в понимании и в обращении, но при увеличении количества привелегий и необходимости их динамического изменения будут возникать и соотвествующие трудности, ACL же довольно громоздка, не столь гибка и сложна в понимании. Долгое время использовав обе системы, пришел к выводу, что нужно разработать свою систему контроля доступом, которая бы имела следующие возможности:
  • простота понимания и простота исполнения
  • динамическое сосздание групп\ролей и перемещение юзера по ролям\группам
  • юзер может состоять в любом количестве групп\ролей
  • легкое, быстрое и понятное изменение доступа
  • минимизация кода при использовании системы
  • мимнимизация размера таблиц связанных с контролем доступа
  • минимизация количества запросов к БД


Читая существующие топики о системах разделения доступа часто натыкался на коментарии: «Зачем городить огород, если есть ACL». Сразу же отвечу чем меня не устраивает ACL.
  • сложность понимания
  • юзер не может быть в нескольких группах одновременно
  • при необходимости ограничения доступа отдельныи юзерам сильно раздуваются таблички (необходимо хранить id каждого юзера )
  • сложность с перемещением юзеров по группам
  • отсутсвие кеширования
  • необходимость делать визуализацию для работы с группами и правами
  • своя тельняжка ближе к телу (в том смысле, что свой код легче потдерживать)



И так начнем с теории.
Структура таблиц


Описание таблиц

Users — тут все просто, эта таблица служит для хранения юзеров, ключевые поля для нас тут id, lgn, pwd. В этой таблице первой записью с id = 0 будет идти юзер по умолчанию, так сказать visitor.

Groups — это группы (к примеру User, Admin,Manager)

Statuses — каждая группа состоит из статусов. Грубо говоря каждый юзер входит в ту или иную группу с определенным статусом. Юзер может состоять в нескольктх группах с различными статусами, но в пределах одной группы юзер может иметь только один статус. К примеру следующие группы имеют статусы User — visitor, active, new; Admin — active,superadmin,deleted; Manager — active, blocked; Visitor (id = 0) будет принадлжеать группе User и быть в статусе visitor. В каждой группе есть статус по умолчанию — defstats_id — остальные статусы могут наследовать права доступа у статуса по умолчанию, а могут иметь и свои права на определенный объект.

Users_statuses — таблица для связывания user и status

Objects — хранит все объекты безопасности (к примеру объектом безопасности может выступать кнопка регистрации, которая показывается только visitors)

objects_categories — первый же опыт эксплуатации данной системы показал, что объекты безопасности необходимо как-то организовывать. Для этого и была введена дополнителная таблица, которая служит лишь для организации (при отображении) объектов безопасности. К примеру, у нас среди объектов безопасности будут такие категории — default — все новые объекты попадают именно в эту категорию ( а затем администратор сам переносит объект в другую категорию), buttons — все объекты связанные с кнопками, links — объекты связанные с ссылками ну и так далее, насколько позволяет вам ваша фантазия и задача.

Access — это ключевая таблица нашей системы. Она связывает объекты безопасности, статусы и привелегии. Эта таблица имеет пять основных столбцов для разграничения типа запрашиваемого доступа:
c — доступ на создание (к примеру проверка доступа на создание новой записи в блоге)
u — доступ на изменение (к примеру проверка доступа на редактирование записи в блоге)
r — доступ на чтение (к примеру проверка доступа на чтение записи в блоге)
d — доступ на удаление (к примеру проверка доступа на удаление записи из блога)
l — доступ на показ списка (к примеру проверка доступа на отображение всех записей в блоге или показ всех юзеров)

Эти поля могут принимать следующие значения:
0 — доступ запрещен
1 — доступ разрешен только владельцу
2 — доступ разрешен всем

В данной таблице описываются статусы по умолчанию каждой группы, все остальные статусы, наследуют права доступа у статуса по умолчанию своей группы. Если статус должен иметь свои собственные права доступа ( ну к примеру стутус blocked имеет все права default статуса active, за исключением того, что он не может добавить новые записи в блог), то в таблицу access бдобавляется строчка с соотвествующим status_id.

как система работает

разберем логику работы системы. Проверку доступа будет выполнять функция getAccess (object,access,owners );
параметры функции
object — имя объекта на которые запрашивается доступ.;
access — тип доступа (может быть с,u,r,d,l );
owners — это необязательный параметр, который может быть либо значением типа int — id юзера, либо списком id юзеров. На данный момент оставим этот необязательный параметр без внимания — позже станет понятно как он используется системой.

Стоит сделать оговорку, что нам всегда известно id текущего юзера — храним его в сессии, для незалогированного юзера — это юзер с id =0 и состоящим в группе user со статусом visitor.


И так как же работает система.
1. проверяем есть ли в таблице objects объект с таким именем, если нету то создаем и запоминаем его id

2. из сессии берем id текущего юзера и смотрим в каких статусах находится данный юзер (в реализации, id статусов текущего юзера хранится в сессии)

3. согласно нужному типу достпуа и в зависимости от настройки системы (слабое, сильное) берем max или min значения по нужному полю доступа (r,c,d,u,l) для соотвествующих статусов. Если статуса не оказалось в таблице access, то берем def_status соответствующей группы.

4. Возможные результаты — 0 доступ запрещен, 2 — доступ разрешен всем, 1 — доступ разрешен только владельцам. Если доступ разрешен только владельцам то:
проверяем пустой или нет последний параметр функции checkAccess — owners, если он пустой то доступ разрешаем\запрещаем (по желанию). Если он не пустой и типа int, то проверяем его с id юзера из сессии (текущего юзера). Если они равны, то доступ разрешен, иначе запрещен. Если параметр owners — массив, то проверяем входит ли id текущего юзера (из сессии) в этот массив (in_array), если вхождение есть, то доступ разрешен иначе запрещен.

Вот собственно и весь алгоритм работы.

Пример использования

Пусть у нас есть запись в блоге, которую создал юзер creator_id. Мы хотим отобразить кнопку «редактировать запись» для одних юзеров и скрыть для других.
Все очень просто пишем if (checkAccess('BlogPost','u',$post['creator_id']) ) { echo КНОПКА}
К примеру, Юзер находится в двух группах и статусы имеют следующие привелегии на Update
User\active -1;
Admin\active -2;
согласно настройкам системы мы берем max (либо min) и возвращаем результат

Реализация

Реализована система на cakePHP с кешированием запросов в виде компонента. Достаточно кешировать четыре основные таблички: access, statuses, groups, objects, чтобы не возникало ни единого запроса при проверке доступа. (эти таблички небольшие, так что много места не займут и мы можем позволить закешировать их целиком).Так же были использованы несколько controller/model/view — чисто для визуализации управления системой.

Загружаем таблички в кеш (cake позволяет использовать два вида кеша: файловый и memcached, так что без кеша мы уж точно не останемся, при любых серверах).
Для удобства работы нам необходимо закешировать так, чтобы массивы выглядели следующим образом:
Access
permissions[status_id][object_id][access_type] = permission;
пример:
permissions[status_id][object_id][с] = 1;
permissions[status_id][object_id][d] = 1;
permissions[status_id][object_id][r] = 1;
permissions[status_id][object_id][u] = 1;
permissions[status_id][object_id][l] = 1;

Statuses
statuses[status_id] = group_id тоесть каждый статус хранит в какой он группе

Groups
groups[group_id] = def_status_id тоесть каждая группа хранит свой default статус
Objects
object[object_name] = object_id

Реализация основных функций:
function getAccess($objName = "",$accessType = "r",$authorID=NULL) {

     $objectID = 0;
     $isAccess = true;
  
    /*Getting User ID*/
    if ($this->Session->check('loggedUser')) {
      $userSession = $this->Session->read('loggedUser');
      $userID = $userSession['id'];
    } else {
      $userID = VISITOR_USER;
    }

    /*Check access  
      * 0 - deny;
    * 1 - allow only for author;
    * 2  - allow for ALL;
    */
    $isAccess = $this->__returnAccess($objName,$accessType);

    if ($isAccess == 2){
      $isAccess = true;
    } elseif($isAccess == 1) {
     /*Check author id*/
      if (is_array($authorID) && in_array($userID,$authorID)){
        $isAccess = true;
      } elseif($userID==$authorID){
        $isAccess = true;
      } else {
        $isAccess = false;
      }
      /*EOF Checking author id*/
    } else {
      $isAccess = false;
    }

    return $isAccess;
  }

основная функция, которая возвращает 1,0 или 2
function __returnAccess($objName = "",$accessType = "r"){

  if (!$this->model){
       $this->__initModel();
     }

  /*Getting User ID*/
  if ($this->Session->check('loggedUser')) {
    $userSession = $this->Session->read('loggedUser');
    $userID = $userSession['id'];
  } else {
    $userID = VISITOR_USER;
  }
        
  /*Getting user statuses*/
  if ($this->Session->check('loggedUserStatuses')) {
    $userStatuses = $this->Session->read('loggedUserStatuses');      
  } else {
    $userStatuses = $this->model->query("SELECT user_id, status_id FROM ".$this->model->tablePrefix."users_statuses AS users_statuses WHERE user_id=".$userID);
    $this->Session->write('loggedUserStatuses',$userStatuses);  
  }

     
  $objectID = $this->getObjIdByName($objName);
    
    if (!$objectID) {
      /*Create new object*/
      $objectID =  $this->__createNewObject($objName);
    }
  //Permissions
  $permissions = Cache::read('permissions');
  if (empty($permissions)) {
    $this->loadobjToCache();
    $permissions = Cache::read('permissions');
  }  
  //Groups
  $groups = Cache::read('groups');
  if (empty($groups)) {
    $this->loadobjToCache();
    $groups = Cache::read('groups');
  }     
  //Statuses
  $statuses = Cache::read('statuses');
  if (empty($statuses)) {
    $this->loadobjToCache();
    $statuses = Cache::read('statuses');
  }     
    
  $isAccess = 0;
    
  foreach ($userStatuses as $userStat) {
    if (isset($permissions[$userStat['users_statuses']['status_id']][$objectID] [$accessType])) {
        
    if (intval($permissions[$userStat['users_statuses']['status_id']][$objectID][$accessType])>$isAccess) {
      $isAccess = intval($permissions[$userStat['users_statuses']['status_id']][$objectID][$accessType]);
    }
        
      } else {
        
    /*Getting group ID*/
    $def_status_id = $groups[$statuses[$userStat['users_statuses']['status_id']]];
        
    if (!isset($def_status_id)) {
      $isAccess = 0;
    } else {
      if (intval($permissions[$def_status_id][$objectID][$accessType])>$isAccess) {
      $isAccess = intval($permissions[$def_status_id][$objectID][$accessType]);
      }
    }
      }
      
  }/*EOF foreach*/
    
  return $isAccess;

}

* This source code was highlighted with Source Code Highlighter.


вот собственнно и все, если кому нужен сам компонент могу предоставить и его. Визуализация управлением доступа прдеставлена на рисунке ниже

по вертикали идут группы, статусы и типы доступа. default статусы подсвечиваются красным. По горизонтали идут объекты, под ними сразу список категорий, для переноса. checkbox — для обозначения, что доступ у данного объекта для данного статуса будет такой же как и у default статуса данной группы.(для них ставим — // --). доступ меням при помощи AJAX, тоесть без перезагрузки (при этом обновляем кеш)