Организация доступа к REST API на основе Модели контроля доступа
Разграничение доступа к Node.js web-приложению согласно прав пользователей с использованием библиотеки Casbin

В рамках задачи разграничения доступа к API можно выделить три основных пункта:
Модель контроля доступа.
Алгоритм (правило, политика) определения доступности метода API для пользователя.
Способ реализации: самописный инструмент, готовая библиотека, отдельный сервис SAAS, FAAS и т.д.
Модель контроля доступа
Как правило при выборе Модели контроля доступа рассматриваю:
В общем случае ACL хорошо подходят для систем с небольшим количеством пользователей и сущностей системы потому как определяют прямое соответствие между пользователем и доступными объектами.
RBAC оперирует ролями вместо отдельных пользователей. При этом пользователи могут входить в одну или более групп.
ABAC подразумевает работу не с сущностями системы (ресурсами), а проверку отдельных атрибутов этих сущностей. К примеру, ABAC это когда доступ дается к объекту на основе информации об авторе объекта.
Приведенные выше модели ни в коем случае не являются исчерпывающими, и даже для умеренно сложной системы может потребоваться сочетание моделей.
Для большинства многопользовательских систем удобной будет модель RBAC так как позволяет распределять права на основе групп. При этом необходимо учитывать специфику и планы по развитию.
Выбрать какую-либо одну модель мы можем только если границы и функции системы чётко определены и не будут изменяться. В остальных случаях оптимальным будет фокус на наиболее подходящих моделях с архитектурой, способной на дальнейшее расширение.
Определение доступности метода API
Если механизм разграничения прав создается для нового приложения, т.е. самой API ещё нет, то это не является проблемой. Мы можем определить контракт и следовать ему. Скажем в модели RBAC определяем для группы author возможность читать и писать объекты article. При этом договариваемся, что для чтения используем HTTP метод GET, а для записи PUT. Или мы можем добавлять префиксы к URL и на основе них понимать о сущности и типе действия.
Более сложная ситуация связана с реализацией для существующего API. При этом чаще всего изменение самой API крайне нежелательно. В самом худшем случае нужно описать взаимодействие с каждым методом API для каждой группы. Компромиссным решением является:
Наличие общих проверок по правилам, которым удовлетворяет основная масса API методов.
Специфические проверки для отдельных API методов.
В этом случае может потребоваться сочетать разные модели проверки прав доступа.
Реализации проверки прав доступа
Использование сторонних сервисов, таких как keycloak является не столько техническим, сколько организационным выбором.
Самописный инструмент - это диаметрально противоположное решение. Способ затратный и сопряжённый с рисками. В большинстве случаев его использование обосновано только при отсутствии доступных готовых решений или при необходимости соблюдения ряда дополнительных требований и правил (опять же организационный выбор).
Оптимальным, при отсутствия явных противопоказаний, является использование готовых библиотек. Этот подход наиболее сбалансирован с точки зрения затрат и гибкости.
Поиск существующих Node.js библиотек
В OpenSource существует ряд библиотек для разграничения доступа, ниже представлены три из них:
Наименование | Модель контроля доступа | Способ описания прав доступа |
---|---|---|
ABAC | С помощью объектов в коде приложения | |
RBAC, ABAC | JSON | |
ACL, RBAC, ABAC, смешанная | PERM |
Остановимся на Casbin как наиболее универсальной библиотеке, использующей дополнительную абстракцию над моделями доступа. Благодаря файлу конфигурации основанному на метамоделе PERM (Policy, Effect, Request, Matchers) переключение механизма авторизации заключается в изменении конфигурации.
Пример приложения с разграничением прав доступа на основе ролей
Допустим, что у нас есть API сервис написанный на платформе Node.js, который состоит из двух контроллеров и нескольких методов.
import http from 'http';//#region SERVERconst hostname = '127.0.0.1';const port = 3005;const server = http.createServer((request, response) => { authMiddleware(request, response); switch (true) { case request.url.startsWith('/article'): articleController(request, response); break; case request.url.startsWith('/admin_panel'): adminController(request, response); break; default: notFound(request, response); break; }});server.listen(port, hostname, () => { console.log(`Server running at http://${hostname}:${port}/`);});//#endregion SERVER
//#region CONTROLLERSfunction articleController(req, res) { if (req.method === 'GET' && req.url === '/article') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Read an article'); return; } if (req.method === 'POST' && req.url === '/article/find') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Find an article by filter'); return; } if (req.method === 'POST' && req.url === '/article/write') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Write a new article'); return; } notFound(req, res);}function adminController(req, res) { if (req.method === 'GET' && req.url === '/admin_panel') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Admin panel'); return; } if (req.method === 'PUT' && req.url === '/admin_panel/author') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('Create a new author'); return; } notFound(req, res);}function notFound(req, res) { res.statusCode = 404; res.end();}//#endregion CONTROLLERS
У нас уже есть middleware в которой проверяется токен пользователя.
//#region MIDDLEWAREfunction authMiddleware(req, res) { // Get user token // const token = req.headers['authorization']; // Validate token etc... // Get user name and groups from the token // for simplification we will use values direct from headers const name = req.headers['name']; const groups = req.headers['groups']; if (name null || groups null) { res.statusCode = 401; res.end(JSON.stringify({ message: 'Unauthorized' })); }}//#endregion MIDDLEWARE
По условиям задачи у нас будет три группы: admin
, author
, user
. Первой доступно всё, вторая может работать только со статьями, а последняя подразумевает ограниченный доступ (только на чтение) к статьям. Каждый пользователь может обладать от одной до нескольких групп (в системе подразумевается появление специальных групп доступа к отдельным ресурсам).
Переходим к реализации проверки доступа на основе групп пользователя с помощью библиотеки Casbin. Первое, что требуется это указать конфигурацию (CONF).
[request_definition]r = sub, obj, act[policy_definition]p = sub, obj, act[role_definition]g = , g2 = , [policy_effect]e = some(where (p.eft == allow))[matchers]m = g(r.sub, p.sub) && (g2(r.obj, p.obj) || keyMatch(r.obj, p.obj)) && keyMatch(r.act, p.act)
Из особенностей:
используются две группы
g
иg2
. В рамках первой будем задавать группы пользователей, а в рамках второй - группы ресурсов.применяются функции
keyMatch
это даст возможность указывать в политиках не полные имена объектов (obj) и действий (act), а использовать паттерны.
Важно отметить, что это только одна из возможных конфигураций под решение текущей задачи. Документация у Casbin подробная с множественными примерами и более подробную информацию о параметрах и функциях можно узнать в ней.
В общем случае для REST API за контракт удобно принять следующие утверждения:
HTTP метод GET означает чтение, а остальные - запись;
первый уровень в URL (котроллер) - это сущность в системе. К примеру,
http://127.0.0.1:3005/article/list
подразумевает выполнение действия над сущностьюarticle
.
Руководствуясь конфигурацией и этими правилами создадим политику.
p, group:user, entity:article, readp, group:author, entity:article, writep, group:admin, *, *p, group:user, /article/find, *g, group:author, group:userg, group:admin, group:authorg2, /article, entity:articleg2, /article/*, entity:article
Явным исключением выделяется p, group:user, /article/find, *
, что обосновано методом в API http://127.0.0.1:3005/article/find
, который хоть и выполняет только чтение, но использует POST.
В остальном политика соответствует описанному выше контракту.
Осталось добавить логику проверки в middleware.
const sub = name; // use a user name as subjectconst obj = req.url; // use URL as objectconst act = req.method === 'GET' ? 'read' : 'write'; // GET is 'read', others are 'write'const enforcer = await newEnforcer(newModel(model), new StringAdapter(policy));enforcer.addNamedMatchingFunc('g2', Util.keyMatchFunc);const addGroupsToUser = async (sub, groups) => { await Promise.all(groups.map((group) => enforcer.addRoleForUser(sub, group:${group})));};await addGroupsToUser(name, groups);const isPermitted = await enforcer.enforce(sub, obj, act);if (!isPermitted) { res.statusCode = 403; res.end(JSON.stringify({ message: 'Forbidden' }));}
На строке enforcer.addNamedMatchingFunc('g2', Util.keyMatchFunc);
происходит добавление функции keyMatchFunc
для группы g2
. Это необходимо сделать для того, чтобы мы могли указывать конструкции вида /article/*
для второй группы.
Второй особенностью в коде является функция addGroupsToUser
. Суть её работы состоит в соотнесении текущего пользователя с его группами на уровне механизма авторизации Casbin.
Для проверки авторизации отправим следующие запросы чтобы проверить, что доступ есть:
curl http://127.0.0.1:3005/article -i -H "name: bob" -H "groups: user"curl http://127.0.0.1:3005/article/find -X POST -i -H "name: bob" -H "groups: user"curl http://127.0.0.1:3005/article -i -H "name: alice" -H "groups: user,author"curl http://127.0.0.1:3005/article/write -X POST -i -H "name: alice" -H "groups: user,author"curl http://127.0.0.1:3005/article/write -X POST -i -H "name: tom" -H "groups: admin,author"curl http://127.0.0.1:3005/admin_panel/author -X PUT -i -H "name: tom" -H "groups: admin,author"curl http://127.0.0.1:3005/admin_panel -i -H "name: tom" -H "groups: admin,author"
И несколько, для проверки запрета:
curl http://127.0.0.1:3005/article/write -X POST -i -H "name: bob" -H "groups: user"curl http://127.0.0.1:3005/admin_panel -i -H "name: alice" -H "groups: user,author"
В итоге достигнут желаемый результат - admin
имеет доступ ко всему, author
может работать только со статьями, а user
обладает ограниченным доступом только на чтение. Полный код примера можно найти по этой ссылке.
Примечания
NB 1: Альтернативой middleware может служить обработчик на каждом отдельном методе API. К примеру, через декоратор при OOP архитектуре, как это показано в библиотеке routing-controllers. К плюсам можно отнести лучшую читаемость кода, его декларативность, возможность передачи дополнительных параметров. К минусам - больше правок существующего кода, обязательность декоратора для каждого метода.
NB 2: У Casbin есть песочница, в которой можно проверить разные конфигурации и политики к ним без написания кода.
Материалы
Теория
Инструменты
Практика
Adding Authorization to a Node.js App – Beyond Role-Based Access Control (RBAC)
Understanding Casbin with different Access Control Model Configurations
How to Add Role-Based-Access-Control to Your Serverless HTTP API on AWS
RBAC? ABAC?.. PERM! Новый подход к авторизации в облачных веб-службах и приложениях
Understanding Casbin with different Access Control Model Configurations