Данная статья преследует цель рассказать о новом походе к авторизации в облачных решениях, в основе которого лежит использование интерпретируемого языка определения политики управления доступом — который называется языком моделирования PERM (PML). Данный язык можно использовать для выражения различных моделей управления доступом, таких как список управления доступом (ACL), управление доступом на основе ролей (RBAC), управление доступом на основе атрибутов (ABAC) и других. А также рассказать о практическом воплощении этого подхода в виде конкретной реализации кросс-языковой библиотеки авторизации Casbin
Прежде чем приступать к рассказу, хотел выразить благодарность ключевому создателю как самого подхода, так и данной библиотеки Casbin, который является одним из исследователей, работающим в Microsoft Research Asia Yang Luo, он так же известен и как создатель еще одной, довольно известной библиотеки Npcap, которая с 2019 года работает под капотом утилиты Wireshark.
Основы авторизации
В своей сути, любой, даже сложный процесс авторизации можно схематично разбить на три компонента:
- Субъект доступа — это человек, служба, группа пользователей, кто пытается получить доступ.
- Объект доступа — это тот объект, к которому Субъект пытается получить доступ.
- Авторизатор — это компонент, который принимает решение, разрешить, либо запретить доступ Субъекта к Объекту.
Это можно изобразить в виде такой функциональной схемы:
Рис.1. Принципиальная схема авторизации.
- Субъект доступа обращается к Объекту доступа.
- Объект доступа обращается к Авторизатору, и запрашивает бинарный ответ, разрешен ли этому Субъекту доступ к данному Объекту или нет.
- Авторизатор по заложенной в него логике проверяет возможность доступа Субъекту к Объекту, и отвечает разрешен ли доступ или запрещен. Наиболее распространенная логическая модель по которой идет проверка доступа — это модель авторизации на базе ролей (RBAC).
- Объект доступа на основании ответа Авторизатор разрешает, либо запрещает доступ к себе Субъекта.
Casbin
Casbin — это библиотека авторизации, которая позволяет использовать и комбинировать различные модели управления доступом, такие как ACL, RBAC, ABAC и д.р. С точки зрения Принципиальной схемы авторизации показанной на Рис.1 — Casbin выполняет роль Авторизатора.
Рис.2. Принципиальная схема процесса авторизации с помощью Casbin.
Ключевым элементом механизма авторизации с помощью Casbin является Модель политики авторизации. Эта модель может описываться в текстовом файл *.CONF с помощью метамодели PERM (Policy, Effect, Request, Matchers), ну а по сути представляет из себя коллекцию строк с определенным содержанием.
Модель политики определяет состав кортежа с данными, который поступает в виде запроса на авторизацию, и описывает структуру хранения политик авторизации в хранилище политик авторизации, также определяет логику по которой происходит авторизации. Хранилищем может выступать как файл в формате *.csv
, так и таблица в базе данных, впрочем как и любое другое хранилище, если для него реализован соответствующий адаптер.
Как же было сказано, PERM — это гибкая метамодель для построения моделей авторизации. Аббревиатура расшифровывающаяся как (Policy — Политика, Effect — Эффект, Request — Запрос, Matchers — Сопостовители). Конкретный экземпляр PERM представленный в файле .CONF, описывает, как эти 4 элемента взаимодействуют друг с другом, определяя логику авторизации.
Пример №1. Список контроля доступа (ACL)
Лучше всего понять модель PERM на конкретном примере.
Представим, что мы создали простую CRM систему, которая хранит список клиентов, и хотим внедрить простую систему контроля доступа, чтобы контролировать кто и что может делать с ресурсом Клиент
(client). Для этой задачи подходит модель авторизации которая называется Список Контроля Доступа (Access Control List — ACL).
Эту модель можно выразить в виде системных требований, представленных в следующей таблице, где определено, какие действия разрешены или запрещены пользователям с ресурсом client
:
Пользователь/Действие | Создание (client.create) | Чтение (client.read) | Изменение (client.modify) | Удаление (client.delete) |
---|---|---|---|---|
Алиса (alice) | да | да | да | да |
Боб (bob) | нет | да | нет | да |
Петр (peter) | да | да | да | нет |
Теперь эту модель мы опишем в конфиг файле с именем client_acl_model.conf
на основе метамодели PERM, и дальше разберем каждую секцию
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
Каждая секция модели описывается в формате ключ = значение
. Каждой секции соответствует свой ключ.
В первую очередь в секции [request_definition]
идет определение структуры запроса (r
). Здесь мы указываем, что каждый запрос будет представлять из себя кортеж из трех элементов, в котором первый элемент будет связан с атрибутом по имени sub
(субъект), второй obj
(объект), и третий act
(действие). Пример правильного запроса, основанного на этом определении, является массив значений: ["alice","client","read"]
(может ли alice
read
client
?).
Дальше идет секция [policy_definition]
для определения структуры хранения политики. Как правило, она повторяет структуру запроса. Дополнительно, все правила политики имеют дополнительный предопределенный атрибут eft
, и он может принимать только значение allow
(разрешено) или deny
(запрещено). В данном случае, при определении достаточно простой модели политики ACL мы можем его не указывать, это будет избыточно.
Следующая секция [policy_effect]
, определяет, следует ли разрешить или запретить доступ, если запросу соответствует несколько политик авторизации. В данной модели мы используем следующий эффект политики для нашей CRM e = some(where (p.eft == allow))
, что означает: если есть какое-либо подходящее разрешающее правило политики типа allow
(например,eft
== "allow"), конечным эффектом будет allow
(разрешить). Это конструкция также означает, что если совпадений нет или все совпадения относятся к типу deny
(запрещено), конечный эффект будет deny
.
В разделе [matchers]
мы указываем логическое выражение, где определяем правила соответствия запроса (r
) заданному правилу политики (p
).
В данном случае мы указываем, что первому атрибуту запроса r.sub
должен соответствовать первый атрибут политики p.sub
и (&&
) соответственно второму атрибуту запроса должен соответствовать второй атрибут политики r.obj == p.obj
, и третьему атрибуту запроса должен соответствовать третий атрибут в политики r.act == p.act
.
Модель политики авторизации мы определили. Следующий шаг — необходимо определить правила политики на основе системных требований и определения политики указанных в разделе [policy_definition] конфигурационного файла модели. Мы можем поместить эти правила в базу данных или, в нашем случае, в файл * .csv
с именем client_acl_policy.csv
:
p, alice, client, create
p, alice, client, read
p, alice, client, modify
p, alice, client, delete
p, bob, client, read
p, peter, client, create
p, peter, client, read
p, peter, client, modify
Следует обратить внимание, что, прежде всего, поскольку мы не указываем значение атрибута eft ни для одного из вышеперечисленных правил, все наши правила по умолчанию имеют тип allow
(разрешено). Во-вторых, мы не должны определять какие-либо deny
правила для нашей системы.
Так как именно такую логику обработки правил мы заложили в нашем конфигурационном файле модели.
Следующий шаг — объединить модель политики, правила политики и саму библиотеку Casbin для создания в нашей CRM системы контроля доступа.
Для примера, я буду использовать код на C#, но в принципе он достаточно прост, и интуитивно понятен, даже для тех, кто его не знает.
Для платформы .net Casbin представлен в виде класса Enforcer
, который имеет множество конструкторов, но в простом виде, он принимает в конструктор две строковых переменные, указывающие путь к файлам с моделью и правилами политики.
// Создадим новый экземпляр класса Enforcer
var e = new Enforcer("path/to/client_acl_model.conf", "path/to/client_acl_policy.csv");
// Определим переменные, которые укажем в запросе авторизации
var sub = "alice";
var obj = "client";
var act = "read";
// Выполняем проверку
if (e.Enforce(sub, obj, act)) {
// доступ alice к чтению объекта client разрешен
} else {
// отклонить запрос, показать ошибку
}
Пример №2. Контроль доступа на основе ролей (RBAC)
Наша система авторизации отлично работает для простых сценариев, по ходу увеличения числа пользователей, утомительно каждому из них назначать разрешения, особенно если этих разрешений несколько. Поэтому мы разработали новую версию системы контроля доступа, на базе ролей, показанную на следующей схеме.
Рис.3. Схема ролей доступа (RBAC).
Мы назначаем разные роли разным пользователям. Пользователю bob
назначена роль читателя (reader
), peter
является автором (author
), а alice
теперь админ CRM (admin
).
Затем для каждой роли мы определяем разрешения (вместо того, чтобы спрашивать, какой пользователь что может делать?, как это описано в модели ACL, теперь мы задаемся вопросом какая роль что может делать?). Мы также наследуем одну роль от другой, тем самым наши роли поддерживают транзитивность. На приведенной выше диаграмме мы от читателя наследуем автора, а от автора наследуем администратора, и каждый наследник обладает теми же разрешениями что и родитель и плюс своими собственными.
Исходя из этого дизайна, конфигурационный файл client_rbac_model.conf
для нашей новой модели политики будет выглядеть так:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
Здесь мы видим, что у нас появилась новая секция [role_definition]
для определения ролей. Выражение g = _, _
говорит о том, что в секции [matchers]
мы будем сопоставлять два значения — r.sub
и p.sub
, но их может быть и больше, если допустим наше приложение будет поддерживать мультитенантность, и это необходимо будет учитывать при авторизации. Но об этом дальше.
Еще одно отличие, от модели ACL — мы изменили в секции [matchers]
эту часть r.sub == p.sub
на выражение g (r.sub, p.sub)
, что можно прочитать как: если r.sub
имеет роль (или наследуется от) p.sub
.
Содержимое файла client_rbac_policy.csv
с правилами политики для такой модели будут выглядеть так:
p, reader, client, read
p, author, client, modify
p, author, client, create
p, admin, client, delete
g, bob, reader
g, peter, author
g, alice, admin
g, author, reader
g, admin, author
Пример использования в коде приложения не отличается от предыдущего примера c ACL, за исключением путей к файлам модели и правил политики:
var e = new Enforcer("path/to/client_rbac_model.conf", "path/to/client_rbac_policy.csv");
var sub = "alice";
var obj = "client";
var act = "read";
// Выполняем проверку
if (e.Enforce(sub, obj, act)) {
// доступ alice к чтению объекта client разрешен
} else {
// отклонить запрос, показать ошибку
}
Пример №3. Контроль доступа на основе ролей с поддержкой мультитенатности (RBAC with domains/tenants)
По мере дальнейшего развития нашего приложения CRM, им заинтересовались другие компании, и мы добавили в нашу таблицу с клиентами новый столбец — компания, в который записываем имя копании, которой принадлежит этот клиент, и на основании этого значения отображаем каждой компании только принадлежащих ей клиентов, скрывая клиентов других компаний. Bob ушел в другую компанию, и там стал администратором нашей CRM.
Для поддержки мультитенатности, мы просто добавляем еще один атрибут в кортеж запроса на авторизацию, и в структуру описания правила политики в конфигурационном файле модели client_rbac_with_domain_model.conf
. А в дальнейшем при определении роли, и при указании правил сопоставления, учитываем этот атрибут.
[request_definition]
r = sub, dom, obj, act
[policy_definition]
p = sub, dom, obj, act
[role_definition]
g = _, _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
В сравнении с моделью описанной в примере №2 (RBAC) здесь мы видим что в секции [request_definition]
и [policy_definition]
у нас добавился новый атрибут dom
, в который передается название компании, к которой принадлежит субъект, который хочет авторизоваться.
Так же мы изменили секцию [role_definition]
, добавив еще одно измерение для выяснения принадлежности роли субъекту с учетом компании, к которой он принадлежит, g = _, _
заменили на g = _, _, _
.
И в разделе [matchers]
часть выражения g(r.sub, p.sub)
заменили на g(r.sub, p.sub, r.dom) && r.dom == p.dom
, что можно прочитать как: если r.sub
имеет роль (или наследуется от) p.sub
c учетом значения атрибута запроса r.dom
, и атрибуту запроса r.dom
, соответствует атрибут политики p.dom
.
Содержимое файла client_rbac_with_domain_policy.csv
с правилами политики для такой модели может выглядеть так:
p, reader, company1, client, read
p, author, company1, client, modify
p, author, company1, client, create
p, admin, company1, client, delete
p, reader, company2, client, read
p, author, company2, client, modify
p, author, company2, client, create
p, admin, company2, client, delete
g, author, reader, company1
g, admin, author, company1
g, author, reader, company2
g, admin, author, company2
g, alice, admin, company1
g, peter, author, company1
g, bob, admin, company2
Пример №4. Контроль доступа на базе модели RESTFul
Данная модель поддерживает возможность реализовывать логику авторизации, когда в качестве объектов доступа выступают ресурсы RestAPI интефрейса в формате URI в виде /res/*,/res/:id, а в качестве действий такие методы HTTP как GET,POST,PUT,DELETE.
Данная возможность обеспечивается благодаря использованию встроенных и пользовательских функций и регулярных выражений в разделе [matchers].
Конфигурационный файл в такой модели будет иметь следующий вид (здесь я использую официальный пример из Casbin):
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)
а файл с определением правил политики:
p, alice, /alice_data/*, GET
p, alice, /alice_data/resource1, POST
p, bob, /alice_data/resource2, GET
p, bob, /bob_data/*, POST
p, cathy, /cathy_data, (GET)|(POST)
Пример №5. Контроль доступа на базе атрибутов (ABAC)
Идея, заложенная в модели ABAC достаточно проста, и заключается в том, что политики авторизации строятся не на ролях, а на атрибутах субъектов, объектов доступа, атрибутов операций и атрибутов среды (атрибуты ABAC). Наиболее распространен достаточно сложный стандарт языка управления доступом ABAC под названием XACML. По сравнению с ним, модель ABAC реализованная в Casbin достаточно проста: есть возможность использовать структуры или экземпляры классов, вместо строковых значений атрибутов модели.
Здесь, как и в предыдущем примере, я буду использовать официальный пример из документации Casbin.
Вот пример модели политики на базе ABAC:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == r.obj.Owner
Этот пример мало отличается описанных выше, за исключением того, что при сопоставлении в разделе [matchers], мы значение атрибута запроса r.sub
сопоставляем с со свойством Owner
экземпляра класса, который мы передаем в атрибут запроса r.obj
, при вызове метода e.Enforce()
. В данном случае при сопоставлении, Casbin будет использовать механизм Reflection, чтобы получить из переданного экземпляра класса значение свойства Owner
.
Тогда класс, экземпляр которого мы будем передавать в качестве значения для атрибута r.obj
, будет иметь следующую структуру:
public class ResourceObject
{
...
public string Owner { get; set; }
}
Можно использовать несколько атрибутов ABAC при сопоставлении, например
[matchers]
m = r.sub.Domain == r.obj.Domain
В настоящее время, в качестве источников атрибутов ABAC для сопоставления могут выступать только элементы запроса на авторизацию (r
), такие как r.sub
, r.obj
, r.act
и т.д.
Нельзя в качестве источников атрибутов ABAC использовать элементы политики (p
), такие как например p.sub
, p.obj
и т.д., так как нет способа определить класс или структуру в политике Casbin.
Но для более сложных сценариев с использованием ABAC, существует возможность описать правила ABAC в политике Casbin, чтобы не увеличивать многословность логического выражения в секции [matchers]
модели.
Это достигается путем введения в модель функциональной конструкции eval()
и называется — масштабированием модели.
Пример конфигурации модели политики abac_scale_model.conf
с использованием масштабирования
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub_rule, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = eval(p.sub_rule) && r.obj == p.obj && r.act == p.act
Здесь в разделе [policy_definition]
модели, мы определяем новый атрибут sub_rule
, а в секции [matchers]
используем конструкцию eval(p.sub_rule)
. В данном случае p.sub_rule
представляет из себя определяемый пользователем тип (класс или структура), который содержит необходимые свойства, которые будут использоваться при определении политики.
Файл abac_scale_policy.conf
с правилами политики:
p, r.sub.Age > 18, client1, read
p, r.sub.Age < 60, client2, write
Ну и в коде это будет выглядеть так:
public class User
{
public int Age { get; set;}
public string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
var e = new Enforcer("path/to/abac_scale_model.conf", "path/to/abac_scale_policy.csv");
var sub = new User() { Name = "alice", Age = 19 };
var obj = "client1";
var act = "read";
if (e.Enforce(sub, obj, act)) {
// доступ alice к чтению объекта client1 разрешен
} else {
// отклонить запрос, показать ошибку
}
}
}
Резюме
Библиотека Casbin — очень гибкое и универсальное решение для авторизации, благодаря использованию интерпретируемого языка определения политики управления доступом называемый языком моделирования PERM (PML). Она позволяет использовать как существующие, наиболее распространенные модели управления доступом, так и комбинировать их, либо создавать новые.
В основе Casbin лежит интерпретатор модели политики (policy model) описанной с помощью языка PERM, который сопоставляет значения переданные в качестве атрибутов запроса на авторизацию, с правилами политики (policy) полученными из хранилища политик безопасности.
Библиотека реализована для множества существующих наиболее распространенных языков программирования.
jakobz
А как с такими DSL решается задача «показать список объектов, которые я могу видеть? Надо же в SQL-запрос это как-то транслировать, не выгребать же все записи с БД.
pprometey Автор
Не уверен, что правильно понял суть вопроса, но попробую ответить в меру своего разумения и может в процессе диалога я пойму что именно вы хотите спросить.
Если допустим речь идет о таком вопросе — выдай мне все роли или разрешения для этого объекта, или этого субъекта, то в библиотеке реализована такая фукнциональность, которая называется [Management API](https://casbin.org/docs/en/rbac-api)
Вот как выглядит функция — выдай мне все роли по этому пользователю:
А вот — выдай мне все разрешения для этого пользователя.
И вернется массив разрешений, это может быть допустим id объектов, к которым пользователю предоставлен доступ.
Возможно ответ вы можете увидеть в примере Пример №3. Контроль доступа на основе ролей с поддержкой мультитенатности (RBAC with domains/tenants), когда предоставляем доступ пользователя только к тем клиенам, которые принадлежат к его компании.
jakobz
Есть интерфейс на сайте, показывающий список чего-либо. Скажем — статей в админке CMS. Статей с базе — десятки тысяч, но обычно пользователь имеет доступ только к десятку. Как достать из БД статьи, которые видны конкретному пользователю? Ну, если мы все правила что кому видно — вынули из кода в какой-то DSL?
Другими словами — как написать запрос типа select * from objects as o where hasAssess(currentUser, o)
У меня такие правила в коде, в виде LINQ-выражений, и я такую задачу решать умею. А возникает такая задача даже чаще, чем «проверить есть ли доступ к одному вгруженному с память объекту»
pprometey Автор
Ок, я уже начинаю лучше понимать что интересует, но еще пару вопросов. А как в вашем случае вы определяете принадлежность статьи к какому-нибудь пользователю? Есть таблица статьей, в которой тысячи записей. Есть таблица пользователей. Каким образов в вашем случае, и где, вы связываете статью с конкретным пользователем, т.е. как и где храниться та информация, что вот этому пользователю можно читать эту конкретную статью, а этому можно ее редактировать, а другому можно ее удалять? Где, в каком виде и как хранится эта информация?
jakobz
Информация про пользователей и роли — как у всех лежит в БД.
Для твоего примера, может быть вот такое:
select * from articles a
join roles r on r.userId = currentUserId
where
article.owner = currentUserId
OR (r.role in ['admin', 'supevisor']) — админ всего
OR (r.domain = currentDomainId AND r.role in ['domain-admin', 'domain-supervisor']) — админ домена
pprometey Автор
Ок, ну вот мы выбрали список статей. За выборку из базы отвечает SQL и ORM. Casbin этим не занимается, — это библиотека авторизации, она отвечает на вопрос, можно или нельзя какому-то пользователю сделать какую-то операцию.
Мы отобразили список статей в интерфейсе. Рядом с каждой записью, при наведении курсора отображается значек редактирования и удаления. Как теперь в твоей системе решается вопрос, отображать или не отображать этот значек, и можно ли пользователю удалить или отредактировать запись?
tia_ru
В примере jakobz фильтрация по правам на чтение выполнена на уровне хранилища. Когда пользователю доступно по правам 1% из 100 тыс. документов, то в PERM придётся отправить чуть менее 100 тыс. запросов, чтобы набрать одну страницу списка, что жутко медленно. Поэтому, как минимум, права на чтение должны проверяться как можно ближе к хранилищу. А если в системе есть массовые операции удаления или модификации, то и контроль по всем правам.
PERM хорош для контроля доступа к единичным объектам.
pprometey Автор
Нет, нет. Это неправильный сценарий использования Casbin. Оверинженеринг.
Вы видимо не совсем вникли в суть, для чего используется Casbin и область ее применения. Не надо отправлять 100 тыс запросов. Это абсурд.
В данной ситуации, вы можете 1 раз спросить у Casbin что можно видеть этому конкретному пользователю и 1м SQL запросом выбрать все доступные ему документы.
tia_ru
Статья, кстати, очень понравилась и по сути и по изложению. Спасибо! Но собственно для уточнения области применения и задаются вопросы. Заявленная универсальность Casbin пока не очевидна. Можете поподробнее описать, как в статье, последовательность операций для фильтрации по правам на чтение с помощью Casbin документов при ABAC с ACL управлением доступом (DAC)? Что запросить у Casbin и какие параметры передать? Передать ключевые атрибуты всех 100 тыс. документов или хранить в БД Casbin id всех доступных документов для каждого пользователя или как? Очень актуальная тема на самом деле.
pprometey Автор
Ответил тут habr.com/ru/post/540454
pprometey Автор
Ответил здесь habr.com/ru/post/540454