"Это пшеница, что в темном чулане хранится, в доме, который построил Джек"
Джек имеет права на дом, а значит и на темный чулан, а стало быть и на пшеницу. Но чтобы проверить доступ Джека к пшенице, необходимо найти, в каком она хранится чулане, и в чьем доме этот чулан. Авторизация имеет линейную сложность от глубины иерархии объектов, и это плохо, т.к. всю цепочку объектов нужно читать из базы данных или держать в кеше. Становится еще хуже, если граф объектов имеет циклы и распределен между разными серверами.
Пример
Давайте рассмотрим систему управления задачами: в ней есть проекты и задачи. Проекты образуют иерархию: у каждого проекта может быть один или несколько родительских проектов. В проектах можно задавать права доступа, которые распространяются на все подпроекты. Система распределенная: родительский проект может быть расположен на другом сервере (в компании-контрагенте, например).
Требуется спроектировать процесс авторизации запросов пользователя к проектам и задачам, а также процесс изменения этих прав.
Мне известны два существующих пути решения этой задачи, восходящий и нисходящий:
При использовании нисходящего пути, изменения прав доступа распространяются на все вложенные ресурсы. Например, в Windows, так сделано изменение прав директории. Этот процесс может занять длительное время, возможно придется обращаться к другим серверам. Возникнут неприятные проблемы, если один из серверов окажется недоступен или в иерархии будут циклы. Зато проверка прав осуществляется быстро,
При восходящем пути список прав у каждого проекта свой и изменение выполняется быстро. Но проверка должна пройти по всей цепочке наследования снизу вверх, и этот путь может быть не короче нисходящего.
Существующие схемы авторизации (DAC, RBAC, ABAC...) обрабатывают каждый запрос независимо, исходя из данных аутентификации пользователя. Но пользователь может передавать серверу информацию о предыдущих авторизациях. Например, Джек получил в Росреестре свидетельство о праве на дом, затем в БТИ на основаниии этого свидетельства ему дали справку о владении чуланом. Теперь достаточно проверить, что пшеница хранится именно в указанном в справке чулане.
Inheritance-Based Access Control (IBAC)
Эта схема авторизации работает совместно с другой, базовой. Если права на доступ к ресурсу определены непосредственно для него, то работает базовая схема, а если наследуются от другого ресурса - то работает IBAC.
В ResourceToken хранится URI ресурса, время выдачи, полученные права, срок жизни и список ресурсов, которые позволили получить этот токен: ResourcePath.
На сервере хранятся сведения о наследовании прав доступа к ресурсам: InheritanceRule и сведения об инвалидации токенов Withdraw
-
ResourceToken будем считать валидным, если:
Он подписан закрытым ключем сервера;
Срок жизни токена еще не прошел;
Все ресурсы из ResourcePath отсутствуют среди Withdraw.
-
Авторизация доступа к ресурсу X считается успешной, если:
Базовая схема авторизации разрешила доступ, или;
В заголовках запроса был передан валидный ResourceToken ресурса X, или;
В заголовках запроса был передан валидный ResourceToken ресурса Y и cуществует InheritanceRule от ресурса Y к ресурсу X.
-
При успешной авторизации создается ResourceToken для ресурса X. ResourcePath выбирается следующим образом:
Если доступ авторизован посредством ResourceToken ресурса Y, то берем ResourcePath из этого токена и добавляет URI ресурса Y.
Если доступ авторизован посредством ResourceToken ресурса X, то берем ResourcePath из этого токена.
Иначе берем пустой список.
При удалении правила наследования прав доступа, необходимо добавить ресурс, который наследовал права, в список Withdraw на время максимального срока действия токена. Все токены, использовавшие этот ресурс для получения доступа к другим ресурсам, должны перестать работать.
Кажется, что этот алгоритм не очень-то и быстр: опять линейная сложность от глубины иерархии (длина ResourcePath), да еще и умноженная на длину списка Withdraw! Длина этого списка зависит от правил в системе. Например, для рассмотренной выше системы управления задач, она почти наверное равна нулю: если в системе и есть возможность перемещать проекты внутрь других проектов, то ей пользуются крайне редко. А если список Withdraw пуст, то и по ResourcePath проходить не нужно, и сложность константная.
А еще на Withdraw можно просто забить и доступ пропадет по истечению времени жизни токена, а пользователю можно сообщить, что изменение прав доступа займет 24 часа.
Возможности
Множественное наследование
Иногда появляются задачи, которые затрагивают несколько отделов компании. Приходится заводить проект, лежащий в нескольких проектах, но не всегда система позволяет это, в том числе из-за сложности авторизации. Если у проекта может быть несколько надпроектов, в реализации IBAC ничего не меняется. Даже в случае циклов (X → Y → X) проблемы скорей появятся на клиенте, чем на сервере.
Распределенные системы
Сейчас популярно отдавать проекты на аутсорс в другую компанию. Давайте сделаем систему управления задач распределенной: проекты могут храниться на разных серверах. Имея ResourceToken пользователь может получить доступ до проекта другой компании, но для этого сервера должны публиковать следующие сведения:
Публичный ключ для проверки подписи токенов.
Список Withdraw для проверки ResourcePath.
Все. Больше ничего менять не нужно. Список InheritanceRule хранится рядом с ресурсом, который наследует доступ (на том же сервере), поэтому легко доступен во время авторизации. Но, в этот случае придется все-таки пройти по ResourcePath и запросить Withdraw со всех необходимых серверов.
Микросервисы
В микросервисной архитектуре есть неудобство, что почти все сервисы зависят от сервиса авторизации. IBAC не удаляет эту зависимость, но ослабляет её: для проверки прав нужен только публичный ключ и список Withdraw.
Выводы
Спроектирована схема авторизации.
Работает почти наверное за O(1).
Может быть использована в распределенных системах.
Не боится множественного наследования и циклов.
Комментарии (4)
koreychenko
06.05.2022 13:17+2Мой опыт показывает, что многоуровневое наследование прав это штука довольно редкая и, в реальных бизнес-задачах очень часто является выстрелом себе в ногу.
Поэтому только права к конкретному ресурсу, их и в токен можно запихать прекрасно, и проверять с линейной сложностью, ну и всё такое.
Опять-таки, из вашего примера: в соответствие с законодательством я могу иметь права на дом, но если я сдал в нём квартиру, то я не имею права в неё заходить.
Обычно в нормальных системах права "расширяются" вглубь.
Например, если человек имеет доступ к морде админки, то это совсем не обязательно, что он может редактировать контент, для этого ему нужно еще одно право.
В итоге, у него будет пачка прав из серии: 'admin_access', 'content_edit' и.т.п.
Эту пачку прав можно зашифровать в тот же токен и прекрасно проверять при доступе к ресурсу.
Чтобы не сходить с ума при создании нового пользователя, отвешивая ему прав каждый раз, можно создать роль, прикрутить к ней эту пачку прав и уже назначать пользователю роль.
Но тут возникает интересное - где должна храниться связка роль-права и что шифровать в токене? В принципе, можно при создании токена выгружать туда все права.
Ну и да, это всё RBAC.fransua Автор
06.05.2022 13:52Вы правы, эту штука довольно редкая.
Но потребность в ней возникла у меня в пет-проекте, примерно описанном в статье: распределенный таск-менеджер, с большой глубиной иерархии и множественным наследованием. И вот RBAC с трудом получается применить.
saipr
Это утверждение истинно только в логике первого порядка (исчислении предикатов первого порядка). Я думаю, вот бы обрадовались банки и иже с ними, если бы это высказывание применялось на практике. А на практике надо разобраться, чья пшеница и на каком основании хранится в чулане.
mayorovp
Порядок логики не имеет отношения к владению