Всем привет! Меня зовут Андрей Таболин, я системный аналитик в компании Bimeister. Casbin – одна из популярных библиотек для построения авторизации в веб-сервисах. В этой статье расскажу, как я тестировал Casbin, попутно подготовил своё решение для сравнения и покажу результаты работы обоих. Тестировалась в первую очередь эффективность работы с СУБД на разных объёмах данных для ролевой модели доступа (RBAC). Использовал: Node.js + PostgreSQL. Для понимания статьи нужно иметь хотя бы поверхностное представление о том, что такое RBAC и PERM. RBAC (Role-Based Access Control) – популярная модель контроля доступа, которая для разрешения действия над объектом проверяет наличие роли у пользователя. Есть ещё ABAC – модель, проверяющая атрибуты. И другие модели, которых мы касаться не будем. Подробнее: Подходы к контролю доступа: RBAC vs. ABAC. PERM (Policy, Effect, Request, Matchers) – метамодель для описания целевого контроля доступа. С его помощью можно подготовить: RBAC, ABAC, ACL и др. Описание выбранной модели происходит в конфигурационном файле (*.conf), необходимом для работы Casbin. Подробнее: RBAC? ABAC?.. PERM! Новый подход к авторизации в облачных веб-службах и приложениях Casbin: How it Works интерактивный редактор Casbin для закрепления понимания PERM Я решил использовать следующие тестовые данные: Объекты, к которым ограничиваем доступ Операции над ними Пользователей Роли и разрешения в них в отношении объектов Распределение ролей по пользователям Объект будет относиться к определённой группе (Group) и типу (Type). Под каждый тип объекта будет своя таблица с названием вида: Group1Type1.Для каждого типа предусмотрел операции CRUD (Create Read Update Delete) и распределил по ним роли с соответствующими операциями.Итого, в тестовом наборе (Group Set) будет 2 группы объектов, по 4 типа в каждой группе, по 4 роли на каждый тип: После приступил к созданию нагрузочного объёма для ролевой модели. Определил группы пользователей (User Gr), назначил им роли. Количество ролей на группу пользователей отличается, один пользователь в рамках группы объектов не будет обладать несколькими ролями. Итоговый набор данных назвал пул (Pool), он содержит: 32 уникальных типа объектов = 32 таблицы, по 10 объектов в каждой. Для тестирования ролевой количество объектов не имеет значения 16 ролей, распределённых между 1000 пользователями При подготовке пула заложил множитель. Он позволит протестировать решения на разных объёмах: 1 пул, 2 пула или 10. Тестовая модель данных для RBAC определена. Для проверки RBAC достаточно одной операции – чтение. Мне важно было понять скорость принятия решения: предоставить доступ на операцию пользователю или нет. Для прототипов я создал небольшую консольную утилиту (CLI), так было удобнее. Поэтому команды пользователя будут в соответствующем стиле: хочу получить все объекты конкретной группы и типа: list obj group=1 type=1 хочу получить все объекты доступных мне типов из указанной группы: list obj group=1 Константы могут меняться и приведены для примера. В рамках операций проверял, как быстро ролевая: group, type – примет решение: имею я доступ к указанной группе и типу объектов group – проверит каждый тип на доступ в рамках одной группы, в итоге получу список доступных типов Примеры CLI отражают знакомые кейсы пользователя: зашёл в раздел и вижу/не вижу объекты зашёл в раздел и вижу только доступные объекты разных типов Установил саму библиотеку Casbin по инструкции от авторов для Node.js.  Для соединения с СУБД Casbin предлагает использовать адаптеры как middleware. Выбор зависит от языка разработки и самого источника данных. Под мой стек и задачу взял Sequelize Adapter. Дополнительно мне потребовалось поставить драйвер pg для PostgreSQL – выдавало ошибку при запуске. Драйвер используется самим Sequelize Adapter. У Casbin есть требования к таблице, с которой он будет работать. Некоторые адаптеры создают таблицы сами по-умолчанию. Но с моим нужно создать явно, строго с именем «casbin_rule» и полями ниже. Следует обратить внимание на автоматически инкрементируемый первичный ключ. Casbin работает с политиками, но не с id записей. Сделать это можно средствами адаптера или руками. Я создал руками в СУБД. Поле Настройка id serial, Primary Key   ptype varchar 255, null   v0 varchar 255, null   v1 varchar 255, null   v2 varchar 255, null   v3 varchar 255, null   v4 varchar 255, null   v5 varchar 255, null   Инфо по полям: id – ид записи таблицы ptype – тип политики Casbin v0 - v5 – параметры по порядку, которые указываются в [policy_definition] файла конфигурации Casbin Настроил индексацию по рекомендации. Подготовил файл конфигурации Casbin (.conf): У меня получился следующий маппинг параметров файла конфигурации Casbin с созданной таблицей casbin_rule: В «casbin_rule» внёс немного данных для проверки работы Casbin с СУБД. Вывел в консоль доступные политики по фильтру, указав: роль, группу объектов и их тип. Супер, дорогу к СУБД через Casbin проложил. Дальше нужно было подготовить остальное хранение. Решил, что буду тестировать две реализации ролевой модели: Casbin и самописную (handmade) реализацию. Для этого нужно было подготовить таблицы в БД: Users - пользователи Auth - логины пользователей Roles - роли Permissions - правила доступа. Одна роль может содержать много доступных операций (permissions) в отношении одного или многих типов объектов UserRoles - пользователи и назначенные на них роли casbin_rule - таблица для Casbin, обсудили выше и уже подготовили Objects* - таблицы для объектов, которые будут создаваться автоматически в зависимости от количества пулов данных Для вашего понимания приведу примеры содержимого пары таблиц. В остальных по логической диаграмме БД, думаю, содержимое понятно.  табл. «casbin_rule» id ptype v0 v1 v2 v3 v4 v5 1 p role1 group1 type1 read   2 p role2 group2 type2 read   3 p role2 group2 type2 create   табл. «Permissions»   Id RoleId ObjectGroup ObjectType Permission 1 1 group1 type1 read 2 2 group2 type2 read 3 2 group2 type2 create Общий алгоритм функций CLI прост и не зависит от реализации: handmade или Сasbin. Пример команды list obj на запрос объектов: Из схемы видно, что функция CLI должна содержать: инфорсер – функция, проверяющая доступ пользователя к объекту. Взял из терминологии Casbin для упрощения понимания подходов функция для запроса данных из БД Заготовка функции: После перешёл к подготовке того, что будет под функцией enforce. Для описанной схемы БД мне было достаточно сделать так: Создание подключения к БД (db) вынес за рамки инфорсера, это позволило сэкономить время проверки. Один раз создали и пользуемся. Но не забываем контролировать. Можно подметить, что в таблице Permissions в моей схеме нет пользователя. Но т.к. я готовил handmade, то никто не запрещал мне в методе db.getUserPermissions добавить sql-запрос вида: Альтернатива инлайн SQL-запросам является ORM. В моём случае можно использовать тот же Sequelize. Для финального тестирования я фоном подготовил реализацию handmade на нём.   На сайте Casbin есть пример создания и использования инфорсера. Изначально я пошёл схожим с handmade путём – универсальный инфорсер. Файл конфигурации Сasbin и заранее созданный адаптер к БД можно передавать с аргументами в момент исполнения, они могут быть разные. Мощно, круто. Но радость моя быстро закончилась, когда я начал делать замеры подходов. Результаты были неутешительные. В ходе разбирательств выяснил, что casbin.newEnforcer загружает все политики из источника в создаваемый объект. Получается, что я при каждом вызове инфорсера просил Casbin прочитать всё из БД и загрузить к себе. Неудивительно, что результаты меня опечалили. Поняв природу Casbin, я вынес создание инфорсера в момент запуска CLI. В последующем мне осталось только вызывать его в командах пользователя таким образом: Что делать, если данные в БД изменились? Если говорить в общем, то работа с политиками в хранилище осуществляется через API самого инфорсера Casbin. Если политики в БД были изменены не через наш инфорсер, то можно загрузить актуальные с помощью LoadPolicy.   После подготовки инфорсеров требовалось подготовить команды пользователя: list obj group={n} type={m} – покажи все объекты группы N типа M list obj group={n} – покажи все объекты доступных типов группы N Handmade Инфорсер поместил в отдельный файл: Далее импортировал его в команду CLI:   Casbin В классе CLI определил метод init для создания инфорсера при запуске интерфейса: Добавил инфорсер Casbin в команду пользователя: Handmade Здесь логика меняется. Всего 4 типа объектов в одной группе, а доступ может быть только к двум, к примеру. Эти два и требуется вывести пользователю. Увидел пару вариантов решения задачи: получить существующие типы объектов для запрошенной группы и каждый тип проверить через инфорсер. В результате получу список доступных типов и запрошу данные по нему немного модифицировать существующий запрос в БД getUserPermissions, чтобы он мог получать пустой тип и возвращать все доступные типы по группе Решил сходить ради интереса в п.2. Обернул его не в функцию enforce, а в функцию getAllowedTypes: Код в CLI: Casbin С Casbin трюк по получению доступных типов тоже пройдёт. Я уже проводил похожий с помощью e.getFilteredPolicy. Но такая карета быстро превратится в тыкву, когда я захочу усложнить сценарий выборки за счёт передачи или хранения в политиках масок/последовательностей: group* type[1-10] !delete пр. Спойлер: в Casbin при вызове e.enforce() работа с такими сценариями может быть предусмотрена. Ещё выделение getAllowedTypes не выглядит каким-то универсальным средством проверки прав. Поэтому здесь решил пойти по пути проверки каждого типа на доступ, т.е. по п.1 из раздела выше. Для handmade инфорсера я подготовил фоном схожий подход, чтобы можно было сравнить в ходе замеров. В инфорсере Casbin я ничего не менял, поэтому опущу код, но посмотрим CLI: Для измерения времени работы инфорсеров воспользовался в Node.js функцией perfomance.now. Фиксировал время перед запуском инфорсера и после получения результата, вычислял разницу. Нагрузка на инфорсер по командам: list obj group={n} type={m} – инфорсер проводит одну проверку по уникальному типу объекта. Уникальность типа складывается из: группа объекта + тип объекта. list obj group={n} – инфорсер проводит проверку каждого типа в рамках группы. В тестовых данных четыре типа на одну группу. Для проверки handmade у меня получилось: SQL – реализация с инлайн SQL-запросами в БД ORM – реализация через ORM Sequelize getAllowedTypes (GAT) – реализация, при которой команда list obj group={n} сразу отдаст список доступных типов, а не будет проверять каждый тип на доступ. Реализация есть для SQL и ORM. В рамках тестирования данного режима я не буду проверять команду list obj group={n} type={m}, потому что результат будет схож с SQL/ORM. Для проверки Casbin: casbin – проверка работы инфорсера Casbin с его загрузкой в системную память casbin (no init enf) – реализация, при которой инфорсер Casbin создаётся во время проверки доступа Я добавил к командам CLI опцию Enforce Repeats, которая позволила устанавливать количество запусков инфорсера. Время работы будет вычисляться среднее (average). Единица измерения времени – миллисекунды (ms). Взял пользователей (User) с разным количеством ролей (User Roles), чтобы проверить влияние количества ролей на результат. Аппаратная часть для теста: Processor: i5-1135G7 @ 2.40GHz, 2419 Mhz, 4 Core(s), 8 Logical Processor(s) 1 000 пользователей, 16 ролей, 32 уникальных типа сущностей. Объём данных: Потребитель Имя таблицы Строк handmade Auth 1 000 Roles 16 UserRoles 1 720 Permissions 352 casbin casbin_rule 2 072 Результаты: Какие выводы я сделал после первого теста: инлайн SQL-запросы работают ожидаемо быстро. Но при таком подходе важно учитывать задержку работы служб и сети между хостом и СУБД. У всех будет индивидуальная. запросы через ORM на холодном старте дают задержку. Последующий запуск требует меньше времени. Пример трёх ручных последовательных запусков команды list obj group=1 type=1 rep=1 для handmade ORM с перерывом ~1 сек: enforce = 156 ms enforce = 4 ms enforce = 5 ms ORM справляется близко к уровню инлайн запроса. Отличает время на холодный старт. использование getAllowedTypes дало небольшой прирост производительности в инлайн SQL при частой сработке, но при холодном старте для ORM показал негативный результат. Если и оптимизировать инфорсер, то лучше это делать внутри, чтобы наружу была видна одна функция enforce. количество ролей на пользователе не влияют на скорость работы handmade инфорсера. Но у Сasbin сложилось впечатление, что влияет. casbin (no init enf) предметно показал, что создавать инфорсер в момент исполнения команды CLI не стоит, если хотим получить большую производительность. Исходя из выводов выше, для дальнейших тестов: убрал user750 с двумя ролями, оставил user930 с четырьмя убрал handmade (GAT). В целом я понял, что за счёт таких подходов можно оптимизировать работу внутри инфорсера. Но при использовании ORM нужно сравнивать тщательнее. убрал casbin (no init enf). Вне материала статьи проводил с ним тесты. Время работы инфорсера увеличивалась с объёмом данных. На 10 пулах list obj group=1 type=1 rep=1 показывал 0,6 сек, а на 100 пулах 4,426 сек. 10 000 пользователей, 160 ролей, 320 уникальных типа сущностей. Объём данных: Потребитель Имя таблицы Строк handmade Auth 10 000 Roles 160 UserRoles 17 200 Permissions 3 520 casbin casbin_rule 20 720 Ранее были сомнения насчёт выборки по user930 у casbin. Заменил его на пользователя user9910:   Выводы: у ORM специально оставил пример, когда при частой сработке холодный старт даже при другой команде уходит. Если пройдёт 5-10 сек, то время работы будет примерно 150ms на холодном и 3ms при частом обращении. на производительность инфорсера Casbin сказывается: количество записей, количество ролей на пользователей и местоположение пользователя в массиве политик Casbin. Такие выводы сделал из результатов тестирования с пользователями с другим количеством ролей, которые находятся в конце списка политик: 100 000 пользователей, 1 600 ролей, 3 200 типов сущностей. Объём данных: Потребитель Имя таблицы Строк handmade Auth 100 000 Roles 1 600 UserRoles 172 000 Permissions 35 200 casbin casbin_rule 207 200  Оставил одного пользователя в конце списка политик: Casbin «из коробки» готов покрыть функциональные требования разных моделей доступа: RBAC, ABAC, ALC, др. В рамках одной системы можно их комбинировать, создавая инфорсеры с разными файлами конфигурации. За счёт этого можно закрыть большое количество требований к системе. Скорость работы инфорсера на Node.js при проверке доступа к ресурсу составила ~150ms на 200 000 записях таблицы Casbin. Что может быть достаточным для многих веб-сервисов. На компилируемых языках скорость должна быть выше. В рамках одной системы можно комбинировать Casbin с handmade решением в местах, где требуется большая производительность. При таком решении будут таблицы, которые обеспечивают консистентность данных. А таблица Casbin будет служить, как операционная. Если записи по доступу в основных таблицах будут меняться, то потребуется обновить инфорсер Casbin. Уже через инфорсер данные будут попадать в таблицу Casbin. Позволит сэкономить время разработки на несложных участках и сконцентрироваться на требовательных. Можно оптимизировать скорость работы инфорсера, храня url в политиках: user1, /group1/* , read. Если user1 захочет получить доступ на чтение /group1/type4, то инфорсер Casbin вернёт true. Записей в таблице будет меньше, скорость выше. За счёт проделанной работы, я смог намного лучше понять природу Casbin, его основные возможности. Убедиться в его эффективности. Библиотека уже предлагает дополнительные средства для оптимизации проверок и всё ещё развивается. На этом пока всё. Если у вас есть под рукой советы по ускорению/использованию Casbin, инфо по скорости работы на других стеках/кейсах или полезные ссылки по данной библиотеке, то обязательно оставляйте комментарии. Они значимо смогут дополнить общий материал для всех специалистов, кто ищет эффективные подходы в авторизации. Репозиторий с исходным кодом CLI из статьи ANSI INCITS 359-2004 (стандарт RBAC) PML: An Interpreter-Based Access Control Policy Language for Web Services Гибкая авторизация с помощью Casbin и PERM. Практический пример API Casbin Sequelize  System Analyst