1/** 2 * TASK-0134 — independent brute-force oracle for the permission engine. 3 * 4 * This is a SEPARATE implementation of the documented resolution contract 5 * (packages/permissions/src/resolve.ts, header "Resolution contract", steps 6 * a–f) written from the PROSE contract, not from the engine's code. The 7 * differential fuzz (`permission-fuzz.test.ts`) feeds the same random world 8 * to both and asserts they agree on the final level. A subtle engine bug 9 * (off-by-one in the ancestor walk, a wrong max, a share-visibility slip, an 10 * explicit/inherit precedence mix-up) surfaces as an oracle mismatch. 11 * 12 * The oracle is intentionally written in a DIFFERENT shape than the engine 13 * (declarative tables + reduce/find-first instead of the engine's 14 * filter+loop) so that a bug shared by a literal copy would still be caught: 15 * the two must independently reach the same answer. 16 * 17 * DoD: tests/ may not import app code, but the engine packages 18 * (@factory/permissions, @factory/database-engine) are pure and in scope — 19 * this file imports only the type surface + the level helpers, never the 20 * engine's decision function. 21 */ 22import { 23 levelRank, 24 maxLevel, 25 type PermissionLevel, 26 type ResourceType, 27 type WorkspaceRole 28} from "@factory/permissions"; 29import type { AncestorRef, Grant, PermissionRequest } from "@factory/permissions"; 30import type { Share } from "@factory/permissions"; 31 32// --------------------------------------------------------------------------- 33// The role-baseline table, restated from the contract prose (step b). 34// owner/admin are full_access at the root AND on descendants; member collapses 35// from edit (root) to view (descendant); guest collapses from view (root) to 36// none (descendant). 37// --------------------------------------------------------------------------- 38const BASE_TABLE: Record<WorkspaceRole, { root: PermissionLevel; descendant: PermissionLevel }> = { 39 owner: { root: "full_access", descendant: "full_access" }, 40 admin: { root: "full_access", descendant: "full_access" }, 41 member: { root: "edit", descendant: "view" }, 42 guest: { root: "view", descendant: "none" } 43}; 44 45// --------------------------------------------------------------------------- 46// Grant applicability (step d of the contract): user → id match, team → the 47// user is in the team, role → the granteeId equals the requester's role. 48// Written as a single predicate (the engine switch-cases it). 49// --------------------------------------------------------------------------- 50function applies( 51 g: Grant, 52 req: Pick<PermissionRequest, "userId" | "userTeams" | "workspaceRole"> 53): boolean { 54 if (g.granteeType === "user") return g.granteeId === req.userId; 55 if (g.granteeType === "team") return req.userTeams.includes(g.granteeId); 56 return g.granteeId === req.workspaceRole; // role 57} 58 59// --------------------------------------------------------------------------- 60// Share visibility (step e), restated from rules.ts prose: public/workspace → 61// always; team → user in teamId; users → user in userIds; private → never. 62// A "none" share level contributes nothing even when visible. 63// --------------------------------------------------------------------------- 64function shareVisible( 65 s: Share, 66 req: Pick<PermissionRequest, "userId" | "userTeams"> 67): boolean { 68 switch (s.scope) { 69 case "public": 70 case "workspace": 71 return true; 72 case "team": 73 return s.teamId !== undefined && req.userTeams.includes(s.teamId); 74 case "users": 75 return (s.userIds ?? []).includes(req.userId); 76 case "private": 77 return false; 78 } 79} 80 81// --------------------------------------------------------------------------- 82// The oracle decision. Independent formulation of final = explicit 83// (overrides) else max(inherited, base, share). 84// --------------------------------------------------------------------------- 85export function oracleLevel(req: PermissionRequest): PermissionLevel { 86 const atRoot = req.resourceType === "workspace"; 87 88 // (b) baseline 89 const base = (atRoot ? BASE_TABLE[req.workspaceRole].root : BASE_TABLE[req.workspaceRole].descendant) ?? "none"; 90 91 // (e) shares — max visible level 92 let share: PermissionLevel = "none"; 93 for (const s of req.shares) { 94 if (shareVisible(s, req) && s.level !== "none") share = maxLevel(share, s.level); 95 } 96 97 // (c) inheritance — the NEAREST ancestor (excluding the resource itself, 98 // which is the last chain entry) that has ≥1 applicable grant wins; its 99 // max applicable level is inherited. No chain / no match → none. 100 // 101 // Recorded engine semantics (mirror): a grant with `level: "none"` on an 102 // ancestor IS still an applicable grant for the nearest-ancestor rule — 103 // it BLOCKS inheritance from farther ancestors and contributes no level. 104 // (The engine's `inheritedFromAncestors` filters applicability by 105 // grantee+resource ONLY; the none-skip happens inside the max.) An 106 // explicit (exact-resource) none grant is different — it is never "explicit" 107 // and cannot override (see the (d) loop below, which keeps the level check). 108 let inherited: PermissionLevel = "none"; 109 const chain = req.ancestorChain; 110 if (chain !== undefined && chain.length > 0) { 111 // nearest-to-farthest over the ANCESTORS (indices 0..len-2; len-1 is the 112 // resource itself and is excluded). 113 const ancestors: AncestorRef[] = chain.slice(0, chain.length - 1).reverse(); 114 for (const anc of ancestors) { 115 const matching = req.grants.filter( 116 (g) => applies(g, req) && g.resourceType === anc.resourceType && g.resourceId === anc.id 117 ); 118 if (matching.length > 0) { 119 inherited = matching.reduce<PermissionLevel>( 120 (m, g) => (g.level === "none" ? m : maxLevel(m, g.level)), 121 "none" 122 ); 123 break; // nearest wins — stop at the first applicable ancestor 124 } 125 } 126 } 127 128 // (d) explicit — max applicable level on the exact resource (none if none) 129 let explicit: PermissionLevel = "none"; 130 for (const g of req.grants) { 131 if ( 132 applies(g, req) && 133 g.resourceType === req.resourceType && 134 g.resourceId === req.resourceId && 135 g.level !== "none" 136 ) { 137 explicit = maxLevel(explicit, g.level); 138 } 139 } 140 141 // (f) final — explicit overrides; else max(inherited, base, share). 142 if (explicit !== "none") return explicit; 143 return maxLevel(inherited, maxLevel(base, share)); 144} 145 146// A level must always be a valid member of LEVELS (a liveness invariant the 147// fuzz asserts in addition to oracle agreement). 148export function isLevel(x: unknown): x is PermissionLevel { 149 return ( 150 x === "none" || x === "view" || x === "comment" || x === "edit" || x === "full_access" 151 ); 152} 153 154// rank exposed for assertions (the engine's own ranking). 155export function rank(l: PermissionLevel): number { 156 return levelRank(l); 157} 158 159export type { ResourceType }; 160