Skip to content
academia.sh

Lesson 17 / 23

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.

Contents

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.

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
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.

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
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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close