Часть I. Почему доступ становится сложным
Глава 1. Почему user → role → permission перестаёт работать
В большинстве информационных систем доступ начинается с простой модели.
Есть пользователь. У пользователя есть роль. У роли есть разрешения. Например:
Пользователь → Роль → Разрешение
Пользователь Alice имеет роль Engineer. Роль Engineer позволяет читать и изменять данные. Значит, можно записать:
Alice → Engineer → READ, UPDATE
Модель проста. Она легко объясняется, легко реализуется и хорошо работает, пока система действительно отвечает на простой вопрос:
Что этот пользователь вообще может делать?
Но в реальной системе этого вопроса обычно недостаточно.
1.1 Право должно относиться к чему-то
Представим, что в системе есть два проекта:
Project A
Project B
Alice работает с первым проектом. Bob — со вторым. При этом оба имеют одну и ту же роль:
Alice → Engineer
Bob → Engineer
И одинаковые разрешения:
READ
UPDATE
Если смотреть только на роли, пользователи полностью эквивалентны. Но это не означает, что они должны иметь одинаковый доступ. Alice должна иметь возможность изменить документ проекта A. Bob — документ проекта B. Поэтому вопрос уже нельзя сформулировать просто:
Может ли Alice выполнять
UPDATE?
Нужно спросить:
Может ли Alice выполнить
UPDATEнад этим объектом?
Появляется объект.
Пользователь → Разрешение → Объект
Например:
Alice → UPDATE → Document A
Это уже другая модель. Разрешение перестаёт быть свойством пользователя.
Оно становится отношением между пользователем, действием и объектом.
1.2 Но одного объекта тоже недостаточно
Теперь добавим структуру. Пусть проект содержит группы:
Project A
├── Design
├── Manufacturing
└── Archive
Документы находятся внутри этих групп. Alice работает с Design. Bob — с Manufacturing. Тогда два документа одного проекта могут иметь разный уровень доступности для одного и того же пользователя.
Alice → Design ✓
Alice → Manufacturing ?
Если Document A находится в Design, а Document B — в Manufacturing, вопрос доступа выглядит так:
Alice → READ → Document A
Alice → READ → Document B
Но чтобы ответить на него, недостаточно знать только Alice и документ. Нужно знать, как документ связан с группой. И как группа связана с проектом. И как Alice связана с этой структурой. Возникает цепочка отношений:
Alice
↓
Design
↓
Project A
↓
Document A
Теперь доступ определяется уже не одним фактом.
Он определяется структурой отношений.
1.3 Роль не исчезает
Это не означает, что роли больше не нужны. Наоборот, роль по-прежнему удобно описывает набор действий. Например:
Engineer
READ
UPDATE
Manager
READ
UPDATE
APPROVE
Проблема в другом. Роль отвечает на вопрос:
Какие действия в принципе доступны субъекту?
Но она не отвечает на вопрос:
Над какими объектами эти действия доступны?
Поэтому две части модели нужно разделить. Первая:
Engineer → READ, UPDATE
Вторая:
Alice → Project A
Только вместе они позволяют получить ответ:
Alice
+ Engineer
+ Project A
↓
READ / UPDATE
↓
объекты Project A
Роль стала частью модели доступа, но перестала быть всей моделью.
1.4 Появляется область действия
Можно попробовать исправить исходную модель, добавив область действия:
Пользователь → Роль → Разрешение → Область
Например:
Alice
↓
Engineer
↓
UPDATE
↓
Project A
Это уже ближе к реальной системе. Но сразу возникает следующий вопрос:
Что именно является областью действия?
Проект?
Группа?
Организация?
Конкретный объект?
Или несколько из них одновременно?
Например, документ может быть доступен пользователю потому, что:
- пользователь работает в той же организации;
- пользователь имеет роль в проекте;
- пользователь состоит в определённой группе;
- пользователь является владельцем объекта;
- объект опубликован в доступной области;
- доступ был явно предоставлен другому пользователю или организации.
Все эти случаи различны. Но результат у них один:
субъект получает возможность выполнить действие над объектом.
Следовательно, нам недостаточно просто добавить ещё одно поле scope. Нужно понять, каким образом разные факты участвуют в формировании доступа.
1.5 Право может зависеть и от объекта
До сих пор мы рассматривали права пользователя. Но объект тоже может ограничивать доступ.
Представим, что Alice имеет:
READ
UPDATE
APPROVE
А объект опубликован только с разрешениями:
READ
UPDATE
Наличие APPROVE у Alice само по себе ещё не означает, что она может утвердить этот объект. Получается пересечение:
Права субъекта
∩
Ограничения объекта
∩
Требуемое действие
Например:
Права Alice:
READ + UPDATE + APPROVE
Права публикации:
READ + UPDATE
Запрошено:
APPROVE
Результат:
APPROVE ∉ (Alice ∩ Publication)
Доступ не возникает.
Таким образом, право уже нельзя представить как простое свойство пользователя. Оно возникает из нескольких независимых источников.
1.6 Владелец — тоже отдельный случай
Есть ещё одна распространённая ситуация.
Пользователь создаёт объект. Можно сказать:
Создатель имеет доступ к созданному объекту.
Но это не обязательно означает, что создатель и владелец — одно и то же понятие. И тем более это не означает, что создатель навсегда сохраняет все права на объект.
Объект может быть передан другому владельцу. Может быть опубликован для других пользователей. Может быть закрыт. Может получить делегированный доступ.
Поэтому в модели появляются разные факты:
Кто создал объект?
Кто является владельцем?
Кому объект опубликован?
Какие действия разрешены?
В какой области действует разрешение?
Если свести всё это к одному полю user_id, модель начнёт терять смысл.
1.7 Доступ становится отношением
На этом этапе полезно изменить сам способ мышления. Вместо:
У пользователя есть разрешение
нужно говорить:
У субъекта есть возможность выполнить действие
над определённым объектом
в определённых условиях.
Это уже не свойство одного объекта. Это отношение. В самом общем виде его можно записать:
Subject → Action → Object
Но и этого недостаточно. Потому что один и тот же субъект может иметь разные права на один и тот же объект в разных обстоятельствах. Поэтому появляется четвёртая составляющая:
Subject → Action → Object
↑
Context
Именно здесь возникает понятие контекста. Пока мы не определяем его точно. Это важно. Контекст — не просто ещё одно поле.
Это совокупность условий и отношений, относительно которых определяется доступ.
1.8 Контекст может быть составным
Например, один и тот же объект может рассматриваться в разных контекстах:
Организация A
↓
Проект P
↓
Группа G
↓
Документ D
Для одного пользователя контекстом может быть организация. Для другого — проект. Для третьего — группа. В другом сценарии существенным окажется владение объектом. В ещё одном — явное предоставление доступа.
Поэтому контекст нельзя заранее свести к одному универсальному справочнику. Он возникает из самой модели системы. Это важное отличие от попытки просто добавить к роли ещё одно поле scope. Scope может быть одним из элементов контекста.
Но контекст шире.
1.9. Почему нельзя вычислять всё заново при каждом запросе
Можно попытаться оставить модель полностью динамической. При каждом обращении к объекту выполнять примерно такой алгоритм:
1. Найти субъекта.
2. Найти его роли.
3. Найти его отношения с группами.
4. Обойти иерархию.
5. Найти связи объекта.
6. Найти публикации.
7. Проверить владельца.
8. Проверить дополнительные предоставления.
9. Сопоставить разрешения.
10. Принять решение.
Для небольшой системы это возможно. Но по мере роста системы стоимость такого подхода увеличивается. Причём растёт не только количество данных. Растёт количество отношений. Изменение одного факта может затронуть множество результатов.
Например:
Alice вступила в Group G
может изменить доступ к сотням объектов.
Изменение роли:
Alice получила UPDATE в Project P
может изменить доступ к тысячам объектов проекта.
Изменение публикации:
Document D опубликован в Project P
может изменить доступ для большого числа пользователей.
Получается интересная ситуация. Исходных фактов относительно немного:
роль
членство
иерархия
владелец
публикация
предоставление доступа
Но производных решений может быть очень много.
Это подводит нас к следующему архитектурному вопросу:
Нужно ли каждый раз заново вычислять последствия изменения исходных фактов?
1.10. От исходных фактов к модели доступа
Вместо этого можно разделить два процесса.
Первый процесс изменяет исходные факты:
Alice вступила в Group G
Второй определяет, какие последствия это имеет для модели доступа:
Alice
↓
Group G
↓
доступные области
↓
доступные объекты
То есть появляется промежуточный слой.
Исходные факты
↓
Модель безопасности
↓
Решение о доступе
Это важное архитектурное разделение. Исходные данные описывают что произошло.
Модель безопасности описывает к чему это приводит с точки зрения доступа.
А проверка доступа отвечает уже на конкретный вопрос:
Может ли Alice выполнить UPDATE над Document D?
1.11. Меняется и смысл роли
В такой модели роль перестаёт быть центром системы. Она становится одним из исходных фактов.
Например:
Alice имеет роль Engineer
Это ещё не готовое разрешение на конкретный объект. Такое разрешение может появиться только после учёта других фактов:
Alice
├── имеет роль Engineer
├── состоит в Group G
├── Group G находится в Project P
└── Document D доступен через Project P
Из этих фактов можно получить производный результат:
Alice → UPDATE → Document D
Но этот результат уже не является непосредственно назначенной ролью. Он является эффективным правом — результатом применения модели безопасности к совокупности исходных фактов.
Это различие станет одним из центральных в дальнейшем.
1.12. От простой таблицы ролей к модели отношений
В начале у нас была простая схема:
User → Role → Permission
Затем появился объект:
User → Permission → Object
Затем области и отношения:
User
├── Role
├── Group
├── Project
└── Ownership
Object
├── Group
├── Project
├── Publication
└── Ownership
И наконец появляется модель:
Subject
│
┌────────┼────────┐
│ │ │
Role Group Ownership
│ │ │
└────────┼────────┘
↓
Context
↓
Object
↓
Action
Это уже не модель назначения ролей. Это модель отношений. Она позволяет задать более точный вопрос:
Имеет ли данный субъект право выполнить данное действие над данным объектом в данном контексте?
Именно этот вопрос мы будем развивать дальше.
Но прежде чем строить механизм его вычисления, необходимо разобраться с самим понятием контекста. Потому что если назвать контекстом всё, что влияет на доступ, не определив его границы, мы просто перенесём сложность из одной таблицы в другую.
Следующая задача — понять, что такое контекст и почему он является частью самой модели объекта, а не просто параметром запроса.