| 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; |
| 3 | |
| 4 | import { aclChain, type FolioAclNode, type FolioGrant, type Person, type SpaceRules } from "../access.ts"; |
| 5 | import { agentMayFind, agentReach, audienceRule, type AudienceRule } from "./agents.ts"; |
| 6 | |
| 7 | // The leak rules of docs/ARTIFACTS_MODE.md section 4.3. |
| 8 | |
| 9 | const ana: Person = { user_id: "ana", owner: false, teams: new Set(["web"]) }; |
| 10 | const bo: Person = { user_id: "bo", owner: false, teams: new Set() }; |
| 11 | const cy: Person = { user_id: "cy", owner: false, teams: new Set(["web"]) }; |
| 12 | |
| 13 | const open: SpaceRules = { kind: "workspace", team: null, default_role: "edit", members: [] }; |
| 14 | const team: SpaceRules = { kind: "team", team: "web", default_role: "edit", members: [] }; |
| 15 | |
| 16 | function node(id: string, over: Partial<FolioAclNode> = {}): FolioAclNode { |
| 17 | return { id, owner: "user:ana", parent_id: null, space_id: null, inherit: true, general_access: "none", general_role: null, ...over }; |
| 18 | } |
| 19 | |
| 20 | function reach(n: FolioAclNode, grants: Record<string, FolioGrant[]>, space: SpaceRules | null, rule: AudienceRule, people: Person[] = [], visits: string[] = [], asker = ana) { |
| 21 | return agentReach({ |
| 22 | chain: aclChain(n.id, new Map([[n.id, n]])), |
| 23 | grants: new Map(Object.entries(grants)), |
| 24 | space, |
| 25 | asker, |
| 26 | askerVisited: visits.includes(asker.user_id), |
| 27 | rule, |
| 28 | people, |
| 29 | visited: (p) => visits.includes(p.user_id), |
| 30 | agent_mode: "edit", |
| 31 | }); |
| 32 | } |
| 33 | |
| 34 | test("audience rules: none or only the asker is the asker; more than 20 people is the workspace", () => { |
| 35 | assert.deepEqual(audienceRule(null, "ana"), { kind: "asker" }); |
| 36 | assert.deepEqual(audienceRule({ kind: "people", user_ids: ["ana"] }, "ana"), { kind: "asker" }); |
| 37 | assert.deepEqual(audienceRule({ kind: "people", user_ids: ["ana", "bo", "bo"] }, "ana"), { kind: "people", user_ids: ["bo"] }); |
| 38 | assert.deepEqual(audienceRule({ kind: "workspace" }, "ana"), { kind: "workspace" }); |
| 39 | const crowd = Array.from({ length: 21 }, (_, i) => `u${i}`); |
| 40 | assert.deepEqual(audienceRule({ kind: "people", user_ids: crowd }, "ana"), { kind: "workspace" }); |
| 41 | const twenty = Array.from({ length: 19 }, (_, i) => `u${i}`); |
| 42 | assert.equal(audienceRule({ kind: "people", user_ids: twenty }, "ana").kind, "people"); |
| 43 | }); |
| 44 | |
| 45 | test("rule 1: a public channel finds only open-space and workspace-wide folios", () => { |
| 46 | const ws: AudienceRule = { kind: "workspace" }; |
| 47 | assert.equal(agentMayFind(reach(node("a", { space_id: "open" }), {}, open, ws)), true); |
| 48 | assert.equal(agentMayFind(reach(node("a", { general_access: "workspace", general_role: "view" }), {}, null, ws)), true); |
| 49 | // Private, shared, team, link: never in a public channel. |
| 50 | assert.equal(agentMayFind(reach(node("a"), {}, null, ws)), false); |
| 51 | assert.equal(agentMayFind(reach(node("a"), { a: [{ principal: "user:bo", role: "view" }] }, null, ws)), false); |
| 52 | assert.equal(agentMayFind(reach(node("a", { space_id: "team" }), {}, team, ws)), false); |
| 53 | assert.equal(agentMayFind(reach(node("a", { general_access: "link", general_role: "view" }), {}, null, ws, [], ["ana"])), false); |
| 54 | }); |
| 55 | |
| 56 | test("rule 1: a conversation finds only what everyone in it can read", () => { |
| 57 | const withBo: AudienceRule = { kind: "people", user_ids: ["bo"] }; |
| 58 | const shared = node("a"); |
| 59 | assert.equal(agentMayFind(reach(shared, { a: [{ principal: "user:bo", role: "view" }] }, null, withBo, [bo])), true); |
| 60 | assert.equal(agentMayFind(reach(shared, {}, null, withBo, [bo])), false); |
| 61 | // A team space: Cy is in the team, Bo isn't. |
| 62 | const t = node("t", { space_id: "team" }); |
| 63 | assert.equal(agentMayFind(reach(t, {}, team, { kind: "people", user_ids: ["cy"] }, [cy])), true); |
| 64 | assert.equal(agentMayFind(reach(t, {}, team, withBo, [bo])), false); |
| 65 | // A link folio: only when everyone opened it. |
| 66 | const link = node("l", { general_access: "link", general_role: "view" }); |
| 67 | assert.equal(agentMayFind(reach(link, {}, null, withBo, [bo], ["ana"])), false); |
| 68 | assert.equal(agentMayFind(reach(link, {}, null, withBo, [bo], ["ana", "bo"])), true); |
| 69 | }); |
| 70 | |
| 71 | test("rule 1: the asker's own Private is theirs alone", () => { |
| 72 | assert.equal(agentMayFind(reach(node("p"), {}, null, { kind: "asker" })), true); |
| 73 | assert.equal(agentMayFind(reach(node("p"), {}, null, { kind: "people", user_ids: ["bo"] }, [bo])), false); |
| 74 | // Someone else's Private: not even for its asker. |
| 75 | assert.equal(agentMayFind(reach(node("p", { owner: "user:bo" }), {}, null, { kind: "asker" })), false); |
| 76 | }); |
| 77 | |
| 78 | test("rule 2: reading what the audience can't all read says so", () => { |
| 79 | const r = reach(node("p"), {}, null, { kind: "workspace" }); |
| 80 | assert.equal(r.asker_role, "manage"); |
| 81 | assert.equal(r.audience_can_read, false); |
| 82 | const fine = reach(node("o", { space_id: "open" }), {}, open, { kind: "workspace" }); |
| 83 | assert.equal(fine.audience_can_read, true); |
| 84 | }); |
| 85 | |
| 86 | test("an agent's grant never widens what it can do for its asker", () => { |
| 87 | const n = node("f", { owner: "user:cy" }); |
| 88 | const grants = { f: [{ principal: "agent:ag1", role: "manage" as const }, { principal: "user:bo", role: "comment" as const }] }; |
| 89 | const r = reach(n, grants, null, { kind: "asker" }, [], [], bo); |
| 90 | assert.equal(r.asker_role, "comment"); |
| 91 | assert.deepEqual(r.can, { read: true, suggest: true, edit: false }); |
| 92 | const none = reach(n, grants, null, { kind: "asker" }, [], [], ana); |
| 93 | assert.equal(none.asker_role, null); |
| 94 | assert.deepEqual(none.can, { read: false, suggest: false, edit: false }); |
| 95 | assert.equal(agentMayFind(none), false); |
| 96 | }); |