Search
Write a publication
Pull to refresh

No wheel reinvention или собираем ACL + Auth на PHP

В свете того, что на habr’е появились уже две статьи про ACL — Система разделения прав доступа в веб-приложении — часть 1, теория и Система разделения прав доступа в веб-приложении и обе они сводятся к переизобретению реализации механизмов контроля доступа, то мне хотелось рассказать о другом подходе.

Первая статья – описание довольно спорного подхода. Битовая арифметика это кончено прикольно и все такое ;), но мне бы лично не хотелось с ней часто встречаться, особенно если поддержке, в рамках которой часто нужно менять, то можно пользователям разных групп. Вторая – написана довольно «сумбурно», если можно так сказать. Делать физическую модель сверху вниз (сущности -> атрибуты -> нормализация) для такой маленькой модели вообще смысла нет. Человеку, проектировавшему реальные модели, нужно приложить усилия, чтобы сделать модель ниже 3-ий нормальной формы ;). И в коде (пример использования) чего-то там клонируется, include’ится. IMHO для такой постановки вопроса совсем не KISS ;)

Т.о. два представленных решения не подходят. Чтобы понять, что нужно надо составить требования, к уже обозначенной проблеме.

Проблема: удобный (API) механизм аутентикации и авторизации для интернет-приложения.

Слово «аутентикация» я использую без «фи» т.к. в том слове, от которого оно произошло (authentication) это самое «фи» отсутствует, а еще так говорил один мой преподаватель – сын гордого, южного народа отстаивал именно такое произношение не только среди своих студентов, методом «кнута и пряника», но и среди коллег, но уже другим методом ;).

Чтобы не было путницы, уточню значения этих терминов.

  • Аутентикация – удостоверение перед системой факта «пользователь именно тот, за кого себя выдает»
  • Авторизация – установление возможности осуществить пользователем (аутентицированным) конкретное действие (контролируемое) в системе (Access Control List (ACL) – одна из возможных реализаций)


В рамках ACL существуют следующие объекты:

  • Access Request Object (ARO) – тот, кого нужно контролировать e.g. пользователя
  • Access Control Object (ACO) – тот, что контролируем e.g. доступ в приватную зону сайта


Требования: создать класс, реализующий аутентикацию и авторизацию (ACL) для приложения. Должна быть реализованы группы для ARO с наследованием правил. Правили должны раздаваться как группам, так и пользователям. Правила должны быть двух видов: «разрешить» и «запретить». Должна быть возможность редактировать ACL через графический интерфейс.

Интерфейс для редактирования – довольно удобная штука. В это я убедился на собственном опыте. Опять же это связанно с сопровождением. И именно из-за отсутствия интерфейса я не использую Zend_Acl. Хотя перешел я на Zend Framework позже, чем создал нужный класс, а так может быть и Zend_Acl использовал.

Теперь по поводу «сделать саму» vs «найди/адаптировать готовое». Постольку я уже нашел и адаптировал, будет рассматриваться именно второй вариант.

Использованные компоненты:



Плюсы этих компонент, помимо того, что они готовы и свободно распространяются:

  • Достаточно ясно в реализации (документация/код)
  • PEAR::Auth имеет некоторые feature’и по усилению безопасности (e.g. контроль того, чтобы IP и UserAgent в течении жизни сессии совпадали с значениями на момент ее создания и пр.)
  • PEAR::Auth позволяет писать собственные адаптеры для источников данных
  • phpGACL имеет интерфейс редактирования ACL


Минусы в стандартной комплектации:

  • Обе компоненты используют слои абстракции баз данных не поддерживающий автоинкрементные поля в MySQL из-за чего количество таблиц заметно увеличивается (создаются таблицы-эмуляторы sequence’ов). Кому-то этот минус может показаться несущественным, но я не хочу испытывать страх, всякий раз заглядывая phpMyAdmin’ом в свою базу. Помимо этого в 99%, я думаю, мои сайты будут на MySQL и хочу использовать именно его feature’и. Для PEAR::Auth это решается просто – написанием простого адаптера. phpGACL придется «переводить» в ручную с AdoDb
  • phpGACL выглядит довольно страшно «из коробки». Модель данных тоже не исключение – 18 таблиц + 10 таблиц (sequence’ов). Инсталлятор, кэш, soap-клиент, и пр. Все лишнее нужно будет убрать, сделав embedded версию
  • помимо сказанного в предыдущем пункте в phpGACL имеет несколько избыточные функции для большинства случаев. Это выражается в том, что помимо ARO и ACO есть еще AXO (Access eXtension Object – объекты расширения доступа, при использовании которых ACO становятся непосредственно объектом, к которому контролируется доступ, а AXO – действиями, совершаемыми над этим объектом). AXO в большинстве случаев не нужны и от них нужно будет избавиться. Также для ARO помимо принадлежности к группе (дерево групп) задается принадлежность к секции (список). Это тоже, как мне кажется, лишнее.


Исправление минусов я считаю подготовительным этапом. Но, в принципе, можно было использовать указанные компоненты в том виде, в котором они поставлялись.

Изменения, проделанные в рамках «подготовки»:

  • для PEAR::Auth написан адаптер для PDOCon (моя обертка для PHP Data Object)
  • из phpGACL были удалены AXO и секции для ARO. В соответствие были приведен код и интерфейс управления
  • phpGACL был переведен на PDOCon
  • для phpGACL был отключен кеш в лице Cache_Lite. Сделано это было из-за того, что запросы phpGACL на работающих проектах не давали большой нагрузки и при необходимости кэширование для phpGACL все равно нужно будет интегрировать с общим кешом (контетна)


Модель данных phpGACL из
image

превратилась в
image

Собственно реализация:

  1. <?php
  2.  
  3.   class APC extends Auth
  4.   {
  5.     const STATUS_DISABLED = 0;  
  6.     const STATUS_ENABLED  = 1;  
  7.  
  8.     /**
  9.      * Policy object
  10.      * @var gacl_api
  11.      */
  12.     private $gacl = null;
  13.     /**
  14.      * Database object
  15.      * @var PDOCon
  16.      */
  17.     private $database = null;
  18.     private $userId   = null;
  19.     private $status   = null;
  20.     private $aclCache = array();
  21.  
  22.     public function __construct()
  23.     {
  24.       Zend_Loader::loadFile("gacl/gacl.class.php", null, true);
  25.       Zend_Loader::loadFile("gacl/gacl_api.class.php", null, true);     
  26.       $params = array(
  27.         "db"           => Zend_Registry::get('db'),
  28.         "cryptType"    => "md5",
  29.         "table"        => "acl__aro",
  30.         "usernamecol"  => "value",
  31.         "passwordcol"  => "password",
  32.         "postUsername" => "username",
  33.         "postPassword" => "password"
  34.       );
  35.       parent::__construct("PDOCon", $params, null, false);
  36.       //gacl
  37.       $gacl_options   = array('db_table_prefix' => 'acl__');
  38.       $this->database = Zend_Registry::get('db');
  39.       $this->gacl     = new gacl_api($this->database, $gacl_options);
  40.     }
  41.  
  42.     /**
  43.      * Returns bool specifies whether user is successfuly authenticated
  44.      * @return boolean
  45.      */         
  46.     public function checkAuth()
  47.     {
  48.       if(!parent::checkAuth())
  49.       {
  50.         return false;
  51.       }
  52.       return ($this->getStatus() == self::STATUS_ENABLED);
  53.     }
  54.  
  55.     public function getStatus()
  56.     {
  57.       if(!parent::checkAuth())
  58.       {
  59.         return false;
  60.       }
  61.       if(is_null($this->status))
  62.       {
  63.         $this->status = ($this->getUserId() !== false)
  64.                             ? (int) $this->database->getOne(
  65.                                 "user", array('user_id' => $this->getUserId()), null, "status"
  66.                               )
  67.                             : self::STATUS_DISABLED;
  68.       }
  69.       return $this->status;
  70.     }
  71.  
  72.     public function aclCheck($acoSection, $aco, $returnValue = false)
  73.     {
  74.       if(is_null($this->gacl))
  75.       {
  76.         throw new Exception('GACL does not exist');
  77.       }
  78.  
  79.       $aro = $this->checkAuth() ? $this->getUsername() : 'guest';
  80.       if(!isset($this->aclCache[$acoSection][$aco][$aro]))
  81.       {
  82.         $this->aclCache[$acoSection][$aco][$aro] = $this->gacl->acl_query($acoSection, $aco, $aro);
  83.       }
  84.       if(!$returnValue)
  85.       {
  86.         return $this->aclCache[$acoSection][$aco][$aro]['allow'];
  87.       }
  88.       else
  89.       {
  90.         return $this->aclCache[$acoSection][$aco][$aro]['return_value'];
  91.       }
  92.     }
  93.  
  94.     public function isMemberOf($groupName, $userId = null)
  95.     {
  96.       $userId = is_null($userId) ? $this->getUserId() : $userId;      
  97.  
  98.       $sql = "
  99.         SELECT COUNT(*)
  100.         FROM user u
  101.         JOIN acl__aro            r ON r.id       = u.aro_id
  102.         JOIN acl__groups_aro_map m ON r.id       = m.aro_id
  103.         JOIN acl__aro_groups     g ON m.group_id = g.id
  104.       ".PDOCon::WHERE;
  105.  
  106.       return (1 == $this->database->getRow(
  107.         $sql, 
  108.         array(
  109.           'g.value'   => $groupName, 
  110.           'u.user_id' => $userId
  111.         ), 
  112.         null, 
  113.         PDO::FETCH_NUM, 
  114.         0
  115.       ));
  116.     }
  117.  
  118.     public function getUserId()
  119.     {
  120.       if(!$this->getUsername())
  121.       {
  122.         throw new Exception('user is not set!');
  123.       }      
  124.       if(is_null($this->userId))
  125.       {
  126.         $sql = "
  127.           SELECT u.user_id 
  128.           FROM acl__aro r
  129.           JOIN user u ON u.aro_id = r.id
  130.         ".PDOCon::WHERE;
  131.  
  132.         $this->userId = $this->database->getRow(
  133.           $sql, 
  134.           array('value' => $this->getUsername()), 
  135.           null, 
  136.           PDO::FETCH_NUM, 
  137.           0
  138.         );
  139.       }
  140.       return $this->userId;
  141.     }
  142.  
  143.   }


APC означает «Authorization Policy Controller», и было придумано в большей степени из уважения к MJK, насколько я помню ;). Опять же класс был создан до того, как я познакомился с одноименным opcode cacher’ом и до того, как слово Controller стало однозначно ассоциироваться с MVC.

APC унаследован от PEAR::Auth. Основные методы:

  • checkAuth(), возвращающий true есть пользователь аутентицирован и false в противном случае
  • aclCheck(), проверяющий авторизация на выполнения конкретного действия e.g. aclCheck('article', 'edit')


Подытожив, скажу, что я описал лишь подход. Реализацию и примеры применений я планируй описать в продолжении, при условии, что эта статья вызовет интерес.

P.S. первый пот на habr’е ;)
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.