ABAC (Attribute-Based Access Control) Implementation for Web Application

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815

Implementing ABAC (Attribute-Based Access Control) for Web Applications

RBAC breaks down when rules emerge like "a user can edit a document if they're the author, the document is in draft status, and the user works in the same organization as the document." Role alone won't help—you need context. ABAC makes decisions based on attributes of the subject (user), object (resource), and environment (time, IP, request context).

How the Model Works

Four entities in ABAC:

Subject—user and their attributes: role, department, clearance_level, org_id.

Resource—object and its attributes: owner_id, status, org_id, classification, region.

Actionread, write, delete, approve.

Environmenttime_of_day, ip_address, request_method.

Policy is a predicate over these attributes. For example:

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'

Policy Storage Schema

You can store policies in code (for small rule sets) or in the database with a DSL. Here's a variant storing in PostgreSQL as JSON conditions:

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,  -- condition tree
    actions     TEXT[] NOT NULL,
    resources   TEXT[] NOT NULL  -- glob: 'documents', 'documents/*'
);

-- Example record
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/*']
);

Decision Engine

class ABACEngine {
  constructor(policies) {
    // Policies preloaded and sorted by priority (deny > allow on conflict)
    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);
    }

    // Leaf node
    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)))
    );
  }
}

Integration with Express

const engine = new ABACEngine(await loadPoliciesFromDB());

// Reload policies on change (no server restart)
db.on('policy_changed', async () => {
  engine.updatePolicies(await loadPoliciesFromDB());
});

function abac(action) {
  return async (req, res, next) => {
    const resource = await loadResource(req);  // load object with all attributes
    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 without audit is a blind tool. Every decision is logged:

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 of subject+resource attrs at decision time
);

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);

This answers the question "why couldn't user X do Y with object Z three days ago"—without it, incident investigation becomes guessing.

Combining with RBAC

Pure ABAC is slower than RBAC with many policies—each check goes through all rules. In practice, use a hybrid: RBAC as first layer (fast coarse check by role), ABAC as second (fine contextual rules where needed).

async function authorize(user, resource, action) {
  // Fast RBAC-check: does the role have any access to this resource type?
  if (!await rbac.canAccessResourceType(user.role, resource.type)) {
    return false;  // cut without loading object and traversing ABAC-policies
  }
  // Fine check via ABAC
  return engine.evaluate(user, resource, action);
}

Timeline and Complexity

Basic engine with policies in code—3–4 days. Engine with database policies and UI for editing them—7–10 days. Adding audit log with UI—another 2–3 days. Integration with third-party PDP (Open Policy Agent, Casbin) instead of custom engine—2–3 days for integration plus policy writing time.

Open Policy Agent is a mature alternative to a custom engine. Policies are written in Rego, OPA runs as a sidecar or separate service, your application calls it via HTTP or gRPC. This adds operational complexity but provides policy versioning, hot reload, and built-in audit.