---
title: 'Role-Based Access Control'
source: 'https://academia.sh/en/courses/authentication-and-authorization/role-based-access-control'
course: 'Authentication and Authorization'
language: en
updated: '2026-08-19T05:19:32+00:00'
license: 'CC BY-SA 4.0'
---

# Role-Based Access Control

Binding permissions to duty rather than to the person, how role inheritance conceals the effective permission set, where the role-based decision is right and wrong across a common scenario set, and measuring role explosion.

The previous topic established the answers to "who is in front of me." The
`AND member = ?` condition in the session-deletion query opened the second question:
what someone whose identity is known is allowed to do. This topic addresses that
question and answers it with four separate models. The models do not substitute for one
another; each can express a particular kind of constraint, and not another.

For the comparison to be meaningful, the four models will evaluate **the same scenario
set**. The set consists of eight requests drawn from the library loan service, and each
scenario also has a correct answer — the answer the library's business rule gives. Each
model's success is measured by how well its own decisions agree with that answer.

## Role, Permission, and Inheritance

**Role-based access control** binds permissions to duty rather than to the person. This
model was established for database accounts in the Relational Database Administration
course; the difference here is the layer. There, what was checked was which table a
connection could touch; here, what is checked is which operation a request can carry
out. The two layers exist separately, and neither substitutes for the other: if the
check at the application layer is skipped, the database layer is the last line of
defense.

A **permission** is the combination of an action and a resource type: `view:loan`,
`delete:fine`. A **role** is a named set of permissions. **Role inheritance** is one role
covering another role's permissions; the branch manager inherits the clerk role so that
it can do everything a clerk can.

Inheritance has a cost: a role's actual permission set is not visible where it is
written — it is computed by taking the closure of the inheritance chain. The script
below produces that closure and evaluates the common scenario set.

```bash
cat > rbac.mjs <<'EOF'
const ROLE_PERMISSIONS = {
  member:         ["view:loan", "view:list"],
  clerk:          ["update:loan", "create:loan"],
  branch_manager: ["delete:fine", "delete:loan"],
};
const ROLE_INHERITANCE = { member: [], clerk: ["member"], branch_manager: ["clerk"] };
const SUBJECT_ROLES = {
  "U-1001": ["member"], "U-1002": ["member"], "U-1005": ["member"],
  "P-2001": ["clerk"], "Y-3001": ["branch_manager"],
};

// Common scenario set: the four models in this topic evaluate the same set.
// [name, subject, action, object, object attributes, time, expected answer]
const SCENARIOS = [
  ["S1", "U-1001", "view",   "loan:501", { owner: "U-1001", branch: "S-02" }, "10:15", true],
  ["S2", "U-1001", "view",   "loan:502", { owner: "U-1002", branch: "S-02" }, "10:16", false],
  ["S3", "P-2001", "view",   "loan:502", { owner: "U-1002", branch: "S-02" }, "10:20", true],
  ["S4", "P-2001", "view",   "loan:503", { owner: "U-1005", branch: "S-01" }, "10:21", false],
  ["S5", "P-2001", "update", "loan:502", { owner: "U-1002", branch: "S-02" }, "22:40", false],
  ["S6", "Y-3001", "delete", "fine:77",  { owner: "U-1002", branch: "S-02" }, "11:05", true],
  ["S7", "U-1002", "view",   "list:9",   { owner: "U-1001", branch: "S-02" }, "12:00", true],
  ["S8", "U-1005", "view",   "list:9",   { owner: "U-1001", branch: "S-02" }, "12:01", false],
];

function effectivePermissions(role, seen = new Set()) {
  if (seen.has(role)) return new Set();            // guards against an inheritance cycle
  seen.add(role);
  const set = new Set(ROLE_PERMISSIONS[role] ?? []);
  for (const parent of ROLE_INHERITANCE[role] ?? []) {
    for (const permission of effectivePermissions(parent, seen)) set.add(permission);
  }
  return set;
}

const subjectPermissions = (subject) => {
  const k = new Set();
  for (const role of SUBJECT_ROLES[subject] ?? []) for (const p of effectivePermissions(role)) k.add(p);
  return k;
};

const rbac = (subject, action, object) =>
  subjectPermissions(subject).has(`${action}:${object.split(":")[0]}`);

console.log("role            | effective permission set");
console.log("----------------|-----------------------------------------------------");
for (const role of Object.keys(ROLE_PERMISSIONS)) {
  console.log(role.padEnd(15) + " | " + [...effectivePermissions(role)].sort().join(", "));
}

console.log("\nscenario | subject | action    | object     | RBAC   | expect | match");
console.log("---------|---------|-----------|------------|--------|--------|------");
let matched = 0;
for (const [name, subject, action, object, , , expected] of SCENARIOS) {
  const decision = rbac(subject, action, object);
  if (decision === expected) matched++;
  console.log(
    `${name}       | ${subject} | ${action.padEnd(9)} | ${object.padEnd(10)} | ` +
    `${(decision ? "allow" : "deny").padEnd(6)} | ${(expected ? "allow" : "deny").padEnd(6)} | ` +
    (decision === expected ? "+" : "-")
  );
}
console.log(`\nRBAC correct decisions: ${matched}/${SCENARIOS.length}`);
EOF
node rbac.mjs
```

```text
role            | effective permission set
----------------|-----------------------------------------------------
member          | view:list, view:loan
clerk           | create:loan, update:loan, view:list, view:loan
branch_manager  | create:loan, delete:fine, delete:loan, update:loan, view:list, view:loan

scenario | subject | action    | object     | RBAC   | expect | match
---------|---------|-----------|------------|--------|--------|------
S1       | U-1001 | view      | loan:501   | allow  | allow  | +
S2       | U-1001 | view      | loan:502   | allow  | deny   | -
S3       | P-2001 | view      | loan:502   | allow  | allow  | +
S4       | P-2001 | view      | loan:503   | allow  | deny   | -
S5       | P-2001 | update    | loan:502   | allow  | deny   | -
S6       | Y-3001 | delete    | fine:77    | allow  | allow  | +
S7       | U-1002 | view      | list:9     | allow  | allow  | +
S8       | U-1005 | view      | list:9     | allow  | deny   | -

RBAC correct decisions: 4/8
```

## Where the Model Is Right and Where It Is Wrong

Four of the eight scenarios come out right and four come out wrong. All the wrong ones
lean the same direction: the model grants more permission than it should. This direction
is not an accident — it comes from the model's structure.

S2 is the clearest case. U-1001 can view its own loan record, but not another member's
record. The role-based model cannot tell these two apart, because its decision is based
only on the subject and the resource *type*. `loan:501` and `loan:502` are the same
type; which one belongs to whom is information the model cannot see.

S4 is the staff side of the same problem. A clerk can view loan records, but which
branch's records it may view is not written into the permission name.

S5 is about time. Loan records not being changed outside business hours is a business
rule, and the role-based model has no place to express it — permissions do not see the
request's context.

S8 should be read together with S7. In both, the model says "allow"; in S7 this is
correct, in S8 it is wrong. Even the correct one is correct for the wrong reason: the
model does not know that the list is shared with U-1002 and not with U-1005 — it only
knows that every member can view a list. Getting the correct decision for the correct
reason is the subject of the next two lessons.

This foursome has a shared name: the role-based model is a **coarse-grained** model. It
reaches down to the resource's type, but not to its specific instance.

## Role Explosion

There is one way to resolve missing distinctions within the role-based model itself:
write the missing condition into the role's name. For the branch distinction,
`clerk_S01`, `clerk_S02`; for the hours distinction, on-hours and off-hours versions of
each. The cost of this path can be computed.

```bash
cat > role-explosion.mjs <<'EOF'
const dimensions = [
  ["duty", 4, "reader, clerk, cataloger, branch manager"],
  ["branch", 12, "the number of the library's branches"],
  ["hours", 2, "on-hours / off-hours"],
  ["record ownership", 2, "own record / someone else's record"],
];

console.log("added condition     | factor | roles required    | example");
console.log("--------------------|--------|--------------------|--------------------------------");
let roles = 1;
for (const [name, factor, example] of dimensions) {
  roles *= factor;
  console.log(name.padEnd(19) + " | " + String(factor).padStart(6) + " | " +
    String(roles).padStart(18) + " | " + example);
}
console.log(`\nexample role name: clerk_S02_on_hours_others_record`);
console.log(`assignments: the correct one of ${roles} roles is chosen for each staff member`);
EOF
node role-explosion.mjs
```

```text
added condition     | factor | roles required    | example
--------------------|--------|--------------------|--------------------------------
duty                |      4 |                  4 | reader, clerk, cataloger, branch manager
branch              |     12 |                 48 | the number of the library's branches
hours               |      2 |                 96 | on-hours / off-hours
record ownership    |      2 |                192 | own record / someone else's record

example role name: clerk_S02_on_hours_others_record
assignments: the correct one of 192 roles is chosen for each staff member
```

Four duties climb to forty-eight roles once a branch is added; two more conditions push
it to a hundred ninety-two. What matters is not the number itself but the shape of the
growth: every new condition **multiplies** the role count. When a thirteenth branch
opens, sixteen new roles must be defined, and each one is a record someone looks at by
hand.

The second cost is readability. What a role named `clerk_S02_on_hours_others_record`
means can be worked out from its name, but noticing one mistake among a hundred and
ninety-two such names cannot. A staff member assigned to the wrong role becomes
invisible to a glance at the permission chart.

The limit here is not a flaw in the role-based model but its scope. The model stays
plain and auditable exactly when the duty is fixed and the decision does not depend on
the request's context.

## Where the Model Is Used Correctly

The role-based model does not need to be abandoned; its limit needs to be known. A sound
setup makes the following distinction.

**The coarse-grained decision is given by role.** The answer to "is this request type
open to this duty?" lives in the role. A non-staff person never reaching the loan-record
update endpoint at all is the job of the role check, and it happens at the very start of
the request.

**The fine-grained decision is given by a separate layer.** The question "is this
specific record open to this person?" is the subject of the following lessons, and it
concerns the record itself, not the role.

**Roles are derived from duty, not from the person.** Adding "let this person also have
access" to a role contradicts the role's name. If there is a genuine need, a new role is
defined.

**Role assignments are audited regularly.** The gap between a person's effective
permission set and what their duty requires is measured. The database-side counterpart
of this audit was established in the Relational Database Administration course; the same
procedure is applied on the application side.

## Summary

- Role-based access control binds permissions to duty; a permission is the combination
  of an action and a resource type.
- Role inheritance makes the effective permission set invisible where it is written; the
  set is computed by taking the closure of the inheritance chain.
- The model is coarse-grained: it reaches the resource's type, not its instance. Across
  the common scenario set, four of eight decisions came out wrong, and all four leaned
  toward too much permission.
- Writing missing conditions into a role's name multiplies the role count with every
  condition; twelve branches and two conditions produce a hundred ninety-two roles.
- The role-based decision remains a coarse-grained filter at the start of the request;
  the record-level decision is given in a separate layer.

## Next Step

Three of the four wrong decisions come from the same gap: the model cannot see the
request's context. Who owns the record, which branch a staff member works in, what time
the request arrived — all of this is known at decision time but has no place in a
permission's name. The next lesson takes up attribute-based access control, which turns
this information into the decision's input, and re-evaluates the same eight scenarios
with this model.
