/** * The host-read gate's decisions, made for a viewer under the shell's default * display ceiling with the resolver and providers a worker builds for that * viewer, over documents the owner wrote and labeled. Every value the visitor * may not see carries a string that appears nowhere else, so a search of an * answer for it is a search for the value having escaped. */ import { expect } from "@std/expect"; import { describe, it } from "@std/testing/bdd"; import { type CfcAtom, cfcAtom } from "@commonfabric/api/cfc"; import type { FabricValue } from "@commonfabric/data-model"; import { FabricBytes } from "@commonfabric/data-model/fabric-primitives"; import { rootRenderPolicyFor } from "@commonfabric/html/worker"; import { Identity } from "@commonfabric/identity"; import { defaultRenderConfidentialityCeiling } from "@commonfabric/lib-shell/runtime"; import { type Cell, KeepAsCell, NAME, Runtime } from "@commonfabric/runner"; import { nameSchema, rendererVDOMSchema, stringSchema, } from "@commonfabric/runner/schemas"; import { StorageManager } from "@commonfabric/runner/storage/cache.deno"; import { SEED_ENVELOPE_SCHEMA_HASH, seedStoredEnvelope, writeSeedEnvelopeDoc, } from "../../../runner/test/cfc-seed-envelope.ts"; import { HostReadGate } from "@/backends/host-read-gate.ts"; import { renderConfidentialityResolverFor, renderMembershipProviderFor, renderModulePolicySourceFor, } from "@/backends/runtime-processor.ts"; import { createCellRef } from "@/backends/utils.ts"; import type { CellUpdateNotification } from "@/protocol/mod.ts"; const owner = await Identity.fromPassphrase("host read gate owner"); const visitor = await Identity.fromPassphrase("host read gate visitor"); const space = owner.did(); const ownerOnly = cfcAtom.user(owner.did()); const SEALED_ENTRY = "entry-behind-the-seal"; const OWNER_ONLY_NOTE = "owner-only-margin-note"; const SEALED_NAME = "name-behind-the-seal"; const SEALED_RENAME = "second-name-behind-the-seal"; const SEALED_INTERNAL = "internal-state-behind-the-seal"; const HOME_NAME = "name-for-members-of-the-owner-space"; const CREDENTIAL = "credential-even-the-owner-is-not-shown"; type Labels = readonly [path: string[], confidentiality: readonly CfcAtom[]][]; /** The documents every case reads, and the runtime holding them. */ async function shelf() { const storageManager = StorageManager.emulate({ as: owner }); const runtime = new Runtime({ storageManager, apiUrl: new URL("http://localhost"), }); /** Writes `value` beside `meta`, labeling each path in `labels`. */ const write = async ( id: string, value: unknown, labels: Labels = [], meta: Record = {}, ): Promise> => { const tx = runtime.edit(); const cell = runtime.getCell(space, id, undefined, tx); writeSeedEnvelopeDoc(tx, space); seedStoredEnvelope(tx, { space, id: cell.getAsNormalizedFullLink().id!, type: "application/json", path: [], }, { value, ...meta, ...(labels.length === 0 ? {} : { cfc: { version: 1, schemaHash: SEED_ENVELOPE_SCHEMA_HASH, labelMap: { version: 1, entries: labels.map(([path, confidentiality]) => ({ path, label: { confidentiality }, })), }, }, }), } as FabricValue); expect((await tx.commit().settled).ok).toBeDefined(); return runtime.getCell(space, id); }; const link = (cell: Cell) => cell.getAsLink({ includeSchema: true, keepAsCell: KeepAsCell.All }); const sealedEntry = await write("sealed-entry", SEALED_ENTRY, [[ [], [ownerOnly], ]]); // A piece anyone may see but for one entry, a document of its own. const piece = await write("piece", { [NAME]: "Shared piece", entry: link(sealedEntry), }); // A piece whose sealed entry sits one level inside an untyped field. const nestedPiece = await write("nested-piece", { [NAME]: "Nested piece", entry: { inner: link(sealedEntry) }, }); // A piece whose whole document only its owner may see. const sealedPiece = await write( "sealed-piece", { [NAME]: SEALED_NAME }, [[[], [ownerOnly]]], { internal: { note: SEALED_INTERNAL } }, ); // A piece one of whose fields only its owner may see. const notedPiece = await write( "noted-piece", { [NAME]: "Noted piece", note: OWNER_ONLY_NOTE }, [[["note"], [ownerOnly]]], { internal: { cache: SEALED_INTERNAL } }, ); const publicPiece = await write("public-piece", { [NAME]: "Public piece", entry: "public entry", }); // A piece labeled with the owner's space, which only the exchange rules // resolve, for a member of that space. const homePiece = await write( "home-piece", { [NAME]: HOME_NAME }, [[[], [cfcAtom.space(space)]]], ); // The owner's piece holding a credential, which the default ceiling shows // no one, beside the `openPath` stream and `sidebarUI` it exports: once in // a document of its own, and once in the piece's own document. const credential = await write( "credential", { token: CREDENTIAL, account: "owner account" }, [[["token"], [cfcAtom.resource("CredentialSecret")]], [[], [ownerOnly]]], ); const sidebar = await write("importer-sidebar", { type: "vnode", name: "div", props: {}, children: ["Accounts"], }, [[[], [ownerOnly]]]); const exportsOf = { [NAME]: "Importer", openPath: { $stream: true }, sidebarUI: link(sidebar), }; const importer = await write("importer", { ...exportsOf, auth: link(credential), }, [[[], [ownerOnly]]]); const inlineImporter = await write("inline-importer", { ...exportsOf, auth: { token: CREDENTIAL }, }, [ [[], [ownerOnly]], [["auth", "token"], [cfcAtom.resource("CredentialSecret")]], ]); await runtime.idle(); return { runtime, write, credential, importer, inlineImporter, sealedEntry, piece, nestedPiece, sealedPiece, notedPiece, publicPiece, homePiece, async [Symbol.asyncDispose]() { await runtime.dispose(); await storageManager.close(); }, }; } /** The gate a worker builds for `viewer`, under the shell's default ceiling. */ function gateFor(runtime: Runtime, viewer: Identity): HostReadGate { const ceiling = defaultRenderConfidentialityCeiling(viewer.did()); const membership = renderMembershipProviderFor(runtime, viewer, ceiling); const modulePolicies = renderModulePolicySourceFor(runtime, ceiling); return new HostReadGate(rootRenderPolicyFor(ceiling), { resolveConfidentiality: renderConfidentialityResolverFor( runtime, viewer, ceiling, viewer.did(), membership, modulePolicies, ), membership, modulePolicies, }); } /** `cell`'s address, as a ref carries it with no label view. */ function address(cell: Cell) { const { cfcLabelView: _withheld, ...ref } = createCellRef(cell); return ref; } /** Whether `text` appears anywhere in `answer`. */ function holds(answer: unknown, text: string): boolean { return JSON.stringify(answer ?? null).includes(text); } /** * Subscribes through `gate` and returns the first `count` updates it * delivers, once they have been delivered. */ function updatesOf( gate: HostReadGate, cell: Cell, count: number, ): Promise { return new Promise((resolve) => { const updates: CellUpdateNotification[] = []; const cancel = gate.subscribe( cell, createCellRef(cell), {}, () => {}, (u) => { updates.push(u); if (updates.length === count) { cancel(); resolve(updates); } }, ); }); } /** Subscribes through `gate`, keeping every update it delivers. */ function subscribe( gate: HostReadGate, cell: Cell, options: { includeCfcLabel?: boolean } = {}, ) { const updates: CellUpdateNotification[] = []; const cancel = gate.subscribe( cell, createCellRef(cell), options, () => {}, (u) => { updates.push(u); }, ); return { updates, cancel }; } describe("HostReadGate", () => { describe("read()", () => { it("refuses a visitor a piece that links to a sealed entry, returning no value", async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, visitor).read( docs.piece.asSchema(true), ); expect(holds(answer, SEALED_ENTRY)).toBe(false); expect(answer).toEqual({ refused: { refusedBy: "display-ceiling" } }); }); for (const untyped of [{}, true] as const) { it( `refuses a visitor a sealed entry inside a field typed \`${ JSON.stringify(untyped) }\``, async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, visitor).read( docs.nestedPiece.asSchema({ type: "object", properties: { [NAME]: { type: "string" }, entry: untyped }, }), ); expect(holds(answer, SEALED_ENTRY)).toBe(false); expect("refused" in answer).toBe(true); }, ); } for ( const [reader, schema] of [ ["the shell's title", stringSchema], ["the piece list", nameSchema], ] as const ) { it(`refuses a visitor a sealed piece's \`[NAME]\`, as ${reader} reads it`, async () => { await using docs = await shelf(); const target = reader === "the shell's title" ? docs.sealedPiece.key(NAME).asSchema(schema) : docs.sealedPiece.asSchema(schema); const answer = gateFor(docs.runtime, visitor).read(target); expect(holds(answer, SEALED_NAME)).toBe(false); expect("refused" in answer).toBe(true); }); } it("returns a visitor the `[NAME]` of a piece holding a sealed entry", async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, visitor).read( docs.piece.asSchema(nameSchema), ); expect(answer).toEqual({ value: { [NAME]: "Shared piece" } }); }); it("refuses a visitor a read that stops at a link to a sealed entry, on the entry's own label", async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, visitor).read( docs.piece.key("entry").asSchema({ asCell: ["cell"] }), ); expect(holds(answer, SEALED_ENTRY)).toBe(false); expect(answer).toEqual({ refused: { refusedBy: "display-ceiling" } }); }); it("returns a visitor the whole of a piece anyone may see", async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, visitor).read( docs.publicPiece.asSchema(true), ); expect(answer).toEqual({ value: { [NAME]: "Public piece", entry: "public entry" }, }); }); it("returns the owner their own sealed piece and linked entry", async () => { await using docs = await shelf(); const gate = gateFor(docs.runtime, owner); expect(gate.read(docs.piece.asSchema(true))).toEqual({ value: expect.objectContaining({ entry: SEALED_ENTRY }), }); expect(gate.read(docs.sealedPiece.key(NAME).asSchema(stringSchema))) .toEqual({ value: SEALED_NAME }); }); it("returns the owner a value labeled with their space, which only the exchange rules admit", async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, owner).read( docs.homePiece.key(NAME).asSchema(stringSchema), ); expect(answer).toEqual({ value: HOME_NAME }); }); it("refuses a visitor who is no member of the space the same value", async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, visitor).read( docs.homePiece.key(NAME).asSchema(stringSchema), ); expect(holds(answer, HOME_NAME)).toBe(false); expect("refused" in answer).toBe(true); }); it("returns a stream as the link it names", async () => { await using docs = await shelf(); const stream = docs.runtime.getCell(space, "a-stream", { asCell: ["stream"], }); const answer = gateFor(docs.runtime, visitor).read(stream); expect(answer).toEqual({ value: expect.objectContaining({ "/": expect.anything() }), }); }); it("returns every read as read with no policy", async () => { await using docs = await shelf(); const answer = new HostReadGate(undefined, {}).read( docs.piece.asSchema(true), ); expect(holds(answer, SEALED_ENTRY)).toBe(true); }); }); describe("what a piece exports, as the shell asks it", () => { // The shell asks a piece whether it exports an `openPath` stream, and // for its sidebar, by reading the piece's root for that one field. Each // read is decided on the piece's own node and on what it reads, so a // credential elsewhere in the piece does not refuse it. const openPathRead = { type: "object", properties: { openPath: { type: "object", properties: { $stream: { type: "boolean" } }, }, }, } as const; const sidebarRead = { type: "object", properties: { sidebarUI: { $ref: "#/$defs/vdomNode" } }, $defs: { ...rendererVDOMSchema.$defs }, } as const; for (const piece of ["importer", "inlineImporter"] as const) { const where = piece === "importer" ? "a document of its own" : "the piece's own document"; it(`refuses the owner the whole of a piece holding a credential in ${where}`, async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, owner).read( docs[piece].asSchema(true), ); expect(holds(answer, CREDENTIAL)).toBe(false); expect("refused" in answer).toBe(true); }); } it("returns the owner the `openPath` stream and `sidebarUI` beside a credential in a document of its own, each read from the root for that field", async () => { await using docs = await shelf(); const gate = gateFor(docs.runtime, owner); const openPath = gate.read(docs.importer.asSchema(openPathRead)); const sidebar = gate.read(docs.importer.asSchema(sidebarRead)); expect(openPath).toEqual({ value: { openPath: { $stream: true } } }); expect(sidebar).toEqual({ value: { sidebarUI: expect.objectContaining({ name: "div" }) }, }); expect(holds([openPath, sidebar], CREDENTIAL)).toBe(false); }); it("returns the owner the rest of a credential's document, field by field", async () => { await using docs = await shelf(); const gate = gateFor(docs.runtime, owner); expect("refused" in gate.read(docs.credential.asSchema(true))).toBe(true); expect( gate.read(docs.credential.key("account").asSchema(stringSchema)), ).toEqual({ value: "owner account" }); }); it("names the cell a refused read started from when asked, with no label view", async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, owner).read( docs.importer.asSchema(true), { includeRef: true, includeCfcLabel: true }, ); expect(answer).toEqual({ refused: { refusedBy: "display-ceiling" }, cell: expect.objectContaining({ id: expect.any(String), path: [] }), }); if (answer.cell === undefined) throw new Error("no cell was named"); expect("cfcLabelView" in answer.cell).toBe(false); expect(holds(answer, CREDENTIAL)).toBe(false); }); it("answers both reads of a piece that exports neither with neither", async () => { await using docs = await shelf(); const gate = gateFor(docs.runtime, visitor); for (const schema of [openPathRead, sidebarRead]) { expect(gate.read(docs.publicPiece.asSchema(schema))).toEqual({ value: {}, }); } }); it("refuses a visitor the same reads of the owner's piece", async () => { await using docs = await shelf(); const gate = gateFor(docs.runtime, visitor); for (const schema of [openPathRead, sidebarRead]) { expect(gate.read(docs.importer.asSchema(schema))).toEqual({ refused: { refusedBy: "display-ceiling" }, }); } }); }); describe("fields()", () => { it("lists the owner every field of a piece holding a credential in a document of its own, whose whole read it refuses", async () => { await using docs = await shelf(); const gate = gateFor(docs.runtime, owner); expect("refused" in gate.read(docs.importer.asSchema(true))).toBe(true); const answer = gate.fields(docs.importer); expect(holds(answer, CREDENTIAL)).toBe(false); expect(answer).toEqual({ fields: Object.fromEntries( ["$NAME", "openPath", "sidebarUI", "auth"].map((name) => [ name, { ...address(docs.importer), path: [name] }, ]), ), }); // Each field's own read is decided on its own. expect(gate.read(docs.importer.key("auth").asSchema(true))).toEqual({ refused: { refusedBy: "display-ceiling" }, }); expect(gate.read(docs.importer.key(NAME).asSchema(true))).toEqual({ value: "Importer", }); }); it("refuses the owner the list of a piece holding a credential in its own document, under a fit that decides on every label the document stores", async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, owner).fields(docs.inlineImporter); expect(answer).toEqual({ refused: { refusedBy: "display-ceiling" } }); expect(holds(answer, CREDENTIAL)).toBe(false); }); it("refuses a visitor the list of a piece only its owner may see", async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, visitor).fields(docs.sealedPiece); expect(answer).toEqual({ refused: { refusedBy: "display-ceiling" } }); expect(holds(answer, SEALED_NAME)).toBe(false); }); it("decides the list on the record's own schema, which the list's read does not carry", async () => { await using docs = await shelf(); // No stored label: only the record's schema says who may see it. const keys = await docs.write("keys", { "alice-secret-key": "alice's value", "bob-secret-key": "bob's value", }); const record = keys.asSchema({ type: "object", additionalProperties: { type: "string" }, ifc: { confidentiality: [ownerOnly] }, }); const visitorGate = gateFor(docs.runtime, visitor); expect("refused" in visitorGate.read(record)).toBe(true); const refused = visitorGate.fields(record); expect(refused).toEqual({ refused: { refusedBy: "display-ceiling" } }); expect(holds(refused, "alice-secret-key")).toBe(false); expect(gateFor(docs.runtime, owner).fields(record)).toEqual({ fields: Object.fromEntries( ["alice-secret-key", "bob-secret-key"].map((name) => [ name, address(record.key(name)), ]), ), }); }); it("answers a cell that holds no record with no list, and an empty record with an empty one", async () => { await using docs = await shelf(); const gate = gateFor(docs.runtime, owner); const notRecords = { "a-list": ["first", "second"], "a-string": "a string", "a-number": 7, // An instance, which holds its bytes in no field. "some-bytes": new FabricBytes(new Uint8Array([1, 2, 3])), }; for (const [id, value] of Object.entries(notRecords)) { expect(gate.fields(await docs.write(id, value))).toEqual({}); } expect(gate.fields(docs.runtime.getCell(space, "never-written"))) .toEqual({}); expect(gate.fields(await docs.write("an-empty-record", {}))).toEqual({ fields: {}, }); }); }); describe("readMetadata()", () => { for (const piece of ["sealedPiece", "notedPiece"] as const) { it( `refuses a visitor the metadata of ${ piece === "sealedPiece" ? "a sealed piece" : "a piece with one sealed field" }`, async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, visitor).readMetadata( docs[piece], "internal", ); expect(holds(answer, SEALED_INTERNAL)).toBe(false); expect("refused" in answer).toBe(true); }, ); } it("returns the owner their own piece's metadata", async () => { await using docs = await shelf(); const answer = gateFor(docs.runtime, owner).readMetadata( docs.sealedPiece, "internal", ); expect(answer).toEqual({ value: { note: SEALED_INTERNAL } }); }); }); describe("subscribe()", () => { it("delivers a refusal to a visitor, and no value", async () => { await using docs = await shelf(); const { updates, cancel } = subscribe( gateFor(docs.runtime, visitor), docs.sealedPiece.key(NAME).asSchema(stringSchema), ); cancel(); expect(holds(updates, SEALED_NAME)).toBe(false); expect(updates).toHaveLength(1); expect("refused" in updates[0]).toBe(true); }); it("delivers a refusal once a label written after it opened seals the value", async () => { await using docs = await shelf(); const { updates, cancel } = subscribe( gateFor(docs.runtime, visitor), docs.publicPiece.key(NAME).asSchema(stringSchema), ); await docs.write("public-piece", { [NAME]: "Public piece", entry: "public entry", }, [[[], [ownerOnly]]]); await docs.runtime.idle(); cancel(); expect(updates[0]).toEqual( expect.objectContaining({ value: "Public piece" }), ); expect("refused" in updates[updates.length - 1]).toBe(true); }); it("delivers a refusal for each change it refuses", async () => { await using docs = await shelf(); const { updates, cancel } = subscribe( gateFor(docs.runtime, visitor), docs.sealedPiece.key(NAME).asSchema(stringSchema), ); const before = updates.length; await docs.write("sealed-piece", { [NAME]: SEALED_RENAME }, [[ [], [ownerOnly], ]]); await docs.runtime.idle(); cancel(); expect(holds(updates, SEALED_RENAME)).toBe(false); expect(updates.length).toBeGreaterThan(before); expect(updates.every((update) => "refused" in update)).toBe(true); }); it("delivers nothing for a write that leaves the value and label as delivered", async () => { await using docs = await shelf(); const { updates, cancel } = subscribe( gateFor(docs.runtime, owner), docs.publicPiece.key(NAME).asSchema(stringSchema), { includeCfcLabel: true }, ); await docs.write("public-piece", { [NAME]: "Public piece", entry: "public entry", }); await docs.runtime.idle(); cancel(); expect(updates).toEqual([ expect.objectContaining({ value: "Public piece" }), ]); }); it("delivers a label change on its own", async () => { await using docs = await shelf(); const { updates, cancel } = subscribe( gateFor(docs.runtime, owner), docs.publicPiece.key(NAME).asSchema(stringSchema), { includeCfcLabel: true }, ); await docs.write("public-piece", { [NAME]: "Public piece", entry: "public entry", }, [[[], [cfcAtom.space(space)]]]); await docs.runtime.idle(); cancel(); expect(updates).toHaveLength(2); expect(updates[1]).toEqual(expect.objectContaining({ value: "Public piece", cfcLabel: expect.objectContaining({ entries: expect.anything() }), })); }); it("delivers the value admitted after a refusal, which the update before the refusal delivered too", async () => { await using docs = await shelf(); const { updates, cancel } = subscribe( gateFor(docs.runtime, visitor), docs.publicPiece.key(NAME).asSchema(stringSchema), ); const value = { [NAME]: "Public piece", entry: "public entry" }; await docs.write("public-piece", value, [[[], [ownerOnly]]]); await docs.runtime.idle(); await docs.write("public-piece", value); await docs.runtime.idle(); cancel(); expect(updates[0]).toEqual( expect.objectContaining({ value: "Public piece" }), ); expect("refused" in updates[1]).toBe(true); expect(updates.at(-1)).toEqual( expect.objectContaining({ value: "Public piece" }), ); }); it("delivers a sealed entry inside an untyped field to the owner, and refuses it to a visitor", async () => { await using docs = await shelf(); const schema = { type: "object", properties: { entry: {} }, } as const; const toOwner = subscribe( gateFor(docs.runtime, owner), docs.nestedPiece.asSchema(schema), ); const toVisitor = subscribe( gateFor(docs.runtime, visitor), docs.nestedPiece.asSchema(schema), ); toOwner.cancel(); toVisitor.cancel(); expect(holds(toOwner.updates, SEALED_ENTRY)).toBe(true); expect(holds(toVisitor.updates, SEALED_ENTRY)).toBe(false); expect("refused" in toVisitor.updates[0]).toBe(true); }); it("delivers a stream's event with the stream's label to a subscriber that asks for it", async () => { await using docs = await shelf(); const written = await docs.write( "labeled-stream", { $stream: true }, [[[], [ownerOnly]]], ); const stream = docs.runtime.getCell( space, "labeled-stream", { asCell: ["stream"] }, ); const { updates, cancel } = subscribe( gateFor(docs.runtime, owner), stream, { includeCfcLabel: true }, ); stream.send({ said: "hello" } as never); await docs.runtime.idle(); cancel(); expect(written).toBeDefined(); expect(updates).toEqual([ expect.objectContaining({ value: { said: "hello" }, cfcLabel: { version: 1, entries: [{ path: [], label: { confidentiality: [ownerOnly] } }], }, }), ]); }); it("delivers each of two equal events sent to a stream, under a display policy", async () => { await using docs = await shelf(); const stream = docs.runtime.getCell(space, "a-repeating-stream", { asCell: ["stream"], }); const delivered = updatesOf(gateFor(docs.runtime, owner), stream, 2); stream.send({ action: "increment" } as never); stream.send({ action: "increment" } as never); expect(await delivered).toEqual([ expect.objectContaining({ value: { action: "increment" } }), expect.objectContaining({ value: { action: "increment" } }), ]); }); it("delivers each of two equal events sent to a stream, with no policy", async () => { await using docs = await shelf(); const stream = docs.runtime.getCell(space, "a-repeating-stream", { asCell: ["stream"], }); const delivered = updatesOf(new HostReadGate(undefined, {}), stream, 2); stream.send({ action: "increment" } as never); stream.send({ action: "increment" } as never); expect(await delivered).toEqual([ expect.objectContaining({ value: { action: "increment" } }), expect.objectContaining({ value: { action: "increment" } }), ]); }); it("delivers a stream's events to the owner, deciding them on the stream's document", async () => { await using docs = await shelf(); const stream = docs.runtime.getCell(space, "an-owner-stream", { asCell: ["stream"], }); const { updates, cancel } = subscribe( gateFor(docs.runtime, owner), stream, ); stream.send({ said: "hello" } as never); await docs.runtime.idle(); cancel(); expect(updates).toEqual([ expect.objectContaining({ value: { said: "hello" } }), ]); }); }); });