Реализация 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; // 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)))
    );
  }
}

Интеграция с 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 дня. Движок с хранением политик в базе и 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. Это добавляет операционную сложность, но даёт версионирование политик, горячую перезагрузку и встроенный аудит.