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