| |

Node.js 54 🟢 Role-Based (RBAC) and Attribute-Based (ABAC) Access Control

Authentication answers who the user is. Authorization answers what the user may do. The two are separate concerns, and conflating them produces systems where permissions are scattered across route handlers, hidden in conditionals, and impossible to audit. Two models dominate the field. Role-Based Access Control assigns permissions to roles and roles to users; it is simple, auditable, and the right choice for most applications. Attribute-Based Access Control evaluates policies against a combination of user, resource, action, and environment attributes; it is more expressive and the right choice when the rules cannot be expressed as a fixed set of roles. Most real systems start with RBAC and reach for ABAC only when a specific requirement forces it.

Key point: Authorization is a policy decision, and policy decisions should be centralized, not scattered across handlers. A permission check inside a route is a policy that is invisible to auditors, invisible to tests, and easy to forget. A centralized policy function or middleware is the same decision, made once, in one place.


Why authorization needs its own model

The scattered-check problem. When permission logic lives inside each handler, the same rule is written multiple times, in slightly different forms. A change to the rule requires finding every copy. A missing check on one route is a security hole that no test may catch.

The role explosion problem. A naive RBAC implementation creates a role for every combination of permissions. editor_can_publish, editor_can_delete, editor_can_approve becomes dozens of roles. The model breaks down because roles multiply faster than the code.

The context problem. Some decisions depend on more than the user’s role. A user may edit their own posts but not others’. A manager may view salaries for their direct reports but not for the whole company. A user may access a resource only during business hours. RBAC cannot express these rules because they depend on the resource and the environment, not just the role.

The audit problem. A compliance review asks: who can delete a customer record? If the answer is spread across twelve handlers, each with its own check, the review becomes a manual search. A centralized policy answers the question in one place.

The testing problem. Permission logic is hard to test when it is embedded in handlers. A centralized policy function is a pure function that takes a request context and returns a decision. It can be tested exhaustively without spinning up an HTTP server.

The performance problem. Permissions may need to be fetched from a database on every request. Caching the user’s role and permissions in the session or in a short-lived cache avoids the lookup. The model determines what needs to be cached.


a. Role-Based Access Control

RBAC assigns permissions to roles and roles to users. The user’s effective permissions are the union of the permissions granted by every role they hold.

The data model

The canonical schema has five tables. Users hold roles through a join table, and roles hold permissions through another join table.

users (id, email, password_hash)
roles (id, name)
permissions (id, name, description)
user_roles (user_id, role_id)
role_permissions (role_id, permission_id)

The join tables are what make roles reusable. A role is a named bundle of permissions, and a user can hold multiple roles.

The permission check

A route that requires a specific permission checks the user’s effective permission set.

function requirePermission(permission) {
  return async (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: "unauthenticated" });
    const permissions = await getUserPermissions(req.user.id);
    if (!permissions.has(permission)) {
      return res.status(403).json({ error: "forbidden" });
    }
    next();
  };
}

app.delete("/posts/:id",
  requireAuth,
  requirePermission("post:delete"),
  deletePostHandler);

The middleware is composed with the authentication middleware. The route handler runs only if the user has the permission.

The role hierarchy

A hierarchy lets roles inherit permissions from other roles. An admin role inherits everything from editor, which inherits from viewer. The schema adds a parent_id column to the roles table.

roles (id, name, parent_id)

The permission lookup walks the hierarchy and unions the permissions of all ancestors.

async function getUserPermissions(userId) {
  const roles = await getRolesWithAncestors(userId);
  const permissions = new Set();
  for (const role of roles) {
    for (const perm of role.permissions) {
      permissions.add(perm.name);
    }
  }
  return permissions;
}

The hierarchy reduces role proliferation because common permissions live in the base roles.

Caching the permission set

The permission lookup is on the hot path of every request. Caching the effective permission set in the session or in a short-lived cache avoids the database round trip.

async function getUserPermissions(userId) {
  const cached = await cache.get(`perms:${userId}`);
  if (cached) return new Set(cached);
  const permissions = await loadPermissionsFromDb(userId);
  await cache.set(`perms:${userId}`, [...permissions], { ttl: 300 });
  return permissions;
}

The cache must be invalidated when the user’s roles change. A short TTL bounds the staleness.

The role assignment

Roles are assigned by administrators, not by the user. The assignment is a privileged operation and requires its own permission check.

app.post("/admin/users/:id/roles",
  requireAuth,
  requirePermission("user:assign_role"),
  async (req, res) => {
    await assignRole(req.params.id, req.body.roleId);
    await cache.del(`perms:${req.params.id}`);
    res.json({ ok: true });
  });

The cache invalidation is part of the assignment. A stale cache after a role change is a security bug.


b. Attribute-Based Access Control

ABAC evaluates policies against four categories of attributes: subject (the user), resource (the object being accessed), action (what is being done), and environment (the context of the request). The policy is a function that returns a decision.

The policy function

The policy is a pure function. It takes the request context and returns a boolean. The context includes the user, the resource, the action, and any environment data.

function canEditPost(user, post, env) {
  if (user.role === "admin") return true;
  if (post.authorId === user.id) return true;
  if (user.role === "moderator" && post.status === "flagged") return true;
  if (env.time.getHours() < 9 || env.time.getHours() > 17) return false;
  return false;
}

The function is testable in isolation. The rules are visible in one place. The context is passed explicitly, so nothing is hidden in the request object.

The policy engine

A policy engine stores rules as data and evaluates them at runtime. The rules can be expressed in a DSL, in JSON, or in a small expression language.

const policies = [
  {
    action: "post:edit",
    effect: "allow",
    condition: (ctx) => ctx.user.id === ctx.resource.authorId,
  },
  {
    action: "post:edit",
    effect: "allow",
    condition: (ctx) => ctx.user.role === "admin",
  },
  {
    action: "post:edit",
    effect: "deny",
    condition: (ctx) => ctx.resource.locked === true,
  },
];

function evaluate(action, context) {
  const applicable = policies.filter((p) => p.action === action);
  for (const policy of applicable) {
    if (policy.effect === "deny" && policy.condition(context)) return false;
  }
  for (const policy of applicable) {
    if (policy.effect === "allow" && policy.condition(context)) return true;
  }
  return false;
}

The deny rules are evaluated first, so an explicit deny overrides any allow. The default is deny.

The policy decision point

The policy engine is the Policy Decision Point (PDP). The route handler is the Policy Enforcement Point (PEP). The handler asks the PDP for a decision and enforces it.

async function authorize(action, resourceLoader) {
  return async (req, res, next) => {
    const resource = await resourceLoader(req);
    const context = {
      user: req.user,
      resource,
      action,
      env: { time: new Date(), ip: req.ip },
    };
    if (!evaluate(action, context)) {
      return res.status(403).json({ error: "forbidden" });
    }
    req.resource = resource;
    next();
  };
}

app.patch("/posts/:id",
  requireAuth,
  authorize("post:edit", (req) => Post.findByPk(req.params.id)),
  updatePostHandler);

The separation is the point. The policy engine knows nothing about HTTP. The route handler knows nothing about policy. Each can be tested and changed independently.

The policy externalization

For larger systems, the policy can live outside the application. OPA (Open Policy Agent) is a general-purpose policy engine that evaluates Rego policies. The application sends a query with the context, and OPA returns a decision.

const response = await fetch("http://opa:8181/v1/data/posts/edit/allow", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ input: context }),
});
const { result } = await response.json();
if (!result) return res.status(403).end();

The policy is versioned separately, reviewed by security, and applied uniformly across services.


c. Choosing between RBAC and ABAC

The two models are not mutually exclusive. Most systems use RBAC for the coarse-grained decisions and ABAC for the rules that depend on the resource or the environment.

When RBAC is sufficient

RBAC is sufficient when the permissions are stable, the roles are few, and the rules do not depend on the resource’s properties. An admin panel with a fixed set of features is a good fit. A content management system with editors, authors, and reviewers is a good fit.

When ABAC is required

ABAC is required when the rules depend on the resource’s owner, the resource’s state, the time of day, the user’s department, or any other attribute that varies per request. A document-sharing system where access depends on the document’s sharing settings is a good fit. A healthcare system where access depends on the patient-provider relationship is a good fit.

The hybrid pattern

The hybrid pattern uses roles to gate the entry point and attributes to gate the specific resource. The role check is cheap and runs first. The attribute check runs only if the role check passes.

app.patch("/posts/:id",
  requireAuth,
  requirePermission("post:edit"),
  authorize("post:edit", (req) => Post.findByPk(req.params.id)),
  updatePostHandler);

The permission check confirms the user has the capability at all. The attribute check confirms the user has the capability for this specific resource.

The decision table

ScenarioModel
Fixed feature set, few rolesRBAC
Resource ownershipABAC
Time-based accessABAC
Department-based accessABAC
Coarse-grained gatingRBAC
Fine-grained resource rulesABAC
Both coarse and fineHybrid

The anti-pattern: roles for everything

A common mistake is to model every attribute as a role. post_owner, post_moderator, post_admin becomes dozens of pseudo-roles. The role table becomes a denormalized copy of the resource’s state. This is a sign that ABAC is needed.


Complete Example Session

// ============================================
// PART 1: RBAC DATA MODEL
// ============================================

// users (id, email, password_hash)
// roles (id, name)
// permissions (id, name)
// user_roles (user_id, role_id)
// role_permissions (role_id, permission_id)

async function getUserPermissions(userId) {
  const rows = await db.query(`
    SELECT DISTINCT p.name
    FROM users u
    JOIN user_roles ur ON ur.user_id = u.id
    JOIN role_permissions rp ON rp.role_id = ur.role_id
    JOIN permissions p ON p.id = rp.permission_id
    WHERE u.id = $1
  `, [userId]);
  return new Set(rows.map((r) => r.name));
}

// ============================================
// PART 2: RBAC MIDDLEWARE
// ============================================

function requirePermission(permission) {
  return async (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: "unauthenticated" });
    const permissions = await getUserPermissions(req.user.id);
    if (!permissions.has(permission)) {
      return res.status(403).json({ error: "forbidden" });
    }
    next();
  };
}

// ============================================
// PART 3: THE ABAC POLICY ENGINE
// ============================================

const policies = [
  {
    action: "post:edit",
    effect: "deny",
    condition: (ctx) => ctx.resource.locked,
  },
  {
    action: "post:edit",
    effect: "allow",
    condition: (ctx) => ctx.user.role === "admin",
  },
  {
    action: "post:edit",
    effect: "allow",
    condition: (ctx) => ctx.user.id === ctx.resource.authorId,
  },
];

function evaluate(action, context) {
  const applicable = policies.filter((p) => p.action === action);
  for (const p of applicable) {
    if (p.effect === "deny" && p.condition(context)) return false;
  }
  for (const p of applicable) {
    if (p.effect === "allow" && p.condition(context)) return true;
  }
  return false;
}

// ============================================
// PART 4: THE ABAC MIDDLEWARE
// ============================================

function authorize(action, loadResource) {
  return async (req, res, next) => {
    const resource = await loadResource(req);
    if (!resource) return res.status(404).json({ error: "not found" });
    const context = {
      user: req.user,
      resource,
      action,
      env: { time: new Date(), ip: req.ip },
    };
    if (!evaluate(action, context)) {
      return res.status(403).json({ error: "forbidden" });
    }
    req.resource = resource;
    next();
  };
}

// ============================================
// PART 5: THE HYBRID ROUTE
// ============================================

app.patch("/posts/:id",
  requireAuth,
  requirePermission("post:edit"),
  authorize("post:edit", (req) => Post.findByPk(req.params.id)),
  async (req, res) => {
    await req.resource.update(req.body);
    res.json(req.resource);
  });

// ============================================
// PART 6: POLICY TESTS
// ============================================

const assert = require("node:assert");

const admin = { id: 1, role: "admin" };
const author = { id: 2, role: "author" };
const other = { id: 3, role: "author" };

const post = { id: 10, authorId: 2, locked: false };
const lockedPost = { id: 11, authorId: 2, locked: true };

assert.strictEqual(evaluate("post:edit", { user: admin, resource: post }), true);
assert.strictEqual(evaluate("post:edit", { user: author, resource: post }), true);
assert.strictEqual(evaluate("post:edit", { user: other, resource: post }), false);
assert.strictEqual(evaluate("post:edit", { user: admin, resource: lockedPost }), false);
assert.strictEqual(evaluate("post:edit", { user: author, resource: lockedPost }), false);

// ============================================
// PART 7: CACHING PERMISSIONS
// ============================================

async function getUserPermissionsCached(userId) {
  const key = `perms:${userId}`;
  const cached = await redis.get(key);
  if (cached) return new Set(JSON.parse(cached));
  const permissions = await getUserPermissions(userId);
  await redis.set(key, JSON.stringify([...permissions]), "EX", 300);
  return permissions;
}

// ============================================
// PART 8: INVALIDATING ON ROLE CHANGE
// ============================================

app.post("/admin/users/:id/roles",
  requireAuth,
  requirePermission("user:assign_role"),
  async (req, res) => {
    await assignRole(req.params.id, req.body.roleId);
    await redis.del(`perms:${req.params.id}`);
    res.json({ ok: true });
  });

// ============================================
// PART 9: THE DENY-OVERRIDES PATTERN
// ============================================

// Deny rules are evaluated first.
// An explicit deny cannot be overridden by an allow.
// This is the standard behavior in policy engines.

const policiesWithDeny = [
  { action: "post:edit", effect: "deny", condition: (ctx) => ctx.resource.locked },
  { action: "post:edit", effect: "allow", condition: (ctx) => ctx.user.role === "admin" },
];

// An admin cannot edit a locked post.
// The deny rule wins.

// ============================================
// PART 10: AUDIT LOGGING
// ============================================

function authorizeWithAudit(action, loadResource) {
  return async (req, res, next) => {
    const resource = await loadResource(req);
    const context = { user: req.user, resource, action };
    const decision = evaluate(action, context);
    await auditLog.write({
      userId: req.user?.id,
      action,
      resourceId: resource?.id,
      decision: decision ? "allow" : "deny",
      timestamp: new Date(),
    });
    if (!decision) return res.status(403).json({ error: "forbidden" });
    req.resource = resource;
    next();
  };
}

The ten parts showed the RBAC data model, middleware, the ABAC policy engine, ABAC middleware, the hybrid route, policy tests, caching, invalidation, deny precedence, and audit logging. The same posts resource ran through every part, showing how the two models combine.


Quick Reference

RBAC Tables

TablePurpose
usersUser accounts
rolesNamed permission bundles
permissionsIndividual capabilities
user_rolesUser-to-role assignments
role_permissionsRole-to-permission assignments

ABAC Context

CategoryExamples
SubjectUser ID, role, department, clearance
ResourceOwner, status, classification, locked
ActionRead, write, delete, approve
EnvironmentTime, IP, device, location

RBAC vs ABAC

AspectRBACABAC
GranularityCoarseFine
RulesFixedDynamic
Resource-awareNoYes
Environment-awareNoYes
ComplexityLowHigh
TestabilityEasyEasy with pure functions

Policy Patterns

PatternPurpose
Deny firstExplicit deny overrides any allow
Default denyAbsence of an allow is a deny
Centralized PDPPolicy decisions in one place
PEP at routeEnforcement at the handler
Audit logRecord every decision

Best Practices

✅ Do This:

// Centralize policy decisions in one function.
function evaluate(action, context) { /* ... */ }  // ✅

// Use middleware for enforcement.
app.delete("/posts/:id", requireAuth, requirePermission("post:delete"));  // ✅

// Deny first, then allow.
for (const p of policies) if (p.effect === "deny" && p.condition(ctx)) return false;  // ✅

// Cache permissions with a short TTL.
await redis.set(key, data, "EX", 300);  // ✅

// Invalidate the cache on role change.
await redis.del(`perms:${userId}`);  // ✅

// Test the policy function in isolation.
assert.strictEqual(evaluate("post:edit", ctx), true);  // ✅

// Log every decision.
await auditLog.write({ userId, action, decision });  // ✅

❌ Don’t Do This:

// Don't scatter permission checks across handlers.
if (req.user.role === "admin") { /* ... */ }  // ❌ In every handler

// Don't trust the client's role claim.
const role = req.body.role;  // ❌ Server decides

// Don't model resource attributes as roles.
role: "post_owner_42"  // ❌ Role explosion

// Don't forget to invalidate the cache.
await assignRole(userId, roleId);  // ❌ Stale permissions

// Don't default to allow.
return true;  // ❌ Default deny instead

// Don't skip the resource load.
authorize("post:edit");  // ❌ Needs the resource for ABAC

// Don't log only denials.
// Log allows too, for audit.  // ❌

Common Pitfalls

PitfallWhy It HappensFix
Scattered checksNo centralized policyUse a PDP function
Role explosionAttributes modeled as rolesSwitch to ABAC
Stale permissionsCache not invalidatedDelete cache on role change
Missing resource checkRBAC onlyAdd ABAC for resource-specific rules
Default allowNo fallbackDefault deny
Deny overriddenAllow evaluated firstEvaluate deny rules first
Inconsistent decisionsLogic duplicatedOne policy engine
No audit trailNo loggingLog every decision

Real-World Examples

1. RBAC Middleware

app.delete("/posts/:id", requireAuth, requirePermission("post:delete"), handler)

2. ABAC Policy

condition: (ctx) => ctx.user.id === ctx.resource.authorId

3. Hybrid Route

app.patch("/posts/:id", requireAuth, requirePermission("post:edit"), authorize("post:edit", load), handler)

4. Deny Precedence

for (const p of policies) if (p.effect === "deny" && p.condition(ctx)) return false

5. Permission Cache

await redis.set(`perms:${userId}`, data, "EX", 300)

6. Cache Invalidation

await redis.del(`perms:${userId}`)

7. Policy Test

assert.strictEqual(evaluate("post:edit", { user: author, resource: post }), true)

8. Audit Log

await auditLog.write({ userId, action, resourceId, decision })

9. Role Hierarchy

roles (id, name, parent_id)

10. External Policy Engine

const response = await fetch("http://opa:8181/v1/data/posts/edit/allow", { ... })

Visual

RBAC Structure

┌────────────────────────────────────────────────────────────┐
│  RBAC MODEL                                                │
│                                                            │
│  ┌─────────┐         ┌─────────┐         ┌─────────────┐  │
│  │  users  │───N:M──▶│  roles  │───N:M──▶│ permissions │  │
│  └─────────┘         └─────────┘         └─────────────┘  │
│                                                            │
│  Alice ──▶ [editor, reviewer]                              │
│  editor ──▶ [post:create, post:edit]                       │
│  reviewer ──▶ [post:approve]                               │
│                                                            │
│  Alice's effective permissions:                            │
│    post:create, post:edit, post:approve                    │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  Roles are named bundles of permissions.          │     │
│  │  Users hold roles.                                │     │
│  │  The union of permissions is the effective set.   │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

ABAC Context

┌────────────────────────────────────────────────────────────┐
│  ABAC DECISION CONTEXT                                     │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  SUBJECT (who)                                    │     │
│  │    user.id, user.role, user.department            │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  RESOURCE (what)                                  │     │
│  │    post.authorId, post.locked, post.status        │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  ACTION (how)                                     │     │
│  │    post:edit, post:delete, post:approve           │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  ENVIRONMENT (when/where)                         │     │
│  │    time, ip, device, location                     │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  POLICY ENGINE → allow or deny                    │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Hybrid Pattern

┌────────────────────────────────────────────────────────────┐
│  HYBRID RBAC + ABAC                                        │
│                                                            │
│  REQUEST                                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  PATCH /posts/42                                  │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  requireAuth          ← who is the user?          │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  requirePermission("post:edit")  ← RBAC           │     │
│  │    Does the user have the capability at all?      │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  authorize("post:edit", loadPost)  ← ABAC         │     │
│  │    Does the user have the capability for this     │     │
│  │    specific resource?                             │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  HANDLER                                          │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  RBAC gates the entry point.                               │
│  ABAC gates the specific resource.                         │
└────────────────────────────────────────────────────────────┘

Deny Precedence

┌────────────────────────────────────────────────────────────┐
│  POLICY EVALUATION ORDER                                   │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  1. Collect policies matching the action.         │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  2. Evaluate DENY rules first.                    │     │
│  │     If any matches → return deny.                 │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  3. Evaluate ALLOW rules.                         │     │
│  │     If any matches → return allow.                │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  4. No match → default deny.                      │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  An explicit deny cannot be overridden by an allow.        │
│  The default is deny.                                      │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
RBACPermissions assigned to roles, roles to users
ABACPolicies evaluated against subject, resource, action, environment
Effective permissionsUnion of all permissions from all roles
Role hierarchyRoles inherit from parent roles
Policy engineCentralized decision function (PDP)
Enforcement pointMiddleware at the route (PEP)
Deny precedenceDeny rules override allow rules
Default denyNo matching allow means denied
Cache invalidationDelete on role change
Audit logRecord every decision

Key takeaways:

  • RBAC is the default. It is simple, auditable, and sufficient when the permissions are stable and the roles are few. The five-table schema is the canonical model.
  • ABAC is for rules that depend on the resource or the environment. Resource ownership, time of day, department, and resource state cannot be expressed in RBAC without role explosion.
  • The hybrid pattern is the practical choice. Use RBAC to gate the entry point and ABAC to gate the specific resource. The role check is cheap and runs first; the attribute check runs only if the role check passes.
  • Centralize policy decisions. A pure function that takes a context and returns a decision is testable, auditable, and visible. Scattered checks are invisible, untested, and easy to forget.
  • Deny rules override allow rules. Explicit denies are evaluated first. The default is deny. An allow rule cannot override a deny rule.
  • Cache permissions with a short TTL, and invalidate on role change. The permission lookup is on the hot path. The cache must be deleted when the user’s roles change.
  • Log every decision. An audit log that records allows and denies is the evidence that compliance reviews ask for. Denials alone are not enough.
  • Separate the PDP from the PEP. The policy engine knows nothing about HTTP. The route handler knows nothing about policy. Each can be tested and changed independently.

Remember: Authorization is a policy decision, and policy decisions belong in one place. RBAC assigns permissions to roles and roles to users; it is simple and auditable, and it is the correct starting point for most applications. ABAC evaluates policies against the subject, resource, action, and environment; it is more expressive and the correct choice when the rules depend on the resource or the context. Real systems use both: RBAC for the coarse-grained gate, ABAC for the fine-grained resource rule. The policy engine is a pure function, tested in isolation. The middleware is the enforcement point, composed with authentication. Deny rules override allow rules, and the default is deny. Permissions are cached and invalidated on change. Every decision is logged. The result is an authorization system that is correct, testable, and auditable, and that does not hide its policy in the corners of route handlers.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!