← Back to list

ABAC: Attribute-Based Access Control

When access decisions require more than just a user’s role

Anderson Henrique Botega · 2026-04-21 03:51 · 5 claps · 5.1 min read
#abac #cybersecurity
Open on Medium ↗
Wiki topics: 🔒 · Cybersecurity

ABAC: Attribute-Based Access Control

When access decisions require more than just a user’s role

What is ABAC?

Attribute-Based Access Control (ABAC) is an access control model where the decision to allow or deny an action is based on attributes — of the user, the resource, and the context of the request.

ABAC is often used alongside RBAC in modern systems, where roles provide coarse-grained access and attributes refine decisions at a more granular level.

Unlike RBAC, where the question is “what is this user’s role?”, in ABAC the question is richer: “who is this user, what are they trying to access, under what circumstances, and do the policies allow it?”

Three categories of attributes form the foundation of the model:

  • Subject attributes: who is acting (job title, department, clearance level, location)
  • Resource attributes: what is being accessed (file owner, classification, associated department)
  • Context attributes: the circumstances of the action (time of day, IP address, device, environment)

These attributes are combined in policies that return permit or deny for each request.

The problem without ABAC

Imagine a financial system where analysts can view reports — but only reports from their own department, only during business hours, and only for their assigned clients. With pure RBAC, expressing this often leads to an explosion of roles or scattered conditional logic in the application code.

In practice, the code starts accumulating inline checks that grow out of control:

// routes/reports.ts — scattered and fragile checks

app.get('/reports/:id', async (req, res) => {
  const user = req.user;
  const report = await getReport(req.params.id);

  // Correct role?
  if (user.role !== 'analyst' && user.role !== 'manager') {
    return res.status(403).json({ error: 'Access denied' });
  }

  // Same department?
  if (user.department !== report.department) {
    return res.status(403).json({ error: 'Access denied' });
  }

  // Business hours?
  const hour = new Date().getHours();
  if (hour < 8 || hour > 18) {
    return res.status(403).json({ error: 'Outside of allowed hours' });
  }

  // Assigned client?
  if (!user.assignedClients.includes(report.clientId)) {
    return res.status(403).json({ error: 'Access denied' });
  }

  res.json(report);
});

This logic often gets duplicated across multiple routes. When requirements change — for example, managers gain access outside business hours — you have to search through the entire codebase. Tests become complex. Audits are nearly impossible.

How to solve it with ABAC in TypeScript

1. Define the attribute types

// abac/types.ts
export interface UserAttributes {
  id: string;
  role: 'analyst' | 'manager' | 'admin';
  department: string;
  clearanceLevel: number;
  assignedClients: string[];
}

export interface ResourceAttributes {
  id: string;
  type: 'report' | 'contract' | 'invoice';
  department: string;
  clientId: string;
  classification: 'public' | 'internal' | 'confidential';
}

export interface ContextAttributes {
  hour: number;         // 0-23
  ipAddress: string;
  environment: 'production' | 'staging';
}

export type Action = 'read' | 'write' | 'delete' | 'export';

export type PolicyResult = 'permit' | 'deny';

2. Write the policies

Each policy is a pure function that receives the attributes and returns permit, deny, or null (not applicable). This makes unit testing straightforward and keeps rules explicit and readable:

// abac/policies.ts
import { UserAttributes, ResourceAttributes, ContextAttributes, Action, PolicyResult } from './types';

type Policy = (
  user: UserAttributes,
  resource: ResourceAttributes,
  context: ContextAttributes,
  action: Action
) => PolicyResult | null; // null = policy does not apply

// Policy 1: analysts can only access their own department
const sameDepartmentPolicy: Policy = (user, resource) => {
  if (user.role !== 'analyst') return null;
  return user.department === resource.department ? 'permit' : 'deny';
};

// Policy 2: confidential resources require clearance level >= 3
const clearancePolicy: Policy = (user, resource) => {
  if (resource.classification !== 'confidential') return null;
  return user.clearanceLevel >= 3 ? 'permit' : 'deny';
};

// Policy 3: analysts can only operate during business hours
const businessHoursPolicy: Policy = (user, _, context) => {
  if (user.role !== 'analyst') return null;
  return context.hour >= 8 && context.hour <= 18 ? 'permit' : 'deny';
};

// Policy 4: analysts can only access their assigned clients
const assignedClientPolicy: Policy = (user, resource) => {
  if (user.role !== 'analyst') return null;
  return user.assignedClients.includes(resource.clientId) ? 'permit' : 'deny';
};

export const policies: Policy[] = [
  sameDepartmentPolicy,
  clearancePolicy,
  businessHoursPolicy,
  assignedClientPolicy,
];

3. The decision engine (PDP)

The Policy Decision Point (PDP) is the heart of ABAC: it evaluates all policies and returns the final decision. The deny-overrides strategy ensures any explicit denial blocks access:

// abac/pdp.ts
import { policies } from './policies';
import { UserAttributes, ResourceAttributes, ContextAttributes, Action } from './types';

export function evaluate(
  user: UserAttributes,
  resource: ResourceAttributes,
  context: ContextAttributes,
  action: Action
): 'permit' | 'deny' {
  const results = policies
    .map(policy => policy(user, resource, context, action))
    .filter((r): r is 'permit' | 'deny' => r !== null);

  // No applicable policy = deny by default (fail-closed)
  if (results.length === 0) return 'deny';

  // Any explicit deny blocks access (deny-overrides)
  if (results.includes('deny')) return 'deny';

  return 'permit';
}

4. Middleware in Express

With the PDP in place, the authorization middleware becomes clean and declarative:

// middleware/authorize.ts
import { Request, Response, NextFunction } from 'express';
import { evaluate } from '../abac/pdp';
import { Action, ResourceAttributes, UserAttributes } from '../abac/types';

type ResourceLoader = (req: Request) => Promise<ResourceAttributes>;

export function authorize(action: Action, loadResource: ResourceLoader) {
  return async (req: Request, res: Response, next: NextFunction) => {
    const user = req.user as UserAttributes;
    const resource = await loadResource(req);
    const context = {
      hour: new Date().getHours(),
      ipAddress: req.ip ?? '',
      environment: process.env.NODE_ENV as 'production' | 'staging',
    };

    const decision = evaluate(user, resource, context, action);

    if (decision === 'deny') {
      return res.status(403).json({ error: 'Access denied' });
    }
    next();
  };
}

// Usage in routes — no inline logic
app.get(
  '/reports/:id',
  authorize('read', req => getReport(req.params.id)),
  async (req, res) => {
    const report = await getReport(req.params.id);
    res.json(report);
  }
);

5. Hook in React

On the frontend, the same engine can be used to show or hide UI elements based on the current user and resource attributes:

// hooks/useAccess.ts
import { useAuth } from './useAuth';
import { evaluate } from '../abac/pdp';
import { ResourceAttributes, Action } from '../abac/types';

export function useAccess(resource: ResourceAttributes, action: Action): boolean {
  const { user } = useAuth();
  if (!user) return false;

  const context = {
    hour: new Date().getHours(),
    ipAddress: '',
    environment: 'production' as const,
  };

  return evaluate(user, resource, context, action) === 'permit';
}

// components/ReportActions.tsx
export function ReportActions({ report }: { report: Report }) {
  const canExport = useAccess(report, 'export');
  const canDelete = useAccess(report, 'delete');

  return (
    <div>
      {canExport && <ExportButton report={report} />}
      {canDelete && <DeleteButton report={report} />}
    </div>
  );
}

The same decision logic runs on both the backend and the frontend — no duplication, no inconsistency.

Summary

ABAC extends what RBAC alone cannot express: rules that depend not just on who you are, but on what you are accessing and under what circumstances. The key benefits:

  • Expressive policies: any combination of attributes can be modeled
  • No role explosion: no need to create a role for every edge case
  • Auditable: all decisions pass through a single point (PDP)
  • Testable: each policy is a pure function with well-defined inputs and outputs

RBAC and ABAC are not mutually exclusive. In most modern systems, they are combined within a single policy-based authorization layer. The most common approach in mature systems is to combine them: RBAC for coarse-grained control (“analysts can read reports”) and ABAC for fine-grained refinement (“but only from their own department, within business hours, and for their assigned clients”).

The real power of ABAC is that security rules finally live where they belong: in explicit, testable, auditable policies — not scattered across route handlers and React components. When access requirements grow beyond what roles alone can express, ABAC provides the additional context needed to model those decisions precisely.

The full working project for this article — backend, frontend, and tests — is available on GitLab: 👉 https://gitlab.com/iam-access-control/abac-attribute-based-access-control


메타데이터
post_id
f57afa23e11d
slug
abac-attribute-based-access-control-f57afa23e11d
url
https://medium.com/@henriquebotega/abac-attribute-based-access-control-f57afa23e11d
canonical_url
https://medium.com/@henriquebotega/abac-attribute-based-access-control-f57afa23e11d
author_url
https://medium.com/@henriquebotega
status
ok
fetched_at
2026-07-11 03:00:40