Делаю свою CMS (точнее то, что я называю CMS). Проект дошел до уровня проверки прав пользователей. Такая система должна позволять:

  1. Проверять права пользователя на ресурс
  2. Назначать права на ресурс группе
  3. Наследование прав дочерней группе от родительской
  4. Правила на родительский ресурс распространяются на все дочерние ресурсы
  5. Наследование прав (пример: право на изменение включает в себя право на чтение)

Попробую описать мое виденье такой системы прав.


Разрешения


Сначала немного терминологии, которой я буду придерживаться. Разрешение — это конкретное действие. Право — это возможность пользователя совершать какое либо действие.
По умолчанию система имеет следующие предопределенный список разрешений.
Имя Описание
none Нет прав
read Чтение
create Создание
update Изменение
delete Удаление
all Все права
Разрешения в списке представлены от наименьшего к наибольшему ( т.е. каждое следующее разрешение (строка) включает в себя все предыдущие разрешения). К примеру если пользователю назначили право на Создание (create), то значит у него есть и право на чтение (read). Соответственно разрешение all — даёт все права. А разрешение none — это запрет всех прав.

Ресурсы


Ресурс задаётся в виде ссылки /aaa/bbb/ccc/. При этом все права, заданные, к примеру, для /aaa/, относятся и ко всем дочерним ресурсам: /aaa/bbb/, /aaa/bbb/ccc/ и т.д. Для ресурса /aaa/bbb/ ресурс /aaa/ — родительский, ресурс /aaa/bbb/ccc/ — дочерний.

Группы



Группа — это объект, в котором происходит назначение прав (задание правил) на ресурсы. Есть ОДНА родительская для всех групп группа. Эта группа имеет все права на все ресурсы. Обычно такую группу называют superadmin, god(бог). Но чтобы было отличие от других ACL — я назвал такую корневую группу diablo (дьявол). Так что в моей системе все права будут «от дьявола». Отсюда и название статьи про дьявольский acl .
Каждая дочерняя группа может только УМЕНЬШАТЬ права. Т.е. если группа admin(администратор) имеет право create (создание) для ресурса /aaa/bbb/ccc/, то группа user (пользователь) не может получить право на удаление ресурса aaa/bbb/ccc/ — в этом случае произойдет увеличение права, так как разрешение delete (удаление) стоит выше разрешения create (создание). Также группа user(пользователь) не может получить право delete (удаление) на ресурсы /aaa/bbb/, /aaa/, / так как это родительские ресурсы для ресурса /aaa/bbb/ccc/ а значит в этом случае тоже произойдет увеличение прав. Принцип уменьшения прав нужен для того, чтобы можно было давать пользователям вести группы и при этом не опасаться, что они смогут получить доступ к ресурсам, которые вы не хотите им показывать, так как они не смогут добавить себе права, которых у них нет.
Каждая группа может содержать только одно правило для одного ресурса (включая родительские ресурсы). К примеру если в группе установлено правило для ресурса /aaa/bbb/ccc/, то не должно быть других правил для ресурсов /aaa/bbb/ccc/, /aaa/bbb/, /aaa/, /

Таблицы


Таблицы для хранения данных.


Функция проверки: имеет ли пользователь разрешение на ресурс


В качестве примера будем проверять разрешение create пользователя на ресурс /aaa/bbb/ccc/index.html
Сначала картинка дерева групп, на которой буду описывать примеры для лучшего понимания алгоритма (и мне самому в том числе). Группа №1 — это группа diablo (Дьявол), которая имеет все права на все. В группах 12, 13, 15, 10 заданы правила для нужного нам ресурса для разрешений, которые < заданного — что приводит к запрету на ресурс. В группах 3, 4, 22 заданы правила для нужного нами ресурса для разрешений, которые ? заданному — что снижает уровень доступа, но все-равно попадает под наш пример. Т.е. если в группе 1 на ресурс задано разрешение all, то в группе (к примеру) 3 задано разрешение delete, которое все-таки выше заданного в примере разрешения create.

Алгоритм работы следующий:
  1. По таблице Users по идентификатору пользователя (и по идентификатору = 0 — идентификатор гостя) определяем список групп (aGroupsUsers), к которым относится данный пользователь. Для нашего примера это будет список следующих групп.

    Как можно заметить, в нашем примере пользователь определен и в группе 2 и в дочерних группах 23 и 13. Теоретически наличие пользователя в группах 23 и 13 избыточно так как если пользователь определен в родительской группе, то значит он относится и ко ВСЕМ дочерним группам. Однако практически такое возможно (хотя бы из-за того что есть пользователь с идентификатором 0 (Гость), к которому относится каждый авторизированный пользователь). Именно поэтому я сделал такой пример.
  2. По таблице Permissions определяем список двух групп идентификаторов разрешений: а) которые < заданного б) которые ? заданного. К примеру для разрешения create будут сформированы такие списки: а) aDeny=[none, read, update] б) aAllow=[create, delete, all].
  3. Разбиваем ресурс на составляющие: т.е. помимо самого ресурса определяем всех его родителей. К примеру для ресурса /aaa/bbb/ccc/index.html получаем следующий список: /aaa/bbb/ccc/index.html, /aaa/bbb/ccc/, /aaa/bbb/, /aaa/, /. И по таблице Resources определяем идентификаторы указанного списка ресурсов (aResources).
  4. По таблице Rules и списку идентификаторов из пп.3 выбираем список групп (aGroupsRules), в которых определены правила для заданного ресурса (+ родительские ресурсы). В нашем случае результат будет следующий:

    Практически могут быть правила, которые заданы в дочерних группах к которым относится пользователь. И они попадут в выборку. Однако в нашем примере будем считать, что таких групп нет — все-равно они никак не влияют на результат.
  5. Мы выбрали список групп, к которым принадлежит пользователь (см.пп.1). Теперь нам нужно проверить все эти группы на наличие требуемых прав на заданном нами ресурсе с учетом родительских прав. Т.е. нам нужно проверить следующие ветки групп (ветка групп — это список групп от текущей до самого корня (дьявола)):
    • 23, 12, 6, 2, 1
    • 13, 6, 2, 1
    • 2, 1
    • 38, 27, 17, 8, 3, 1
    • 18, ,9, 4, 1
    • 20, 10, 4, 1
    • 32, 22, 11, 5, 1
    При этом если в одной из веток результат проверки прав положительный, то остальные проверять не обязательно, так как права у нас и так уже есть. Если же при поиске мы перешли на группу, в которой задано правило запрета, то дальше можно не проверять эту ветку и перейти к следующей. Ниже дан алгоритм определения прав. В переменной fAccessGroup по окончании работы алгоритма будет находиться результат доступа пользователя к ресурсу: True/False.

    В нашем примере поиск закончится на ветке «2, 1» так как она имеет доступ к ресурсу.
    Также хочу обратить ваше внимание, что в соответствии с данным алгоритмом вы никак не сможете изменить права доступа дьявола — он всегда будет иметь права на все просто в силу своего имени diablo.

Оптимизация
Как можно заметить (если вы прочитали алгоритм работы) у нас получается достаточно много выборок из БД, так как при проверки ветки группы мы последовательно перемещаемся от дочерней группы к родительской и чем длиннее ветка, тем больше выборок из БД нужно делать. Один из вариантов увеличить скорость работы — это кешировать результаты поиска по группе. Т.е. если мы при очередной проверке определили что группа G имеет разрешение P для ресурса R, то мы можем сохранить это в отдельной таблице

В этом случае после шага 3 мы можем выбирать по списку групп aGroupsUsers и списку ресурсов aResources данные из таблицы Cache. Если в выбранных данных есть разрешения ? требуемого, то значит пользователь имеет доступ к ресурсу. Если нет — то выполняем проверки по описанному алгоритму. При проверке ветки групп учитываем информацию о запрете, выбранную из КЕШ-а.
Я не буду описывать этот процесс подробно, так как это уже относится к конкретной реализации, а я в статье хотел сделать именно общее описание алгоритма работы. Но если у вас есть идеи оптимизации, то пишите их в комментариях.


Некоторые замечания


  • Чисто теоретически таблица Permissions не особо нужна, так как можно разрешения хранить в обычном массиве (я именно так и планирую делать). А вместо идентификаторов разрешений в таблицу правил можно записывать техническое имя (none,read,create,update,delete,all) разрешения. Однако я всё-же указал такую таблицу, так как возможно кому-то будет проще хранить разрешения именно в таблице, к тому же возможно так будет более понятно.
  • По умолчанию в группу guest нужно включить пользователя с идентификатором 0 (гость). Всех новых пользователей следует включать в группу user(пользователь).
  • Каждый модуль будет добавлять правила для своих ресурсов (хотя это уже наверное относится к каждому конкретному случаю реализации и не относится к проверке прав доступа ).
  • Для тех, кто хочет написать комментарий в стиле «нафига ты это придумывал, ведь в системе XXX это [почти]точно также реализовано»: мне не подойдет реализация в системе XXX, так как у меня самописная система (вплоть до самописной ORM) и реализовать проверку прав я буду на ней. Так что ссылки на системы в которых система прав реализована [почти]также приветствуется, но без указаний что зря я не прикрутил именно эту систему.
  • Если кто нашел проблему, которую я проглядел — пишите в комментариях. Этот уже третий вариант статьи, предыдущие два были отбракованы так как в результате написания статьи в алгоритме были обнаружены ошибки
  • Рассматривал вариант, когда дьявол не имеет прав ни на что и идет увеличение прав. В этом случае меньше шансов дать кому-то доступ к запрещенному для него контенту. Однако при таком подходе если дать пользователю права назначать права на группы, то он может сам себе назначить полный доступ. Так что в итоге все-таки решил что дьявол будет всемогущ .

Список ссылок по теме:


p.s.Если минусы ставите, то хоть пишите — за что.

Комментарии (7)