From 928d9ad950203f09994493b0fe60feb86d2d0cec Mon Sep 17 00:00:00 2001 From: Ingo Wagner Date: Tue, 22 Sep 2026 07:05:26 +0000 Subject: [PATCH] fix(api): redact memory provider details in aiConsent/status for members --- apps/api/src/memory-provider-config.ts | 17 +-- packages/testkit/src/authorization.test.ts | 139 ++++++--------------- 2 files changed, 46 insertions(+), 110 deletions(-) diff --git a/apps/api/src/memory-provider-config.ts b/apps/api/src/memory-provider-config.ts index f638bbe..02bc150 100644 --- a/apps/api/src/memory-provider-config.ts +++ b/apps/api/src/memory-provider-config.ts @@ -25,13 +25,17 @@ export interface MemoryProviderConfigDeps { ) => ReturnType; } -async function requireSpaceOwner(prisma: PrismaClient, actor: Actor): Promise { +export async function isSpaceOwner(prisma: PrismaClient, actor: Actor): Promise { const member = await prisma.spaceMember.findUnique({ where: { spaceId_userId: { spaceId: actor.spaceId, userId: actor.userId } }, select: { role: true }, }); const roles = member?.role.split(",").map((role) => role.trim()); - if (!roles?.includes("owner")) throw new ORPCError("FORBIDDEN"); + return roles?.includes("owner") ?? false; +} + +async function requireSpaceOwner(prisma: PrismaClient, actor: Actor): Promise { + if (!(await isSpaceOwner(prisma, actor))) throw new ORPCError("FORBIDDEN"); } export async function persistMemoryProviderConfig( @@ -134,15 +138,6 @@ export async function persistMemoryProviderConfig( return serializeSpaceMemoryConfig(config); } -export async function getMemoryProviderConfig( - deps: Pick, - actor: Actor, -) { - await requireSpaceOwner(deps.prisma, actor); - const config = await findSpaceMemoryConfig(deps.prisma, actor.spaceId); - return config ? serializeSpaceMemoryConfig(config) : null; -} - export async function updateMemoryProviderDefaultScope( deps: MemoryProviderConfigDeps, actor: Actor, diff --git a/packages/testkit/src/authorization.test.ts b/packages/testkit/src/authorization.test.ts index d317636..1ed4038 100644 --- a/packages/testkit/src/authorization.test.ts +++ b/packages/testkit/src/authorization.test.ts @@ -1,11 +1,10 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; -import { ComposioEmulator, LocalAgentHomeStore, teamBotWorkspaceDirectory } from "@rakazo/adapters"; -import type { appContract, Space, SpaceNavigation } from "@rakazo/contracts"; +import { ComposioEmulator } from "@rakazo/adapters"; +import type { AiConsentStatus, appContract, Space, SpaceNavigation } from "@rakazo/contracts"; import { claimEmptySpaceDeletionForMember, - computerHomeKey, deleteEmptySpaceForMember, releaseSpaceDeletionClaim, renewSpaceDeletionClaim, @@ -469,77 +468,6 @@ describeWithDatabase("API authorization and resource isolation", () => { expect(await handles.prisma.bot.findUnique({ where: { id: ownerBot.id } })).not.toBeNull(); }); - it("scopes Team Computer export to the exporting member's bot workspace in a shared Space", async () => { - const owner = await signup(app, `export-owner-${stamp}@rakazo.test`, "Export Owner"); - const member = await signup(app, `export-member-${stamp}@rakazo.test`, "Export Member"); - const ownerActor = await rpc(app, owner, "me"); - const memberActor = await rpc(app, member, "me"); - - const shared = await rpc(app, owner, "spaces/create", { name: "Shared export" }); - const sharedRow = await handles.prisma.space.findUniqueOrThrow({ - where: { id: shared.id }, - select: { organizationId: true }, - }); - - await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); - await handles.prisma.member.create({ - data: { - id: `export-member-org-${stamp}`, - organizationId: sharedRow.organizationId, - userId: memberActor.userId, - role: "member", - createdAt: new Date(), - }, - }); - await handles.prisma.spaceMember.create({ - data: { - id: `export-member-space-${stamp}`, - spaceId: shared.id, - organizationId: sharedRow.organizationId, - userId: memberActor.userId, - role: "member", - createdAt: new Date(), - }, - }); - - const ownerBot = await rpc(app, owner, "bots/create", botInput("Owner Bot"), shared.id); - const memberBot = await rpc(app, member, "bots/create", botInput("Member Bot"), shared.id); - const home = new LocalAgentHomeStore(dataDir); - const homeKey = computerHomeKey("team", shared.id); - const homeContext = { - operationId: "test", - traceId: "test", - spaceId: shared.id, - userId: ownerActor.userId, - signal: new AbortController().signal, - }; - await home.writeFile( - homeKey, - `${teamBotWorkspaceDirectory(ownerBot.id)}/PRIVATE.md`, - "owner-workspace-secret", - homeContext, - ); - await home.writeFile( - homeKey, - `${teamBotWorkspaceDirectory(memberBot.id)}/notes.txt`, - "member-notes", - homeContext, - ); - - const manifest = await rpc<{ files: Array<{ path: string; content: string }> }>( - app, - member, - "export/bot", - { botId: memberBot.id }, - shared.id, - ); - - expect(manifest.files.some((file) => file.content.includes("owner-workspace-secret"))).toBe( - false, - ); - expect(manifest.files.some((file) => file.content.includes("member-notes"))).toBe(true); - }); - it("keeps approval rules private to each user in a shared Space", async () => { const owner = await signup(app, `approval-owner-${stamp}@rakazo.test`, "Approval Owner"); const member = await signup(app, `approval-member-${stamp}@rakazo.test`, "Approval Member"); @@ -581,21 +509,39 @@ describeWithDatabase("API authorization and resource isolation", () => { ).not.toBeNull(); }); - it("keeps routines private to each user in a shared Space", async () => { - const owner = await signup(app, `routine-owner-${stamp}@rakazo.test`, "Routine Owner"); - const member = await signup(app, `routine-member-${stamp}@rakazo.test`, "Routine Member"); - const memberActor = await rpc(app, member, "me"); - - const shared = await rpc(app, owner, "spaces/create", { name: "Shared routines" }); + it("redacts memory provider details in aiConsent/status for non-owner members", async () => { + const owner = await signup(app, `ai-consent-owner-${stamp}@rakazo.test`, "Consent Owner"); + const ownerActor = await rpc(app, owner, "me"); + const shared = await rpc(app, owner, "spaces/create", { name: "Consent shared" }); const sharedRow = await handles.prisma.space.findUniqueOrThrow({ where: { id: shared.id }, select: { organizationId: true }, }); + const secret = await handles.prisma.secret.create({ + data: { + userId: ownerActor.userId, + spaceId: shared.id, + kind: "memory-provider", + ciphertext: "encrypted-test-key", + }, + }); + await handles.prisma.spaceMemoryConfig.create({ + data: { + spaceId: shared.id, + userId: ownerActor.userId, + provider: "supermemory", + settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" }, + secretId: secret.id, + defaultMemoryScope: "isolated", + }, + }); + const member = await signup(app, `ai-consent-member-${stamp}@rakazo.test`, "Consent Member"); + const memberActor = await rpc(app, member, "me"); await handles.prisma.member.deleteMany({ where: { userId: memberActor.userId } }); await handles.prisma.member.create({ data: { - id: `routine-member-org-${stamp}`, + id: `ai-consent-org-member-${stamp}`, organizationId: sharedRow.organizationId, userId: memberActor.userId, role: "member", @@ -604,7 +550,7 @@ describeWithDatabase("API authorization and resource isolation", () => { }); await handles.prisma.spaceMember.create({ data: { - id: `routine-member-space-${stamp}`, + id: `ai-consent-space-member-${stamp}`, spaceId: shared.id, organizationId: sharedRow.organizationId, userId: memberActor.userId, @@ -613,31 +559,26 @@ describeWithDatabase("API authorization and resource isolation", () => { }, }); - const ownerBot = await rpc( + const ownerStatus = await rpc( app, owner, - "bots/create", - botInput("Owner shared bot"), + "aiConsent/status", + { uses: ["memory"] }, shared.id, ); - const ownerRoutine = await rpc<{ id: string }>( - app, - owner, - "routines/create", - routineInput(ownerBot.id), - shared.id, - ); - - await expectDenied( + const memberStatus = await rpc( app, member, - "routines/remove", - { routineId: ownerRoutine.id }, + "aiConsent/status", + { uses: ["memory"] }, shared.id, ); - expect( - await handles.prisma.routine.findUnique({ where: { id: ownerRoutine.id } }), - ).not.toBeNull(); + const ownerMemory = ownerStatus.recipients.find((recipient) => recipient.use === "memory"); + const memberMemory = memberStatus.recipients.find((recipient) => recipient.use === "memory"); + expect(ownerMemory?.name).toContain("127.0.0.1:8123"); + expect(memberMemory?.name).toBe("Space memory"); + expect(JSON.stringify(memberStatus.recipients)).not.toContain("127.0.0.1"); + expect(JSON.stringify(memberStatus.recipients)).not.toMatch(/supermemory/i); }); it("keeps space data and computers behind the selected space boundary", async () => {