11. ABAC: атрибутивная модель
Что такое ABAC?
Заголовок раздела «Что такое ABAC?»ABAC (Attribute-Based Access Control) — управление доступом на основе атрибутов. Решение принимается на основе характеристик пользователя, ресурса, действия и окружения.
RBAC: Редактор может редактировать статьиABAC: Редактор может редактировать СВОИ статьи, ЕСЛИ они не опубликованы И время 9-18ABAC гибче RBAC:
- RBAC: “Редакторы могут редактировать”
- ABAC: “Редакторы могут редактировать только свои черновики в рабочее время”
Атрибуты в ABAC
Заголовок раздела «Атрибуты в ABAC»Subject (Пользователь): - role: "editor" - department: "marketing" - clearanceLevel: 3
Resource (Ресурс): - type: "article" - status: "draft" - authorId: "user123" - classification: "public"
Action (Действие): - type: "update" - method: "PUT"
Environment (Окружение): - time: "14:30" - ipAddress: "192.168.1.1" - dayOfWeek: "Monday"Простая реализация
Заголовок раздела «Простая реализация»// Политика как функцияfunction canEditPost(user, post) { // Автор может редактировать свои черновики if (post.authorId === user.id && post.status === 'draft') { return true; }
// Модератор может редактировать любые черновики if (user.role === 'moderator' && post.status === 'draft') { return true; }
// Админ может всё if (user.role === 'admin') { return true; }
return false;}
// Использование в роутеapp.put('/api/posts/:id', authMiddleware, async (req, res) => { const post = await db.posts.findById(req.params.id);
if (!canEditPost(req.user, post)) { return res.status(403).json({ error: 'Access denied' }); }
const updated = await db.posts.update(req.params.id, req.body); res.json(updated);});Policy Engine
Заголовок раздела «Policy Engine»Для сложных систем — движок политик:
class PolicyEngine { constructor() { this.policies = new Map(); }
// Регистрируем политику define(resource, action, policy) { const key = `${resource}:${action}`; this.policies.set(key, policy); return this; }
// Проверяем доступ async can(user, action, resource, context = {}) { const key = `${resource.type}:${action}`; const policy = this.policies.get(key);
if (!policy) { return false; // по умолчанию — запрещено }
return policy({ user, resource, action, context }); }}
const policyEngine = new PolicyEngine();
// Определяем политикиpolicyEngine .define('post', 'read', ({ user, resource }) => { // Опубликованные посты видят все if (resource.status === 'published') return true; // Черновики только автор или модератор+ if (resource.authorId === user.id) return true; return ['moderator', 'admin'].includes(user.role); })
.define('post', 'create', ({ user }) => { return ['editor', 'admin'].includes(user.role); })
.define('post', 'update', ({ user, resource }) => { // Автор может редактировать свои черновики if (resource.authorId === user.id && resource.status === 'draft') { return true; } return user.role === 'admin'; })
.define('post', 'delete', ({ user, resource }) => { if (resource.authorId === user.id) return true; return user.role === 'admin'; })
.define('post', 'publish', ({ user }) => { return ['moderator', 'admin'].includes(user.role); })
.define('user', 'manage', ({ user, resource }) => { if (user.role !== 'admin') return false; // Суперадмин не может удалить сам себя if (resource.id === user.id && action === 'delete') return false; return true; });
// Middlewarefunction authorize(action, resourceLoader) { return async (req, res, next) => { const resource = await resourceLoader(req); const allowed = await policyEngine.can(req.user, action, resource);
if (!allowed) { return res.status(403).json({ error: 'Access denied' }); }
req.resource = resource; next(); };}
// Использованиеapp.put('/api/posts/:id', authMiddleware, authorize('update', req => db.posts.findById(req.params.id)), async (req, res) => { const updated = await db.posts.update(req.params.id, req.body); res.json(updated); });Контекстные условия (Environment)
Заголовок раздела «Контекстные условия (Environment)»policyEngine.define('report', 'export', ({ user, context }) => { // Только в рабочие часы const hour = new Date().getHours(); if (hour < 9 || hour > 18) return false;
// Только из офисной сети const officeIPs = ['192.168.1.0/24', '10.0.0.0/8']; if (!isInNetwork(context.ip, officeIPs)) return false;
// Только с достаточным уровнем допуска return user.clearanceLevel >= 3;});ABAC с CASL (популярная библиотека)
Заголовок раздела «ABAC с CASL (популярная библиотека)»CASL — изоморфная библиотека авторизации, работает в Node.js и браузере.
npm install @casl/abilityimport { AbilityBuilder, createMongoAbility } from '@casl/ability';
function defineAbilityFor(user) { const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
if (user.role === 'admin') { can('manage', 'all'); // всё что угодно } else if (user.role === 'editor') { can('read', 'Post'); can('create', 'Post'); can('update', 'Post', { authorId: user.id }); // только свои can('delete', 'Post', { authorId: user.id, status: 'draft' }); // только черновики свои } else { can('read', 'Post', { status: 'published' }); // только опубликованные }
// Нельзя трогать системные ресурсы cannot('delete', 'Post', { type: 'system' });
return build();}
// Использованиеconst ability = defineAbilityFor(user);
// Проверкаif (ability.can('update', post)) { // разрешено}
if (ability.cannot('delete', { type: 'Post', ...post })) { throw new ForbiddenError('Cannot delete this post');}CASL с Express
Заголовок раздела «CASL с Express»import { ForbiddenError } from '@casl/ability';
function authorize(action, subject) { return (req, res, next) => { try { const ability = defineAbilityFor(req.user);
ForbiddenError.from(ability).throwUnlessCan(action, subject); next(); } catch (err) { if (err.name === 'ForbiddenError') { return res.status(403).json({ error: err.message }); } next(err); } };}
app.delete('/api/posts/:id', authMiddleware, async (req, res, next) => { const post = await db.posts.findById(req.params.id); req.post = post; next(); }, (req, res, next) => { try { const ability = defineAbilityFor(req.user); ForbiddenError.from(ability).throwUnlessCan('delete', req.post); next(); } catch (err) { res.status(403).json({ error: 'Access denied' }); } }, async (req, res) => { await db.posts.delete(req.params.id); res.json({ success: true }); });CASL в React (клиентская сторона)
Заголовок раздела «CASL в React (клиентская сторона)»'use client';
import { createContext, useContext } from 'react';import { createMongoAbility, defineAbilityFor } from '@/lib/ability';import { useSession } from 'next-auth/react';
const AbilityContext = createContext(createMongoAbility([]));
export function AbilityProvider({ children }) { const { data: session } = useSession(); const ability = defineAbilityFor(session?.user);
return ( <AbilityContext.Provider value={ability}> {children} </AbilityContext.Provider> );}
export const useAbility = () => useContext(AbilityContext);
// Использование в компонентеfunction PostActions({ post }) { const ability = useAbility();
return ( <div> {ability.can('update', post) && ( <EditButton /> )} {ability.can('delete', post) && ( <DeleteButton /> )} {ability.can('publish', post) && ( <PublishButton /> )} </div> );}RBAC vs ABAC: когда что
Заголовок раздела «RBAC vs ABAC: когда что»| Критерий | RBAC | ABAC |
|---|---|---|
| Сложность | Простой | Сложнее |
| Гибкость | Ограниченная | Высокая |
| Производительность | Выше | Ниже (вычисления) |
| Аудит | Проще | Сложнее |
| Применение | Большинство приложений | Сложные системы |
Используй RBAC для большинства приложений. Переходи на ABAC когда нужна гранулярность: “пользователь может менять только свои данные в рабочие часы”.
Практические задания
Заголовок раздела «Практические задания»- Реализуй PolicyEngine для системы блога (read/create/update/delete posts)
- Добавь контекстное условие: редактирование только в рабочие часы
- Установи CASL и перепиши политики на него
- Добавь CASL в React для условного показа кнопок