Lesson 04 / 11
File Permissions and Ownership
Running the same 70 access requests through two permission models: the permission-bits model's loose expression producing 6 excess grants, its strict expression 6 excess denials, the two expressions differing from each other in 12 requests, the access control list expressing the policy in full yet 6 requests flipping when the entry order changes, and ownership sitting outside both models.
Published Updated
In this lesson
- Expresses the same access policy in two permission models and compares the results request by request
- Counts the request each model cannot express in two directions: excess grant and excess deny
- Computes how many requests' results change with entry order in an access control list
Contents
The previous lesson counted the 17 risky items a clean install leaves and noted that several of them directly set the permission scheme. When installation finishes, the machine already has a permission scheme: accounts, groups, and who has access to what on which object.
This lesson descends into that scheme. The mechanics of permission bits — three classes, three access types, octal notation, the exclusivity of class selection — were established in the Introduction to Linux course and are not repeated here. The question here is different: when the same policy is expressed in two separate permission models, how many requests give a different result, and which request can each model not express at all.
What Is Measured Is Not Mechanics but Expressive Power
A permission model does two jobs at once. The first is responding to an access request; this is mechanical, and it is learned. The second is being able to carry the policy the organization has in mind; this is the model’s expressive power, and it is limited.
When the two jobs are conflated, the measurement is lost. “The permissions were set correctly” is the mechanical job. The sentence that carries a decision is this: can this policy be expressed exactly in this model, and if not, how many requests does the nearest expression cost, and which way does that cost fall — toward permission granted in excess, or toward legitimate work blocked by a denial made in excess.
Policy, Two Models, and 70 Requests
The example is five files on the branch server of the fictional regional measurement network. The subjects, files, and policy are fictional; the evaluation below is a model — there is no real system or product.
HO17: seven subjects, five objects, and two operations produce 70 requests; intent is the
result the policy wants. HO18: the permission-bits model carries three classes, and class
selection is exclusive. HO19: for each object, the group with the least deviation is
chosen; the model is given its best chance. HO20: two expression strategies are tried —
loose gives the others class the union of the outside needs; strict leaves the others class
empty. HO21: in the access control list model, the first matching entry decides; the list is
evaluated both in the order it was written and in reverse. HO22: deviation is counted in two
directions — excess grant and excess deny are separate columns.
// it/permission.mjs — the same access request set run through two permission models (model): // fictional measurement network files, no real system or product. No randomness. export const SUBJECT = { "field-technician": "field", "branch-manager": "branch", "collector-service": "service", "billing-service": "service", "data-analyst": "analytics", "auditor": "audit", "guest-device": "guest" }; export const GROUP = [...new Set(Object.values(SUBJECT))]; export const OPERATION = ["r", "w"]; // intent: the access the policy wants. acl: the list the admin wrote (deny entries first). export const OBJECT = [ { name: "raw-read", owner: "collector-service", intent: { "collector-service": "rw", "field-technician": "r", "branch-manager": "r", "data-analyst": "r", "auditor": "r" }, acl: [["deny", "billing-service", "rw"], ["allow", "service", "rw"], ["allow", "field", "r"], ["allow", "branch", "r"], ["allow", "analytics", "r"], ["allow", "audit", "r"]] }, { name: "verified-read", owner: "data-analyst", intent: { "data-analyst": "rw", "billing-service": "r", "branch-manager": "r", "auditor": "r" }, acl: [["deny", "collector-service", "rw"], ["allow", "service", "r"], ["allow", "analytics", "rw"], ["allow", "branch", "r"], ["allow", "audit", "r"]] }, { name: "invoice-draft", owner: "billing-service", intent: { "billing-service": "rw", "branch-manager": "r", "auditor": "r" }, acl: [["deny", "collector-service", "rw"], ["allow", "service", "rw"], ["allow", "branch", "r"], ["allow", "audit", "r"]] }, { name: "key-file", owner: "collector-service", intent: { "collector-service": "r" }, acl: [["deny", "billing-service", "rw"], ["allow", "service", "r"]] }, { name: "audit-log", owner: "auditor", intent: { "auditor": "r", "collector-service": "w", "billing-service": "w" }, acl: [["allow", "service", "w"], ["allow", "audit", "r"]] }, ]; export const REQUEST = Object.keys(SUBJECT).flatMap((o) => OBJECT.flatMap((n) => OPERATION.map((i) => ({ o, n, i })))); export const intent = (n, o, i) => (n.intent[o] || "").includes(i); // Permission-bits model: three classes, class choice is exclusive (established in M03/K01). const combine = (n, who) => who.filter((o) => o !== n.owner) .map((o) => n.intent[o] || "").join(""); export function bits(n, group, loose) { const member = Object.keys(SUBJECT).filter((o) => SUBJECT[o] === group); const outside = Object.keys(SUBJECT).filter((o) => SUBJECT[o] !== group && o !== n.owner); return { group, owner: n.intent[n.owner] || "", g: combine(n, member), others: loose ? combine(n, outside) : "" }; } export const bitsResult = (b, n, o, i) => (o === n.owner ? b.owner : SUBJECT[o] === b.group ? b.g : b.others).includes(i); // Best group choice: the group with the least deviation. Deviation = excess grant + excess deny. export function best(n, loose) { return GROUP.map((g) => bits(n, g, loose)) .map((b) => ({ b, ...deviation((o, i) => bitsResult(b, n, o, i), [n]) })) .reduce((a, x) => (x.excessGrant + x.excessDeny < a.excessGrant + a.excessDeny ? x : a)); } // Access control list model: the first matching entry decides. export const aclResult = (n, o, i, reversed) => { const l = reversed ? [...n.acl].reverse() : n.acl; const g = l.find(([, who, ops]) => (who === o || who === SUBJECT[o]) && ops.includes(i)); return g ? g[0] === "allow" : false; }; export function deviation(f, objects = OBJECT) { const k = REQUEST.filter((x) => objects.includes(x.n)); return { excessGrant: k.filter((x) => f(x.o, x.i, x.n) && !intent(x.n, x.o, x.i)).length, excessDeny: k.filter((x) => !f(x.o, x.i, x.n) && intent(x.n, x.o, x.i)).length }; }
// it/permissionmeasure.mjs — the same request set in two permission models: how many requests differ, how many the model cannot express. import { SUBJECT, OBJECT, REQUEST, intent, bitsResult, best, aclResult, deviation } from "./permission.mjs"; const write = (g, ...s) => console.log(s.map((v, i) => (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join("")); const B = { loose: {}, strict: {} }; for (const n of OBJECT) for (const s of ["loose", "strict"]) B[s][n.name] = best(n, s === "loose").b; const bitsFn = (s) => (o, i, n) => bitsResult(B[s][n.name], n, o, i); const MODEL = [["permission-bits (loose)", bitsFn("loose")], ["permission-bits (strict)", bitsFn("strict")], ["access-list (deny-first)", (o, i, n) => aclResult(n, o, i, false)], ["access-list (allow-first)", (o, i, n) => aclResult(n, o, i, true)]]; const granted = REQUEST.filter((x) => intent(x.n, x.o, x.i)).length; console.log(`${Object.keys(SUBJECT).length} subjects x ${OBJECT.length} objects x 2 operations = ` + `${REQUEST.length} requests; granted in intent ${granted}, denied ${REQUEST.length - granted}`); const A = [-28, 14, 13, 16]; console.log("\n1. how far each model deviates from intent"); write(A, "model", "excess grant", "excess deny", "same as intent"); for (const [name, f] of MODEL) { const s = deviation(f); write(A, name, s.excessGrant, s.excessDeny, REQUEST.length - s.excessGrant - s.excessDeny); } const C = [-20, -14, -14, 14, 13]; console.log("\n2. leftover per object in the permission-bits model (with the best group choice)"); write(C, "object", "loose group", "strict group", "excess grant", "excess deny"); for (const n of OBJECT) { const g = best(n, true); const s = best(n, false); write(C, n.name, g.b.group, s.b.group, g.excessGrant, s.excessDeny); } const D = [-56, 11]; console.log("\n3. difference between models (requests with a different result)"); write(D, "pair", "different"); for (let x = 0; x < MODEL.length; x++) for (let y = x + 1; y < MODEL.length; y++) write(D, `${MODEL[x][0]} / ${MODEL[y][0]}`, REQUEST.filter((r) => MODEL[x][1](r.o, r.i, r.n) !== MODEL[y][1](r.o, r.i, r.n)).length); const E = [-24, 14, 17]; console.log("\n4. where the extra access goes"); write(E, "subject", "loose bits", "allow-first acl"); for (const o of Object.keys(SUBJECT)) { const f = (g) => REQUEST.filter((x) => x.o === o && g(x.o, x.i, x.n) && !intent(x.n, x.o, x.i)).length; const a = f(bitsFn("loose")); const b = f((s, i, n) => aclResult(n, s, i, true)); if (a || b) write(E, o, a, b); }
7 subjects x 5 objects x 2 operations = 70 requests; granted in intent 19, denied 51 1. how far each model deviates from intent model excess grant excess deny same as intent permission-bits (loose) 6 0 64 permission-bits (strict) 0 6 64 access-list (deny-first) 0 0 70 access-list (allow-first) 6 0 64 2. leftover per object in the permission-bits model (with the best group choice) object loose group strict group excess grant excess deny raw-read service field 1 3 verified-read field branch 2 2 invoice-draft field branch 3 1 key-file field field 0 0 audit-log service service 0 0 3. difference between models (requests with a different result) pair different permission-bits (loose) / permission-bits (strict) 12 permission-bits (loose) / access-list (deny-first) 6 permission-bits (loose) / access-list (allow-first) 8 permission-bits (strict) / access-list (deny-first) 6 permission-bits (strict) / access-list (allow-first) 12 access-list (deny-first) / access-list (allow-first) 6 4. where the extra access goes subject loose bits allow-first acl collector-service 2 3 billing-service 0 3 data-analyst 1 0 guest-device 3 0
The numbers belong to the measurement class; their inputs are HO17–HO22.
What Permission Bits Cannot Express
The first two rows of the first table are this lesson’s main finding. The permission-bits model resolves 64 of 70 requests the same way as intent. The remaining 6 requests are wrong in both strategies. The direction differs: the loose expression produces 6 excess grants, the strict expression 6 excess denials.
This not being zero is not a setting mistake. The code tries all five groups for each object and picks the one with the least deviation; the model has been given its best chance. The leftover comes from the model’s structure: there are three classes, and a subject can only be reached through one of those three. If the distinction the policy wants does not fit into three classes — if readers sit in four separate groups and one subject must be kept out — only an approximate expression remains.
The second table shows where the leftover accumulates. key-file and audit-log both give zero
leftover in either strategy; in both, the access set falls within a single owner and a single
group. invoice-draft produces 3 excess grants in the loose expression, because the only way to
cover two readers in two different groups is to open the others class, and that class holds three
more subjects.
The fourth table writes down who the cost goes to. One of the subjects that gains excess access
in the loose expression is guest-device, and it gains 3 requests. This subject has no
access to any object under the policy; the model has no place to keep it out, because the
“others” class covers everyone. What the permission-bits model cannot express is not a
permission — it is an exception.
The Direction of the Approximation Is Chosen, Its Existence Is Not
The first row of the third table compares the two strategies: the loose and strict expressions differ from each other in 12 requests. Although the total deviation is 6 in both, the number of requests where they diverge is double that, because the deviations sit on different requests and point in opposite directions.
This is the manageable part of an approximate expression. If the model cannot express the policy exactly, the administrator picks a direction. The loose direction does not stop work but opens out-of-policy access on 6 requests. The strict direction grants no excess access but denies 6 legitimate requests; in the measurement network, this means the auditor cannot read the invoice draft and the branch manager cannot access the raw read.
What cannot be chosen is the deviation itself. The total deviation is 6 in both directions; which strategy is chosen only decides where the cost is written. The question to ask when choosing a permission model is not “which one is safer” but “in how many requests can my policy not be expressed in this model, and which direction can I write the cost of those requests to.”
What an Access Control List Brings, and What It Does Not
The third row of the first table is zero: the access control list model, when deny entries are
evaluated first, resolves all 70 of 70 requests the same way as intent. Because an entry can be
written per subject, the three-class limit disappears, and the guest-device problem never
arises.
The fourth row, however, shows the cost. The same list produces 6 excess grants when the
entries are evaluated in reverse order. The last row of the third table writes this directly: the
two orderings differ from each other in 6 requests. These 6 requests belong to subjects who are
the target of a group-level grant but must be kept out by an individual deny entry —
collector-service and billing-service each gain 3 requests in the fourth table.
The difference here is not a typo — it is that the model’s second axis is order. In the permission-bits model, an object’s permission state is a single value, and reading it determines the result; in an access control list, the same policy can be written in different orders, and two lists carrying the same entries give different results. As expressive power grows, the result becomes harder to read: a list’s correctness cannot be seen by looking at its entries — it can only be understood by running it over the request set.
This is the second place the boundary falls short. Zero deviation rests on the assumption that the list is written correctly and evaluated in the correct order; when either of these two conditions is lost, the model drops to the same number as the permission-bits model’s loose expression — 6 excess grants.
Outside Both: Ownership
There is one item neither model measures. An object’s ownership sits not inside the
permission decisions but above them: the owner can change both the bits and the list. In the
model, the owner of the audit-log file is auditor, and the policy grants the auditor only
read — but because it is the owner, it can grant itself write.
This is clear once it is written in the course’s own measure. The items required to cross an object’s permission boundary, no matter what the policy on that object says, is 1: ownership being compromised. The items reached when the boundary falls are that object’s entire set of permission decisions. This is why ownership is not audited like a permission line — it is counted separately. In the measurement network’s five files, ownership is distributed across four separate subjects, and this distribution determines how many files’ permission scheme opens when a single subject is compromised.
Counting ownership separately also corrects the measurement’s third column. The first two columns — the deviation count and the deviation’s direction — measure the model’s expressive power; the third column measures how long that expression stays valid. In a set of files whose ownership is concentrated in one subject, the policy is void the moment that subject is compromised, and the deviation count loses its meaning.
Summary
- The permission-bits model cannot express 6 of 70 requests; the loose expression produces 6 excess grants, the strict expression 6 excess denials. The leftover comes not from the group choice but from the three-class structure.
- What the model cannot express is not a permission but an exception:
guest-device, which has no access under the policy, gains 3 requests in the loose expression, because the “others” class covers everyone. - The two strategies differ from each other in 12 requests. The deviation’s existence cannot be chosen, only its direction: between access opened in excess and legitimate work stopped.
- The access control list model expresses the same policy exactly across all 70 requests, but it brings a second axis: 6 requests turn into excess grants when the entries are evaluated in reverse order.
- As expressive power grows, correctness becomes unreadable; a list’s correctness cannot be seen by looking at its entries — it can only be understood by running it over the request set.
- Ownership sits outside both models: the items required to cross the permission boundary are 1, no matter what the policy says, and when the boundary falls, that object’s entire set of permission decisions opens.
Next Step
The permission scheme is now measured: which model can express what, and cannot, is written down with a number. But all of this measurement never asked who changes permissions and how. A permission line changing is an event, and the trace that event leaves behind matters as much as the permission line itself.
The next lesson does the same job in two management forms — through a graphical interface and through the command line — and counts the trace items each leaves behind: how many records fall, how many can be reproduced, how many never appear in the audit trail.
To keep your progress and take notes, Log in
My notes
Log in to take notes.