Skip to content
academia.sh

Lesson 18 / 23

Attribute-Based Access Control

Deciding from subject, object, action, and context attributes, deny priority's role in combining rules, the attribute-based model's correct and wrong decisions on the same scenario set, and the consequence of where an attribute is read from.

Contents

In the previous lesson’s eight scenarios, four decisions came out wrong, and three of them shared the same cause: the role-based model cannot see the request’s context. Who owns the record, which branch the staff member works in, what time the request arrived — all of this is information known at decision time, but the permission’s name has no place for it.

Attribute-based access control turns this information into the decision’s input. What produces the decision is no longer membership in a permission list, but a rule that operates on attribute values.

Four Sets of Attributes

The model gathers attributes from four sources at decision time.

Subject attributes belong to the person making the request: their code, role, the branch they work at, seniority, which factors authenticated them.

Object attributes belong to the record being accessed: its type, owner, the branch it belongs to, its confidentiality class, its creation date.

Action is the operation to be carried out; it carries the same meaning as in the role-based model.

Context attributes belong neither to the subject nor to the object; they belong to the request itself: the time, the network location, the device the request arrived from, the session’s age.

A rule is a condition over the values of these four sets. The rule “can view its own record” is the equality of the subject’s code with the object’s owner; the branch rule is the equality of two branch attributes. These equalities were the thing the role-based model could not express, because it never looked at the record at all.

Combining Rules

More than one rule can match the same request, and their outcomes can conflict. The combination rule says what happens in that case. The default choice is deny priority: if one of the matching rules says deny, the decision is deny, no matter how many rules say allow. If no rule matches, the decision is still deny — allow is something that must be granted explicitly.

The evaluator below applies four rules with this combination and re-decides the previous lesson’s eight scenarios. The last column shows which rule produced the decision.

cat > abac.mjs <<'EOF'
const SUBJECTS = {
  "U-1001": { role: "member", branch: "S-02" }, "U-1002": { role: "member", branch: "S-02" },
  "U-1005": { role: "member", branch: "S-01" }, "P-2001": { role: "clerk", branch: "S-02" },
  "Y-3001": { role: "branch_manager", branch: "S-02" },
};

// Common scenario set from the previous lesson.
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],
];

const duringWorkingHours = (time) => time >= "08:30" && time <= "18:00";
const WRITE_ACTIONS = new Set(["update", "create", "delete"]);

const RULES = [
  { name: "R1 own record", effect: "allow",
    condition: (o, e, n) => e === "view" && n.owner === o.code },
  { name: "R2 branch staff", effect: "allow",
    condition: (o, e, n) => ["clerk", "branch_manager"].includes(o.role) &&
      n.branch === o.branch && ["view", "update", "create"].includes(e) },
  { name: "R3 fine deletion", effect: "allow",
    condition: (o, e, n) => o.role === "branch_manager" && n.branch === o.branch &&
      e === "delete" && n.type === "fine" },
  { name: "R4 off-hours write", effect: "deny",
    condition: (o, e, n, b) => WRITE_ACTIONS.has(e) && !duringWorkingHours(b.time) && o.role !== "branch_manager" },
];

function abac(subjectCode, action, objectName, objectAttrs, time) {
  const o = { code: subjectCode, ...SUBJECTS[subjectCode] };
  const n = { type: objectName.split(":")[0], ...objectAttrs };
  const b = { time };
  const matched = RULES.filter((r) => r.condition(o, action, n, b));
  const deny = matched.find((r) => r.effect === "deny");
  const allow = matched.find((r) => r.effect === "allow");
  if (deny) return [false, `${deny.name} (deny wins)`];
  if (allow) return [true, allow.name];
  return [false, "no matching rule"];
}

console.log("scenario | ABAC  | expect | match | rule that decided");
console.log("---------|-------|--------|-------|----------------------------");
let matched = 0;
for (const [name, subject, action, object, attrs, time, expected] of SCENARIOS) {
  const [decision, reason] = abac(subject, action, object, attrs, time);
  if (decision === expected) matched++;
  console.log(`${name}       | ${(decision ? "allow" : "deny").padEnd(5)} | ` +
    `${(expected ? "allow" : "deny").padEnd(6)} | ${(decision === expected ? "+" : "-").padEnd(5)} | ${reason}`);
}
console.log(`\nABAC correct decisions: ${matched}/${SCENARIOS.length}`);
EOF
node abac.mjs
scenario | ABAC  | expect | match | rule that decided
---------|-------|--------|-------|----------------------------
S1       | allow | allow  | +     | R1 own record
S2       | deny  | deny   | +     | no matching rule
S3       | allow | allow  | +     | R2 branch staff
S4       | deny  | deny   | +     | no matching rule
S5       | deny  | deny   | +     | R4 off-hours write (deny wins)
S6       | allow | allow  | +     | R3 fine deletion
S7       | deny  | allow  | -     | no matching rule
S8       | deny  | deny   | +     | no matching rule

ABAC correct decisions: 7/8

Three of the four decisions that came out wrong in the role-based model are now fixed. S2 and S4, by comparing the ownership and branch attributes; S5, by the context attribute. S8’s decision is now also given for the correct reason: the list is not U-1005’s, so no rule matches.

The reason column is the model’s second gain. Every decision can be produced together with the name of the rule that gave it; the answer to “why was it denied” can be written to a log. In the role-based model, that answer was nothing more than “that permission was not in the role.”

Where the Model Falls Short

S7 is still wrong. U-1001 shared reading list number nine with U-1002; U-1002 should be able to see that list. In the attribute-based model, the way to write this constraint is to turn the sharing into an attribute: a sharedWith field is added to the object, and the rule checks whether the subject’s code is in that list.

This path works at small scale and jams at two points. The first is transitivity: if U-1002 shares the list with its own working group, the group’s members should also gain access. The attribute list can only express this by including every group member one by one, and the attribute must be updated every time the group changes. The second is indirection: a constraint like “the records opened by staff working at the branch a branch manager manages” rests on a link two steps away and does not fit into a single attribute field.

Both cases are the signal that the constraint is not an attribute but a relationship. That is the subject of the next lesson.

Where the Attribute Comes From, and How Fresh It Is

The real operating question of the attribute-based model is not how the rules are written but where the attributes are read from. There are two options: the values are carried inside the identity token, or they are read from the record at decision time.

Reading from the token is fast and requires no extra query. Its cost is that the value is the value at the moment the token was issued. The example below decides two requests from a clerk who has changed branches, using both sources.

cat > freshness.mjs <<'EOF'
const RECORD = { "P-2001": { role: "clerk", branch: "S-01" } };   // moved from S-02 to S-01 at 12:00
const TOKEN = { code: "P-2001", role: "clerk", branch: "S-02", issuedAt: "09:00" };

const rule = (o, n) => o.role === "clerk" && n.branch === o.branch;

const requests = [
  ["loan:502 (branch S-02)", { branch: "S-02" }, false],   // should no longer see it
  ["loan:503 (branch S-01)", { branch: "S-01" }, true],    // should see it now
];

console.log("attribute source          | " + requests.map((i) => i[0]).join(" | "));
console.log("--------------------------|-----------------------|-----------------------");
for (const [name, o] of [["from token (09:00)", { ...TOKEN }],
                         ["from record (live)", { code: "P-2001", ...RECORD["P-2001"] }]]) {
  const cells = requests.map(([, n, expected]) => {
    const decision = rule(o, n);
    return `${decision ? "allow" : "deny"} ${decision === expected ? "(correct)" : "(WRONG)"}`.padEnd(21);
  });
  console.log((name.padEnd(25) + " | " + cells.join(" | ")).trimEnd());
}
EOF
node freshness.mjs
attribute source          | loan:502 (branch S-02) | loan:503 (branch S-01)
--------------------------|-----------------------|-----------------------
from token (09:00)        | allow (WRONG)         | deny (WRONG)
from record (live)        | deny (correct)        | allow (correct)

The stale value in the token produces a wrong decision in both directions at once: it allows access to a record the clerk should no longer see, and denies access to the record it should see. The second one surfaces as a support ticket and gets fixed; the first one never surfaces at all.

The decision rule reads: an attribute that changes the decision, and that can change, is read at decision time. Only attributes that never change, or whose change within the token’s lifetime is acceptable, are carried in the token. The shorter the token’s lifetime, the less this distinction matters — the lifetime decision in the refresh-token lesson is the same trade-off wearing a different face here.

Reading from the record costs one query per request. This cost is bounded by reading the attributes once per request and caching them for the request’s duration; the cache’s lifetime must not exceed the request’s lifetime.

The Auditability of Rules

The expressive power the attribute-based model gains comes with a cost: seeing what the rules do together gets harder. In the role-based model, an account’s effective permission set could be computed; in attribute-based rules there is no such set, because the decision depends on the request itself.

Two audit procedures are used instead.

The scenario set. This lesson’s table is itself an audit tool: known requests and expected answers are kept as a set, and the set is re-run every time a rule changes. A minus appearing in the match column means the rule’s text needs to be examined.

Rule coverage. A rule that never matches in any scenario is either unnecessary or written incorrectly; a rule that matches in every scenario is most likely too broad. How many times each rule matches can be counted, and this count says something about how the rule is written.

A third rule is one of writing discipline: rules must be free of side effects and must produce the same decision for the same input every time. A rule that writes a record, calls an external service, or uses a random value during the decision cannot be re-run and cannot be audited.

Summary

  • Attribute-based access control produces the decision from subject, object, action, and context attributes; the ownership and branch constraints that could not be expressed in the role-based model are written here as equality conditions.
  • Deny takes priority when combining rules, and the decision is deny if no rule matches.
  • The model can produce the reason behind every decision; this makes it possible to log the reason for a denial.
  • Transitive and indirect links do not fit into an attribute field; these constraints are relationships, not attributes.
  • An attribute that changes the decision is read at decision time if it can change; a stale value carried in the token produces a wrong decision in both directions at once.

Next Step

S7 has now been decided wrong two lessons in a row. A shared list’s accessibility belongs neither to the subject nor to the object — it is the link between the two. The next lesson takes up relationship-based access control, which turns these links into first-class data: objects and subjects form a graph, and the access question turns into a reachability query over that graph.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close