@@ -0,0 +1,139 @@
|
||||
// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0
|
||||
// Who may open which live stream (@conusai-kit/host/postgres `createHostLive`, umbrella TS6.2), decided with the
|
||||
// kernel's own tenancy over this app's registry: a member gets the collection for a topic they
|
||||
// resolve, with its ids; a signed-out request is 401; another organization's slug and a workspace
|
||||
// the caller does not resolve are 404; an entity that is not a collection is denied, and so is a
|
||||
// project collection asked for the whole organization; a member removed meanwhile is revoked at the
|
||||
// next re-check, and one whose role lost the read permission of a granted collection is told
|
||||
// `grants_changed`. The stream itself is proven by tests/live-conformance and e2e/notes-live.spec.ts.
|
||||
|
||||
import type { Principal } from "@conusai-kit/auth"
|
||||
import { createHostLive, createLiveGrants } from "@conusai-kit/host/postgres"
|
||||
import { createTenancyHooks, orgMemberships, TENANCY } from "@conusai-kit/tenancy"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { makeApp, type Person } from "../modules/account/__tests__/harness.js"
|
||||
|
||||
const app = makeApp()
|
||||
let principal: Principal | null = null
|
||||
const quiet = {
|
||||
log: { info: () => undefined, warn: () => undefined, error: () => undefined },
|
||||
captureException: () => undefined,
|
||||
span: <T>(_name: string, _attributes: unknown, fn: () => T): T => fn(),
|
||||
}
|
||||
|
||||
const { authorize } = createHostLive({
|
||||
runtime: () => ({ registry: app.registry, observe: quiet }),
|
||||
requestContext: async () => {
|
||||
const container = app.root.child()
|
||||
container.value(TENANCY, createTenancyHooks({ data: app.data, registry: app.registry }))
|
||||
return { principal, container }
|
||||
},
|
||||
log: () => undefined,
|
||||
grants: createLiveGrants(),
|
||||
})
|
||||
|
||||
const as = (person: Person | null) => {
|
||||
principal = person ? ({ actor: person.actor, method: "bearer" } as Principal) : null
|
||||
}
|
||||
const open = (org: string, body: { entities: string[]; topics: Array<{ ws: string; project?: string }> }) =>
|
||||
authorize(new Request(`http://app.test/api/v1/orgs/${org}/live`, { method: "POST" }), {
|
||||
protocol: "clp/1",
|
||||
after: null,
|
||||
...body,
|
||||
})
|
||||
|
||||
async function orgWithProject(person: Person, slug: string) {
|
||||
const call = person.call()
|
||||
const created = (await call.callByName("tenancy.org.create", { slug, name: slug })) as { orgId: string }
|
||||
await call.callByName("tenancy.workspace.create", { org: slug, slug: "main", name: "Main", visibility: "org" })
|
||||
await call.callByName("tenancy.project.create", {
|
||||
org: slug,
|
||||
workspace: "main",
|
||||
slug: "notes",
|
||||
name: "Notes",
|
||||
moduleKey: "notes",
|
||||
})
|
||||
return created.orgId
|
||||
}
|
||||
|
||||
describe("the live stream's authorization", async () => {
|
||||
const ada = await app.signUp("ada-live@example.test")
|
||||
const grace = await app.signUp("grace-live@example.test")
|
||||
const acme = await orgWithProject(ada, "acme-live")
|
||||
await orgWithProject(grace, "globex-live")
|
||||
const topic = { ws: "main", project: "notes" }
|
||||
|
||||
it("grants a member the collection for a topic they resolve, with its ids, and denies the rest", async () => {
|
||||
as(ada)
|
||||
const grant = await open("acme-live", { entities: ["notes.note", "notes.secret"], topics: [topic] })
|
||||
expect(grant.org).toBe(acme)
|
||||
expect(grant.granted.entities).toEqual(["notes.note"])
|
||||
expect(grant.granted.topics[0]?.ids.org).toBe(acme)
|
||||
expect(grant.granted.topics[0]?.ids.ws).toBeTruthy()
|
||||
expect(grant.granted.topics[0]?.ids.project).toBeTruthy()
|
||||
expect(grant.denied).toEqual([{ entity: "notes.secret", reason: "unknown" }])
|
||||
expect(await grant.recheck()).toBeNull()
|
||||
})
|
||||
|
||||
it("denies a project collection for the whole organization", async () => {
|
||||
as(ada)
|
||||
const grant = await open("acme-live", { entities: ["notes.note"], topics: [] })
|
||||
expect(grant.granted.entities).toEqual([])
|
||||
expect(grant.denied).toEqual([{ entity: "notes.note", reason: "scope" }])
|
||||
})
|
||||
|
||||
it("refuses a signed-out request, another organization's slug and a workspace the caller does not resolve", async () => {
|
||||
as(null)
|
||||
await expect(open("acme-live", { entities: ["notes.note"], topics: [topic] })).rejects.toMatchObject({
|
||||
status: 401,
|
||||
})
|
||||
as(grace)
|
||||
await expect(open("acme-live", { entities: ["notes.note"], topics: [topic] })).rejects.toMatchObject({
|
||||
status: 404,
|
||||
})
|
||||
as(ada)
|
||||
await expect(open("acme-live", { entities: ["notes.note"], topics: [{ ws: "nowhere" }] })).rejects.toMatchObject({
|
||||
status: 404,
|
||||
})
|
||||
await expect(open("globex-live", { entities: ["notes.note"], topics: [topic] })).rejects.toMatchObject({
|
||||
status: 404,
|
||||
})
|
||||
})
|
||||
|
||||
it("ends a stream with grants_changed once the member's role no longer reads a granted collection", async () => {
|
||||
const linus = await app.signUp("linus-live@example.test")
|
||||
const invitation = (await ada.call().callByName("tenancy.member.invite", {
|
||||
org: "acme-live",
|
||||
email: linus.email,
|
||||
role: "member",
|
||||
})) as { token: string }
|
||||
await linus.call().callByName("tenancy.invitation.accept", { org: "acme-live", token: invitation.token })
|
||||
as(linus)
|
||||
const grant = await open("acme-live", { entities: ["notes.note"], topics: [topic] })
|
||||
expect(grant.granted.entities).toEqual(["notes.note"])
|
||||
expect(await grant.recheck()).toBeNull()
|
||||
// A custom organization role that reads the organization and nothing of the notes module.
|
||||
const role = (await ada.call().callByName("tenancy.role.create", {
|
||||
org: "acme-live",
|
||||
name: "Directory only",
|
||||
level: "org",
|
||||
permissions: ["tenancy.org.read", "tenancy.workspace.read", "tenancy.project.read"],
|
||||
})) as { key: string }
|
||||
await ada.call().callByName("tenancy.member.assign", {
|
||||
org: "acme-live",
|
||||
userId: linus.actor.userId,
|
||||
level: "org",
|
||||
roleKey: role.key,
|
||||
})
|
||||
expect(await grant.recheck()).toBe("grants_changed")
|
||||
})
|
||||
|
||||
it("revokes a member who was removed, at the next re-check", async () => {
|
||||
as(ada)
|
||||
const grant = await open("acme-live", { entities: ["notes.note"], topics: [topic] })
|
||||
const scope = { level: "org" as const, orgId: acme, actor: ada.actor }
|
||||
const rows = (await app.data.forScope(scope).of(orgMemberships).list({})).items
|
||||
for (const row of rows) await app.data.forScope(scope).of(orgMemberships).delete(row.id)
|
||||
expect(await grant.recheck()).toBe("membership")
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user