Ми часто стикаємося з ситуацією, коли RBAC тріщить по швах. В одному проєкті після впровадження RBAC ми отримали 47 запитів на зміну ролей за місяць — кожне вимагало погодження. ABAC скоротив це до 2 запитів. З'являються правила на кшталт «користувач може редагувати документ, якщо він його автор, документ перебуває в статусі draft, і користувач працює в тій самій організації, що й документ». Роль тут не допоможе — потрібен контекст. ABAC (Attribute-Based Access Control) приймає рішення на основі атрибутів суб'єкта (користувача), об'єкта (ресурсу) та середовища (час, IP, контекст запиту). Наш досвід показує, що гібрид RBAC+ABAC дає оптимальний баланс продуктивності та гнучкості. Ми гарантуємо прозорість рішень за допомогою детального аудиту.
Як влаштована модель
Чотири сутності в ABAC:
- Subject — користувач та його атрибути: role, department, clearance_level, org_id.
- Resource — об'єкт та його атрибути: owner_id, status, org_id, classification, region.
- Action — read, write, delete, approve.
- Environment — time_of_day, ip_address, request_method.
Політика — це предикат над цими атрибутами. Наприклад:
ALLOW IF
subject.org_id == resource.org_id
AND (subject.role == 'editor' OR subject.id == resource.owner_id)
AND resource.status IN ('draft', 'review')
AND action == 'write'
Чому ABAC кращий за RBAC?
RBAC простий і швидкий, але не масштабується для складних правил. ABAC дозволяє виразити практично будь-яке бізнес-обмеження: «менеджер може схвалити заявку, якщо сума < 10000 і заявка створена в його відділі». У RBAC довелося б вводити нову роль. ABAC вирішує це декларативно. Порівняємо ключові параметри:
| Критерій | RBAC | ABAC |
|---|---|---|
| Гнучкість | Низька (ролі фіксовані) | Висока (атрибути) |
| Підтримка контексту | Ні | Так |
| Продуктивність | Висока | Середня (при великій кількості політик) |
| Складність впровадження | Низька | Середня |
Схема зберігання політик
Можна зберігати політики в коді (підходить для невеликої кількості правил) або в базі з DSL. Ось варіант зі зберіганням у PostgreSQL у вигляді JSON-умов:
CREATE TABLE abac_policies (
id SERIAL PRIMARY KEY,
name VARCHAR(128) NOT NULL,
description TEXT,
effect VARCHAR(8) NOT NULL CHECK (effect IN ('allow', 'deny')),
priority INT NOT NULL DEFAULT 0,
conditions JSONB NOT NULL, -- дерево умов
actions TEXT[] NOT NULL,
resources TEXT[] NOT NULL -- glob: 'documents', 'documents/*'
);
-- Приклад запису
INSERT INTO abac_policies (name, effect, priority, conditions, actions, resources)
VALUES (
'editors_can_write_own_draft',
'allow',
10,
'{
"operator": "AND",
"conditions": [
{"attribute": "subject.role", "op": "in", "value": ["editor", "senior_editor"]},
{"attribute": "subject.org_id", "op": "eq", "value": {"ref": "resource.org_id"}},
{"attribute": "resource.status", "op": "in", "value": ["draft", "review"]}
]
}',
ARRAY['write', 'delete'],
ARRAY['documents', 'documents/*']
);
Двигун прийняття рішень
class ABACEngine {
constructor(policies) {
// Політики попередньо завантажені та відсортовані за пріоритетом (deny > allow при конфлікті)
this.policies = policies.sort((a, b) => {
if (a.effect === 'deny' && b.effect !== 'deny') return -1;
return b.priority - a.priority;
});
}
evaluate(subject, resource, action, environment = {}) {
const context = { subject, resource, action, environment };
for (const policy of this.policies) {
if (!policy.actions.includes(action)) continue;
if (!this.matchesResource(policy.resources, resource.type)) continue;
if (this.evaluateCondition(policy.conditions, context)) {
return policy.effect === 'allow';
}
}
return false; // default deny
}
evaluateCondition(condition, ctx) {
if (condition.operator === 'AND') {
return condition.conditions.every(c => this.evaluateCondition(c, ctx));
}
if (condition.operator === 'OR') {
return condition.conditions.some(c => this.evaluateCondition(c, ctx));
}
if (condition.operator === 'NOT') {
return !this.evaluateCondition(condition.condition, ctx);
}
// Листовий вузол
const leftVal = this.resolveAttribute(condition.attribute, ctx);
const rightVal = condition.value?.ref
? this.resolveAttribute(condition.value.ref, ctx)
: condition.value;
switch (condition.op) {
case 'eq': return leftVal === rightVal;
case 'neq': return leftVal !== rightVal;
case 'in': return Array.isArray(rightVal) && rightVal.includes(leftVal);
case 'gte': return leftVal >= rightVal;
case 'lte': return leftVal <= rightVal;
case 'contains': return Array.isArray(leftVal) && leftVal.includes(rightVal);
default: return false;
}
}
resolveAttribute(path, ctx) {
// 'subject.org_id' → ctx.subject.org_id
return path.split('.').reduce((obj, key) => obj?.[key], ctx);
}
matchesResource(patterns, resourceType) {
return patterns.some(p =>
p === resourceType || (p.endsWith('/*') && resourceType.startsWith(p.slice(0, -2)))
);
}
}
Як інтегрувати ABAC в Express?
const engine = new ABACEngine(await loadPoliciesFromDB());
// Перезавантаження політик при зміні (без рестарту сервера)
db.on('policy_changed', async () => {
engine.updatePolicies(await loadPoliciesFromDB());
});
function abac(action) {
return async (req, res, next) => {
const resource = await loadResource(req); // завантажуємо об'єкт з усіма атрибутами
const allowed = engine.evaluate(
req.user, // subject
resource, // resource
action, // action
{ // environment
ip: req.ip,
timestamp: Date.now(),
userAgent: req.headers['user-agent'],
}
);
if (!allowed) {
return res.status(403).json({ error: 'Forbidden' });
}
req.resource = resource;
next();
};
}
router.put('/documents/:id', authenticate, abac('write'), updateDocument);
router.delete('/documents/:id', authenticate, abac('delete'), deleteDocument);
Audit log
ABAC без аудиту — сліпий інструмент. Кожне рішення двигуна логується:
CREATE TABLE abac_audit_log (
id BIGSERIAL PRIMARY KEY,
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
subject_id INT NOT NULL,
resource_type VARCHAR(128),
resource_id VARCHAR(128),
action VARCHAR(64) NOT NULL,
decision BOOLEAN NOT NULL,
matched_policy_id INT REFERENCES abac_policies(id),
context_snapshot JSONB -- snapshot subject+resource attrs на момент рішення
);
CREATE INDEX idx_abac_audit_subject ON abac_audit_log (subject_id, ts DESC);
CREATE INDEX idx_abac_audit_resource ON abac_audit_log (resource_type, resource_id, ts DESC);
Це дає відповідь на питання «чому користувач X не зміг зробити Y з об'єктом Z три дні тому» — без нього розслідування інцидентів перетворюється на ворожіння.
Комбінування з RBAC
Чистий ABAC повільніший за RBAC при великій кількості політик — кожна перевірка проходить через усі правила. На практиці використовують гібрид: RBAC як перший шар (швидка груба перевірка за роллю), ABAC як другий (тонкі контекстні правила тільки там, де потрібно).
async function authorize(user, resource, action) {
// Швидкий RBAC-check: чи є у ролі хоч якийсь доступ до цього типу ресурсу?
if (!await rbac.canAccessResourceType(user.role, resource.type)) {
return false; // відсікаємо без завантаження об'єкта та проходу по ABAC-політиках
}
// Тонка перевірка через ABAC
return engine.evaluate(user, resource, action);
}
Як оцінити складність проєкту?
Терміни залежать від обсягу політик та архітектури. Ми виділяємо три варіанти:
| Варіант | Термін | Що входить |
|---|---|---|
| Базовий | 3–4 дні | Двигун у коді, 5–10 політик, тести |
| Розширений | 7–10 днів | Політики в БД, REST API, UI для редагування, аудит |
| Інтеграція з OPA | 2–3 дні | Sidecar-розгортання, написання політик на Rego, тестування |
Що входить у роботу з впровадження ABAC?
- Аналіз моделі доступу та атрибутів користувачів, ресурсів, оточення.
- Проєктування та документування політик доступу.
- Розробка двигуна прийняття рішень (або інтеграція з OPA/Casbin).
- Реалізація REST API для керування політиками.
- Створення системи аудиту з візуалізацією логів.
- Покриття тестами (unit + integration).
- Деплой та налаштування моніторингу.
- Навчання команди: як писати та налагоджувати політики.
- Гарантія на код та підтримка після впровадження.
Базовий двигун зі зберіганням політик у коді — 3–4 дні. Двигун зі зберіганням політик у базі та UI для їх редагування — 7–10 днів. Додавання audit log з UI — ще 2–3 дні. Інтеграція зі сторонньою PDP (Open Policy Agent, Casbin) замість самописного двигуна — 2–3 дні на інтеграцію плюс час на написання політик на Rego або PERM.
Open Policy Agent — зріла альтернатива самописному двигуну. Політики пишуться на Rego, OPA запускається як sidecar або як окремий сервіс, додаток звертається до нього через HTTP або gRPC. Це додає операційну складність, але дає версіонування політик, гаряче перезавантаження та вбудований аудит.
Якщо у вас складні вимоги до доступу — зверніться до нас за аудитом. Оцінимо ваш проєкт безкоштовно. Зв'яжіться з нами, щоб обговорити деталі. Замовте реалізацію ABAC під ключ.







