// SPDX-License-Identifier: LicenseRef-ConusAI-Template-1.0 // The web process's tenancy rules around a request's caller (apps/web/src/server/tenancy-calls.ts, // ADR-062), over the app's real registry and kit-tenancy's procedures: // // - an invitation, once committed, admits its address through @conusai-kit/auth's real platform client // against the fake admission endpoint of @conusai-kit/testing, audited with ids only // - with no admission credential the invitation still answers, and the skip is logged with its reason // - a refused or unreachable admission is logged and audited as a failure, never thrown to the inviter // - an invitation that is refused (a role above the inviter's) asks for no admission at all // - with self-service off, only the app-access role `admin` founds an organization; a token without the // claim is not `admin` // - on the Convex profile a grant-changing write is announced to the organization's streams here import { createPlatformIdentityAdmission } from "@conusai-kit/auth" import { Container } from "@conusai-kit/core" import { GRANTS_CHANGED } from "@conusai-kit/tenancy" import { fakeIdentityAdmission } from "@conusai-kit/testing" import { describe, expect, it, vi } from "vitest" import { type TenancyCallOptions, withTenancyCalls } from "../apps/web/src/server/tenancy-calls.js" import { identityLinkOf, organizationSelfServiceOf } from "../config/src/access.js" import { makeApp, type Person } from "../modules/account/__tests__/harness.js" const app = makeApp() type Audited = Parameters[1] function wrapped(person: Person, overrides: Partial = {}) { const audited: Audited[] = [] const log = { info: vi.fn(), warn: vi.fn() } const caller = withTenancyCalls(person.call(), { principal: { actor: person.actor }, container: new Container(), selfServiceOrganizations: true, admission: () => ({ skipped: "no OIDC_ADMISSION_TOKEN" }), audit: async (_action, event) => { audited.push(event) }, log, ...overrides, }) return { caller, audited, log } } describe("an invitation's admission (ADR-062 decision 4)", async () => { const ada = await app.signUp("ada-admission@example.test") await ada.call().callByName("tenancy.org.create", { slug: "acme-admission", name: "Acme" }) it("admits the invited address once the invitation committed, and audits ids only", async () => { const platform = fakeIdentityAdmission() const admission = createPlatformIdentityAdmission({ apiUrl: platform.apiUrl, linkId: platform.linkId, token: platform.token, fetch: platform.fetch, }) const { caller, audited, log } = wrapped(ada, { admission: () => admission }) const invitation = (await caller.callByName("tenancy.member.invite", { org: "acme-admission", email: " New.Person@Example.test ", role: "member", })) as { invitationId: string; token: string } expect(invitation.token.length).toBeGreaterThan(16) expect(platform.admissions).toEqual([ expect.objectContaining({ email: "new.person@example.test", invitationId: invitation.invitationId }), ]) expect(platform.calls[0]).toMatchObject({ method: "POST", credential: "valid", status: 202 }) expect(audited).toEqual([{ orgSlug: "acme-admission", invitationId: invitation.invitationId, outcome: "success" }]) // Neither the log nor the audit event carries the address. expect(JSON.stringify([log.info.mock.calls, log.warn.mock.calls, audited])).not.toContain("example.test") }) it("answers the invitation without an admission credential, and logs why it skipped", async () => { const { caller, audited, log } = wrapped(ada) const invitation = (await caller.callByName("tenancy.member.invite", { org: "acme-admission", email: "skipped@example.test", role: "viewer", })) as { invitationId: string } expect(invitation.invitationId).toBeTruthy() expect(audited).toEqual([]) expect(log.info).toHaveBeenCalledWith("invitation: admission skipped: no OIDC_ADMISSION_TOKEN", { "conusai.invitation_id": invitation.invitationId, }) }) it("answers the invitation when the admission client cannot be built", async () => { const { caller, log } = wrapped(ada, { admission: () => { throw new Error("set OIDC_AUDIENCE") }, }) const invitation = (await caller.callByName("tenancy.member.invite", { org: "acme-admission", email: "unbuilt@example.test", role: "viewer", })) as { invitationId: string } expect(log.info).toHaveBeenCalledWith( "invitation: admission skipped: the admission client could not be built (Error)", { "conusai.invitation_id": invitation.invitationId, }, ) }) it.each([ [429, "rate_limited"], [401, "unauthorized"], [503, "unavailable"], ["network", "unavailable"], ] as const)( "keeps the invitation when the platform answers %s, logged and audited as %s", async (refusal, reason) => { const platform = fakeIdentityAdmission() platform.refuseNext(refusal) const admission = createPlatformIdentityAdmission({ apiUrl: platform.apiUrl, linkId: platform.linkId, token: platform.token, fetch: platform.fetch, }) const { caller, audited, log } = wrapped(ada, { admission: () => admission }) const email = `refused-${String(refusal)}@example.test` const invitation = (await caller.callByName("tenancy.member.invite", { org: "acme-admission", email, role: "viewer", })) as { invitationId: string } expect(invitation.invitationId).toBeTruthy() expect(platform.admissions).toEqual([]) expect(log.warn).toHaveBeenCalledWith(`invitation: admission failed: ${reason}`, { "conusai.invitation_id": invitation.invitationId, }) expect(audited).toEqual([ { orgSlug: "acme-admission", invitationId: invitation.invitationId, outcome: "failure", reason }, ]) }, ) it("asks for no admission when the invitation itself is refused", async () => { const grace = await app.signUp("grace-admission@example.test") const joined = (await ada.call().callByName("tenancy.member.invite", { org: "acme-admission", email: grace.email, role: "admin", })) as { token: string } await grace.call().callByName("tenancy.invitation.accept", { org: "acme-admission", token: joined.token }) const platform = fakeIdentityAdmission() const admission = createPlatformIdentityAdmission({ apiUrl: platform.apiUrl, linkId: platform.linkId, token: platform.token, fetch: platform.fetch, }) const { caller, audited } = wrapped(grace, { admission: () => admission }) // An administrator cannot invite an owner (kit-tenancy's ceiling): nothing committed, nothing admitted. await expect( caller.callByName("tenancy.member.invite", { org: "acme-admission", email: "owner@example.test", role: "owner" }), ).rejects.toMatchObject({ status: 403 }) expect(platform.calls).toEqual([]) expect(audited).toEqual([]) }) }) describe("founding an organization when self-service is off (ADR-062 decision 5)", async () => { const linus = await app.signUp("linus-founder@example.test") it("refuses a person without the app-access role, and one whose token carries no claim", async () => { for (const principal of [{ actor: linus.actor }, { actor: linus.actor, appAccess: { role: "user" as const } }]) { const { caller } = wrapped(linus, { selfServiceOrganizations: false, principal }) await expect( caller.callByName("tenancy.org.create", { slug: "refused-founder", name: "Refused" }), ).rejects.toMatchObject({ status: 403, code: "forbidden" }) } // Nothing was written: the slug is still free for the operator below. expect( ((await linus.call().callByName("tenancy.me.get", undefined)) as { organizations: unknown[] }).organizations, ).toEqual([]) }) it("lets the app-access role admin found one, and anyone when self-service is on", async () => { const admin = wrapped(linus, { selfServiceOrganizations: false, principal: { actor: linus.actor, appAccess: { role: "admin" } }, }) await expect( admin.caller.callByName("tenancy.org.create", { slug: "refused-founder", name: "Ops" }), ).resolves.toMatchObject({ slug: "refused-founder", }) const open = wrapped(linus, { selfServiceOrganizations: true }) await expect( open.caller.callByName("tenancy.org.create", { slug: "self-founded", name: "Self" }), ).resolves.toMatchObject({ slug: "self-founded", }) }) }) describe("a grant change made in the Convex deployment", async () => { const ada = await app.signUp("ada-remote-grants@example.test") await ada.call().callByName("tenancy.org.create", { slug: "acme-remote", name: "Acme" }) const grace = await app.signUp("grace-remote-grants@example.test") const invitation = (await ada.call().callByName("tenancy.member.invite", { org: "acme-remote", email: grace.email, role: "member", })) as { token: string } await grace.call().callByName("tenancy.invitation.accept", { org: "acme-remote", token: invitation.token }) it("is announced to this process's streams of the organization, and only of a streamed one", async () => { const container = new Container() const announced: string[] = [] container.value(GRANTS_CHANGED, { grantsChanged: async (orgId) => void announced.push(orgId) }) const orgIdOf = (slug: string) => (slug === "acme-remote" ? "org-id-of-acme" : undefined) const { caller } = wrapped(ada, { container, remoteGrants: { orgIdOf } }) await caller.callByName("tenancy.member.change-role", { org: "acme-remote", userId: grace.actor.userId, role: "viewer", }) expect(announced).toEqual(["org-id-of-acme"]) // A read is no grant change. await caller.callByName("tenancy.member.list", { org: "acme-remote" }) expect(announced).toEqual(["org-id-of-acme"]) }) }) describe("the access settings of the deployment", () => { it("reads APP_ORGANIZATION_SELF_SERVICE as true by default and refuses anything but true or false", () => { expect(organizationSelfServiceOf({})).toBe(true) expect(organizationSelfServiceOf({ APP_ORGANIZATION_SELF_SERVICE: "" })).toBe(true) expect(organizationSelfServiceOf({ APP_ORGANIZATION_SELF_SERVICE: "false" })).toBe(false) expect(() => organizationSelfServiceOf({ APP_ORGANIZATION_SELF_SERVICE: "no" })).toThrow( "set APP_ORGANIZATION_SELF_SERVICE to true or false", ) }) it("takes the identity link with its admission credential, skips without both, and refuses one of the two", () => { expect(identityLinkOf({})).toEqual({ skipped: "no OIDC_ADMISSION_TOKEN: the identity link issued no admission credential to this app", }) expect(identityLinkOf({ OIDC_LINK_ID: "4f1c2a7e-6b1d", OIDC_ADMISSION_TOKEN: "cai_adm_x" })).toEqual({ linkId: "4f1c2a7e-6b1d", }) for (const env of [{ OIDC_LINK_ID: "4f1c" }, { OIDC_ADMISSION_TOKEN: "cai_adm_secret" }]) { expect(() => identityLinkOf(env)).toThrow("set OIDC_LINK_ID and OIDC_ADMISSION_TOKEN together") } // The credential is never quoted back. expect(() => identityLinkOf({ OIDC_ADMISSION_TOKEN: "cai_adm_secret" })).not.toThrow(/cai_adm_secret/) expect(() => identityLinkOf({ OIDC_LINK_ID: "../other", OIDC_ADMISSION_TOKEN: "t" })).toThrow( "set OIDC_LINK_ID to the identity link's id", ) }) })