Реалізація ABAC (Attribute-Based Access Control) для веб-застосунку

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація ABAC (Attribute-Based Access Control) для веб-застосунку
Складна
~5 робочих днів
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Впровадження ABAC (Attribute-Based Access Control) для веб-додатків

RBAC розтріскується, коли з'являються правила на кшталт «користувач може редагувати документ, якщо він його автор, документ у статусі draft, і користувач працює в тій же організації, що й документ». Роль тут не допоможе — потрібен контекст. ABAC приймає рішення на основі атрибутів суб'єкта (користувача), об'єкта (ресурсу) та середовища (час, IP, контекст запиту).

Як влаштована модель

Чотири сутності в ABAC:

Subject — користувач та його атрибути: role, department, clearance_level, org_id.

Resource — об'єкт та його атрибути: owner_id, status, org_id, classification, region.

Actionread, write, delete, approve.

Environmenttime_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'

Схема зберігання політик

Можна зберігати політики в коді (для невеликої кількості правил) або в базі з 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; // за замовчуванням 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)))
    );
  }
}

Інтеграція з 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);

Аудит-лог

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  -- снімок атрибутів subject+resource у момент рішення
);

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-перевірка: чи у ролі є хоч якийсь доступ до цього типу ресурсу?
  if (!await rbac.canAccessResourceType(user.role, resource.type)) {
    return false;  // відсікаємо без завантаження об'єкта та проходу по ABAC-політикам
  }
  // Тонка перевірка через ABAC
  return engine.evaluate(user, resource, action);
}

Терміни та складність

Базовий движок з політиками в коді — 3–4 дні. Движок з політиками в базі та UI для їх редагування — 7–10 днів. Додавання аудит-логу з UI — ще 2–3 дні. Інтеграція зі сторонньою PDP (Open Policy Agent, Casbin) замість самописного движка — 2–3 дні на інтеграцію плюс час на написання політик.

Open Policy Agent — зріла альтернатива самописному движку. Політики пишуться на Rego, OPA запускається як sidecar або як окремий сервіс, ваш додаток звертається до нього через HTTP або gRPC. Це додає операційну складність, але дає версіонування політик, гарячу перезагрузку та вбудований аудит.