Pull to refresh

Система контроля доступа на cakePHP.

Reading time 9 min
Views 1K
Как вы знаете существуют различные системы контроля доступа.
Некторые из них простые, реализованные только на основе сессий, другие же сложные, такие как 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, тоесть без перезагрузки (при этом обновляем кеш)
Tags:
Hubs:
+7
Comments 10
Comments Comments 10

Articles