| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { |
| 5 | aclChain, |
| 6 | aclRootOf, |
| 7 | agentAbilities, |
| 8 | agentFolioRole, |
| 9 | canShare, |
| 10 | effectiveRole, |
| 11 | explicitAccess, |
| 12 | folioPathOf, |
| 13 | folioReadableByAll, |
| 14 | folioReadableByWorkspace, |
| 15 | folioScope, |
| 16 | generalRoleCap, |
| 17 | inheritsSpace, |
| 18 | isPrivateFolio, |
| 19 | materialize, |
| 20 | roleOf, |
| 21 | type FolioAclNode, |
| 22 | type FolioGrant, |
| 23 | type Person, |
| 24 | type SpaceRules, |
| 25 | } from "./access.ts"; |
| 26 | |
| 27 | // People: Ana (team web), Bo (no team), Cy (team design), Wes (workspace owner). |
| 28 | const ana: Person = { user_id: "ana", owner: false, teams: new Set(["web"]) }; |
| 29 | const bo: Person = { user_id: "bo", owner: false, teams: new Set() }; |
| 30 | const cy: Person = { user_id: "cy", owner: false, teams: new Set(["design"]) }; |
| 31 | const wes: Person = { user_id: "wes", owner: true, teams: new Set() }; |
| 32 | |
| 33 | // Spaces. |
| 34 | const open: SpaceRules = { kind: "workspace", team: null, default_role: "edit", members: [] }; |
| 35 | const openView: SpaceRules = { kind: "workspace", team: null, default_role: "view", members: [] }; |
| 36 | const team: SpaceRules = { kind: "team", team: "web", default_role: "comment", members: [] }; |
| 37 | const membersOnly: SpaceRules = { kind: "private", team: null, default_role: null, members: [{ principal: "user:cy", role: "manage" }] }; |
| 38 | const SPACES: Record<string, SpaceRules> = { open, openView, team, membersOnly }; |
| 39 | |
| 40 | function node(id: string, over: Partial<FolioAclNode> = {}): FolioAclNode { |
| 41 | return { id, owner: "user:ana", parent_id: null, space_id: null, inherit: true, general_access: "none", general_role: null, created_at: "2026-10-01T00:00:00Z", ...over }; |
| 42 | } |
| 43 | |
| 44 | /** The role a person has on `id`, given every node and grant. */ |
| 45 | function roleIn(nodes: FolioAclNode[], grants: Record<string, FolioGrant[]>, id: string, person: Person, visited = false) { |
| 46 | const byId = new Map(nodes.map((n) => [n.id, n])); |
| 47 | const chain = aclChain(id, byId); |
| 48 | const root = chain[chain.length - 1]!; |
| 49 | const space = root.space_id ? SPACES[root.space_id]! : null; |
| 50 | return effectiveRole(chain, new Map(Object.entries(grants)), space ? roleOf(space, person) : null, person, { visited }); |
| 51 | } |
| 52 | |
| 53 | test("private: only the owner, and not the workspace owner", () => { |
| 54 | const nodes = [node("f")]; |
| 55 | assert.equal(roleIn(nodes, {}, "f", ana), "manage"); |
| 56 | assert.equal(roleIn(nodes, {}, "f", bo), null); |
| 57 | assert.equal(roleIn(nodes, {}, "f", wes), null); |
| 58 | const byId = new Map(nodes.map((n) => [n.id, n])); |
| 59 | assert.equal(isPrivateFolio(aclChain("f", byId), new Map()), true); |
| 60 | }); |
| 61 | |
| 62 | test("an open space gives every member its base role; owners manage", () => { |
| 63 | const nodes = [node("f", { space_id: "open", owner: "user:cy" })]; |
| 64 | assert.equal(roleIn(nodes, {}, "f", ana), "edit"); |
| 65 | assert.equal(roleIn(nodes, {}, "f", bo), "edit"); |
| 66 | assert.equal(roleIn(nodes, {}, "f", cy), "manage"); |
| 67 | assert.equal(roleIn(nodes, {}, "f", wes), "manage"); |
| 68 | const byId = new Map(nodes.map((n) => [n.id, n])); |
| 69 | assert.equal(isPrivateFolio(aclChain("f", byId), new Map()), false); |
| 70 | }); |
| 71 | |
| 72 | test("a team space: its team gets the base role; others nothing; workspace owners manage", () => { |
| 73 | const nodes = [node("f", { space_id: "team", owner: "user:cy" })]; |
| 74 | assert.equal(roleIn(nodes, {}, "f", ana), "comment"); |
| 75 | assert.equal(roleIn(nodes, {}, "f", bo), null); |
| 76 | assert.equal(roleIn(nodes, {}, "f", wes), "manage"); |
| 77 | }); |
| 78 | |
| 79 | test("a members-only space: only its members, never the workspace owner", () => { |
| 80 | const nodes = [node("f", { space_id: "membersOnly", owner: "user:cy" })]; |
| 81 | assert.equal(roleIn(nodes, {}, "f", cy), "manage"); |
| 82 | assert.equal(roleIn(nodes, {}, "f", ana), null); |
| 83 | assert.equal(roleIn(nodes, {}, "f", wes), null); |
| 84 | }); |
| 85 | |
| 86 | test("grants to a person, an agent and a team", () => { |
| 87 | const nodes = [node("f")]; |
| 88 | const grants = { f: [{ principal: "user:bo", role: "comment" as const }, { principal: "team:design", role: "edit" as const }, { principal: "agent:ag1", role: "edit" as const }] }; |
| 89 | assert.equal(roleIn(nodes, grants, "f", bo), "comment"); |
| 90 | assert.equal(roleIn(nodes, grants, "f", cy), "edit"); |
| 91 | // An agent grant gives no person anything. |
| 92 | assert.equal(roleIn(nodes, grants, "f", wes), null); |
| 93 | const byId = new Map(nodes.map((n) => [n.id, n])); |
| 94 | assert.equal(isPrivateFolio(aclChain("f", byId), new Map(Object.entries(grants))), false); |
| 95 | }); |
| 96 | |
| 97 | test("a grant raises a space role but never lowers it", () => { |
| 98 | const nodes = [node("f", { space_id: "openView", owner: "user:cy" })]; |
| 99 | assert.equal(roleIn(nodes, { f: [{ principal: "user:bo", role: "edit" }] }, "f", bo), "edit"); |
| 100 | assert.equal(roleIn(nodes, { f: [{ principal: "user:ana", role: "view" }] }, "f", ana), "view"); |
| 101 | const open2 = [node("f", { space_id: "open", owner: "user:cy" })]; |
| 102 | assert.equal(roleIn(open2, { f: [{ principal: "user:ana", role: "view" }] }, "f", ana), "edit"); |
| 103 | }); |
| 104 | |
| 105 | test("general access: workspace gives every member its role, never manage", () => { |
| 106 | const nodes = [node("f", { general_access: "workspace", general_role: "comment" })]; |
| 107 | assert.equal(roleIn(nodes, {}, "f", bo), "comment"); |
| 108 | assert.equal(roleIn(nodes, {}, "f", wes), "comment"); |
| 109 | const capped = [node("f", { general_access: "workspace", general_role: "manage" })]; |
| 110 | assert.equal(roleIn(capped, {}, "f", bo), "edit"); |
| 111 | assert.equal(generalRoleCap(null), "view"); |
| 112 | }); |
| 113 | |
| 114 | test("general access: link gives its role only to people who opened it", () => { |
| 115 | const nodes = [node("f", { general_access: "link", general_role: "view" })]; |
| 116 | assert.equal(roleIn(nodes, {}, "f", bo), null); |
| 117 | assert.equal(roleIn(nodes, {}, "f", bo, true), "view"); |
| 118 | }); |
| 119 | |
| 120 | test("nested docs inherit their parent's grants, space and general access", () => { |
| 121 | const nodes = [ |
| 122 | node("top", { space_id: "team", owner: "user:cy" }), |
| 123 | node("mid", { space_id: "team", owner: "user:cy", parent_id: "top" }), |
| 124 | node("leaf", { space_id: "team", owner: "user:cy", parent_id: "mid" }), |
| 125 | ]; |
| 126 | const grants = { top: [{ principal: "user:bo", role: "view" as const }] }; |
| 127 | assert.equal(roleIn(nodes, grants, "leaf", ana), "comment"); |
| 128 | assert.equal(roleIn(nodes, grants, "leaf", bo), "view"); |
| 129 | assert.equal(roleIn(nodes, grants, "leaf", wes), "manage"); |
| 130 | const byId = new Map(nodes.map((n) => [n.id, n])); |
| 131 | assert.deepEqual( |
| 132 | aclChain("leaf", byId).map((n) => n.id), |
| 133 | ["leaf", "mid", "top"], |
| 134 | ); |
| 135 | assert.equal(inheritsSpace(aclChain("leaf", byId)), true); |
| 136 | }); |
| 137 | |
| 138 | test("a parent's owner keeps full access to what others add under it", () => { |
| 139 | const nodes = [node("top"), node("child", { parent_id: "top", owner: "user:bo" })]; |
| 140 | assert.equal(roleIn(nodes, {}, "child", ana), "manage"); |
| 141 | assert.equal(roleIn(nodes, {}, "child", bo), "manage"); |
| 142 | assert.equal(roleIn(nodes, {}, "child", cy), null); |
| 143 | }); |
| 144 | |
| 145 | test("restricting stops the space, the parent's grants and general access", () => { |
| 146 | const nodes = [ |
| 147 | node("top", { space_id: "open", owner: "user:cy", general_access: "workspace", general_role: "view" }), |
| 148 | node("secret", { space_id: "open", owner: "user:cy", parent_id: "top", inherit: false }), |
| 149 | node("under", { space_id: "open", owner: "user:cy", parent_id: "secret" }), |
| 150 | ]; |
| 151 | const grants = { top: [{ principal: "user:bo", role: "edit" as const }], secret: [{ principal: "user:ana", role: "comment" as const }] }; |
| 152 | assert.equal(roleIn(nodes, grants, "secret", bo), null); |
| 153 | assert.equal(roleIn(nodes, grants, "secret", ana), "comment"); |
| 154 | assert.equal(roleIn(nodes, grants, "under", ana), "comment"); |
| 155 | assert.equal(roleIn(nodes, grants, "under", wes), null); |
| 156 | assert.equal(roleIn(nodes, grants, "under", cy), "manage"); |
| 157 | // A restricted top-level folio in a space leaves the space's people out too. |
| 158 | const top = [node("t", { space_id: "open", owner: "user:cy", inherit: false })]; |
| 159 | assert.equal(roleIn(top, {}, "t", ana), null); |
| 160 | assert.equal(roleIn(top, {}, "t", wes), null); |
| 161 | }); |
| 162 | |
| 163 | test("the access root and path of a new folio", () => { |
| 164 | assert.equal(aclRootOf({ id: "a", inherit: true, parent_id: null }, null), "a"); |
| 165 | assert.equal(aclRootOf({ id: "b", inherit: true, parent_id: "a" }, "a"), "a"); |
| 166 | assert.equal(aclRootOf({ id: "c", inherit: false, parent_id: "b" }, "a"), "c"); |
| 167 | assert.equal(folioPathOf("a", null), "/a/"); |
| 168 | assert.equal(folioPathOf("b", "/a/"), "/a/b/"); |
| 169 | }); |
| 170 | |
| 171 | test("materialize writes the owner, ancestor owners and grants up to the access root, highest role each", () => { |
| 172 | const nodes = [ |
| 173 | node("top", { owner: "user:ana" }), |
| 174 | node("child", { parent_id: "top", owner: "user:bo" }), |
| 175 | node("restricted", { parent_id: "child", owner: "user:bo", inherit: false }), |
| 176 | ]; |
| 177 | const byId = new Map(nodes.map((n) => [n.id, n])); |
| 178 | const grants = new Map<string, FolioGrant[]>([ |
| 179 | ["top", [{ principal: "user:cy", role: "view", granted_at: "2026-10-02T00:00:00Z" }]], |
| 180 | ["child", [{ principal: "user:cy", role: "edit", granted_at: "2026-10-03T00:00:00Z" }]], |
| 181 | ]); |
| 182 | const rows = materialize(["top", "child", "restricted"], byId, grants); |
| 183 | const of = (id: string) => Object.fromEntries(rows.filter((r) => r.folio_id === id).map((r) => [r.principal, `${r.role}@${r.via}`])); |
| 184 | assert.deepEqual(of("top"), { "user:ana": "manage@owner", "user:cy": "view@top" }); |
| 185 | assert.deepEqual(of("child"), { "user:bo": "manage@owner", "user:cy": "edit@child", "user:ana": "manage@top" }); |
| 186 | assert.deepEqual(of("restricted"), { "user:bo": "manage@owner" }); |
| 187 | assert.equal(explicitAccess(aclChain("child", byId), grants).get("user:cy")?.since, "2026-10-03T00:00:00Z"); |
| 188 | }); |
| 189 | |
| 190 | test("the index scope is the space's only when access is exactly the space's", () => { |
| 191 | const byId = (nodes: FolioAclNode[]) => new Map(nodes.map((n) => [n.id, n])); |
| 192 | const plain = byId([node("a", { space_id: "open" })]); |
| 193 | assert.equal(folioScope(aclChain("a", plain), new Map()), "space:open"); |
| 194 | assert.equal(folioScope(aclChain("a", plain), new Map([["a", [{ principal: "user:bo", role: "view" }]]])), "folio:a"); |
| 195 | const general = byId([node("a", { space_id: "open", general_access: "workspace", general_role: "view" })]); |
| 196 | assert.equal(folioScope(aclChain("a", general), new Map()), "folio:a"); |
| 197 | const nested = byId([node("a", { space_id: "open" }), node("b", { space_id: "open", parent_id: "a", owner: "user:bo" })]); |
| 198 | assert.equal(folioScope(aclChain("b", nested), new Map()), "space:open"); |
| 199 | const restricted = byId([node("a", { space_id: "open" }), node("b", { space_id: "open", parent_id: "a", inherit: false })]); |
| 200 | assert.equal(folioScope(aclChain("b", restricted), new Map()), "folio:b"); |
| 201 | const priv = byId([node("p")]); |
| 202 | assert.equal(folioScope(aclChain("p", priv), new Map()), "folio:p"); |
| 203 | }); |
| 204 | |
| 205 | test("readable by the whole workspace: open spaces and workspace general access only", () => { |
| 206 | const byId = (nodes: FolioAclNode[]) => new Map(nodes.map((n) => [n.id, n])); |
| 207 | assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a", { space_id: "open" })])), open), true); |
| 208 | assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a", { space_id: "team" })])), team), false); |
| 209 | assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a", { space_id: "membersOnly" })])), membersOnly), false); |
| 210 | assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a")])), null), false); |
| 211 | assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a", { general_access: "workspace", general_role: "view" })])), null), true); |
| 212 | // A link is never the workspace's, even for people who opened it. |
| 213 | assert.equal(folioReadableByWorkspace(aclChain("a", byId([node("a", { general_access: "link", general_role: "view" })])), null), false); |
| 214 | // Restricted inside an open space: not the workspace's. |
| 215 | const restricted = byId([node("a", { space_id: "open" }), node("b", { space_id: "open", parent_id: "a", inherit: false })]); |
| 216 | assert.equal(folioReadableByWorkspace(aclChain("b", restricted), open), false); |
| 217 | }); |
| 218 | |
| 219 | test("readable by an audience only when every person in it can read", () => { |
| 220 | const nodes = new Map([["f", node("f", { owner: "user:ana" })]]); |
| 221 | const chain = aclChain("f", nodes); |
| 222 | const grants = new Map([["f", [{ principal: "user:bo", role: "view" as const }]]]); |
| 223 | assert.equal(folioReadableByAll(chain, grants, null, [ana, bo]), true); |
| 224 | assert.equal(folioReadableByAll(chain, grants, null, [ana, bo, cy]), false); |
| 225 | const link = new Map([["l", node("l", { general_access: "link", general_role: "view" })]]); |
| 226 | assert.equal(folioReadableByAll(aclChain("l", link), new Map(), null, [ana, bo]), false); |
| 227 | assert.equal( |
| 228 | folioReadableByAll(aclChain("l", link), new Map(), null, [ana, bo], (p) => p.user_id === "bo"), |
| 229 | true, |
| 230 | ); |
| 231 | }); |
| 232 | |
| 233 | test("an agent is capped by its asker and narrowed by its audience", () => { |
| 234 | // The agent holds an edit grant, its asker only view: it may only read. |
| 235 | const nodes = [node("f", { owner: "user:cy" })]; |
| 236 | const grants = { f: [{ principal: "agent:ag1", role: "edit" as const }, { principal: "user:bo", role: "view" as const }] }; |
| 237 | const asker = roleIn(nodes, grants, "f", bo); |
| 238 | assert.equal(asker, "view"); |
| 239 | assert.equal(agentFolioRole(asker, true), "view"); |
| 240 | assert.deepEqual(agentAbilities(agentFolioRole(asker, true), "edit"), { read: true, suggest: false, edit: false }); |
| 241 | // Someone in the conversation can't read it: the agent can't either. |
| 242 | assert.equal(agentFolioRole("manage", false), null); |
| 243 | // Someone who can't read it gets nothing from the agent's grant. |
| 244 | assert.equal(agentFolioRole(roleIn(nodes, grants, "f", ana), true), null); |
| 245 | }); |
| 246 | |
| 247 | test("who may share", () => { |
| 248 | assert.equal(canShare("manage"), true); |
| 249 | assert.equal(canShare("edit"), false); |
| 250 | assert.equal(canShare("edit", true), true); |
| 251 | assert.equal(canShare(null, true), false); |
| 252 | }); |