fix(api): redact memory provider details in aiConsent/status for members
This commit is contained in:
parent
c8c6d9e042
commit
928d9ad950
@ -25,13 +25,17 @@ export interface MemoryProviderConfigDeps {
|
||||
) => ReturnType<typeof prepareMemoryProviderConnection>;
|
||||
}
|
||||
|
||||
async function requireSpaceOwner(prisma: PrismaClient, actor: Actor): Promise<void> {
|
||||
export async function isSpaceOwner(prisma: PrismaClient, actor: Actor): Promise<boolean> {
|
||||
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<void> {
|
||||
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<MemoryProviderConfigDeps, "prisma">,
|
||||
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,
|
||||
|
||||
@ -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<Actor>(app, owner, "me");
|
||||
const memberActor = await rpc<Actor>(app, member, "me");
|
||||
|
||||
const shared = await rpc<Space>(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<Bot>(app, owner, "bots/create", botInput("Owner Bot"), shared.id);
|
||||
const memberBot = await rpc<Bot>(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<Actor>(app, member, "me");
|
||||
|
||||
const shared = await rpc<Space>(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<Actor>(app, owner, "me");
|
||||
const shared = await rpc<Space>(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<Actor>(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<Bot>(
|
||||
const ownerStatus = await rpc<AiConsentStatus>(
|
||||
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<AiConsentStatus>(
|
||||
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 () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user