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>;
|
) => 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({
|
const member = await prisma.spaceMember.findUnique({
|
||||||
where: { spaceId_userId: { spaceId: actor.spaceId, userId: actor.userId } },
|
where: { spaceId_userId: { spaceId: actor.spaceId, userId: actor.userId } },
|
||||||
select: { role: true },
|
select: { role: true },
|
||||||
});
|
});
|
||||||
const roles = member?.role.split(",").map((role) => role.trim());
|
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(
|
export async function persistMemoryProviderConfig(
|
||||||
@ -134,15 +138,6 @@ export async function persistMemoryProviderConfig(
|
|||||||
return serializeSpaceMemoryConfig(config);
|
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(
|
export async function updateMemoryProviderDefaultScope(
|
||||||
deps: MemoryProviderConfigDeps,
|
deps: MemoryProviderConfigDeps,
|
||||||
actor: Actor,
|
actor: Actor,
|
||||||
|
|||||||
@ -1,11 +1,10 @@
|
|||||||
import { mkdtempSync, rmSync } from "node:fs";
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { ComposioEmulator, LocalAgentHomeStore, teamBotWorkspaceDirectory } from "@rakazo/adapters";
|
import { ComposioEmulator } from "@rakazo/adapters";
|
||||||
import type { appContract, Space, SpaceNavigation } from "@rakazo/contracts";
|
import type { AiConsentStatus, appContract, Space, SpaceNavigation } from "@rakazo/contracts";
|
||||||
import {
|
import {
|
||||||
claimEmptySpaceDeletionForMember,
|
claimEmptySpaceDeletionForMember,
|
||||||
computerHomeKey,
|
|
||||||
deleteEmptySpaceForMember,
|
deleteEmptySpaceForMember,
|
||||||
releaseSpaceDeletionClaim,
|
releaseSpaceDeletionClaim,
|
||||||
renewSpaceDeletionClaim,
|
renewSpaceDeletionClaim,
|
||||||
@ -469,77 +468,6 @@ describeWithDatabase("API authorization and resource isolation", () => {
|
|||||||
expect(await handles.prisma.bot.findUnique({ where: { id: ownerBot.id } })).not.toBeNull();
|
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 () => {
|
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 owner = await signup(app, `approval-owner-${stamp}@rakazo.test`, "Approval Owner");
|
||||||
const member = await signup(app, `approval-member-${stamp}@rakazo.test`, "Approval Member");
|
const member = await signup(app, `approval-member-${stamp}@rakazo.test`, "Approval Member");
|
||||||
@ -581,21 +509,39 @@ describeWithDatabase("API authorization and resource isolation", () => {
|
|||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps routines private to each user in a shared Space", async () => {
|
it("redacts memory provider details in aiConsent/status for non-owner members", async () => {
|
||||||
const owner = await signup(app, `routine-owner-${stamp}@rakazo.test`, "Routine Owner");
|
const owner = await signup(app, `ai-consent-owner-${stamp}@rakazo.test`, "Consent Owner");
|
||||||
const member = await signup(app, `routine-member-${stamp}@rakazo.test`, "Routine 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: "Consent shared" });
|
||||||
|
|
||||||
const shared = await rpc<Space>(app, owner, "spaces/create", { name: "Shared routines" });
|
|
||||||
const sharedRow = await handles.prisma.space.findUniqueOrThrow({
|
const sharedRow = await handles.prisma.space.findUniqueOrThrow({
|
||||||
where: { id: shared.id },
|
where: { id: shared.id },
|
||||||
select: { organizationId: true },
|
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.deleteMany({ where: { userId: memberActor.userId } });
|
||||||
await handles.prisma.member.create({
|
await handles.prisma.member.create({
|
||||||
data: {
|
data: {
|
||||||
id: `routine-member-org-${stamp}`,
|
id: `ai-consent-org-member-${stamp}`,
|
||||||
organizationId: sharedRow.organizationId,
|
organizationId: sharedRow.organizationId,
|
||||||
userId: memberActor.userId,
|
userId: memberActor.userId,
|
||||||
role: "member",
|
role: "member",
|
||||||
@ -604,7 +550,7 @@ describeWithDatabase("API authorization and resource isolation", () => {
|
|||||||
});
|
});
|
||||||
await handles.prisma.spaceMember.create({
|
await handles.prisma.spaceMember.create({
|
||||||
data: {
|
data: {
|
||||||
id: `routine-member-space-${stamp}`,
|
id: `ai-consent-space-member-${stamp}`,
|
||||||
spaceId: shared.id,
|
spaceId: shared.id,
|
||||||
organizationId: sharedRow.organizationId,
|
organizationId: sharedRow.organizationId,
|
||||||
userId: memberActor.userId,
|
userId: memberActor.userId,
|
||||||
@ -613,31 +559,26 @@ describeWithDatabase("API authorization and resource isolation", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const ownerBot = await rpc<Bot>(
|
const ownerStatus = await rpc<AiConsentStatus>(
|
||||||
app,
|
app,
|
||||||
owner,
|
owner,
|
||||||
"bots/create",
|
"aiConsent/status",
|
||||||
botInput("Owner shared bot"),
|
{ uses: ["memory"] },
|
||||||
shared.id,
|
shared.id,
|
||||||
);
|
);
|
||||||
const ownerRoutine = await rpc<{ id: string }>(
|
const memberStatus = await rpc<AiConsentStatus>(
|
||||||
app,
|
|
||||||
owner,
|
|
||||||
"routines/create",
|
|
||||||
routineInput(ownerBot.id),
|
|
||||||
shared.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
await expectDenied(
|
|
||||||
app,
|
app,
|
||||||
member,
|
member,
|
||||||
"routines/remove",
|
"aiConsent/status",
|
||||||
{ routineId: ownerRoutine.id },
|
{ uses: ["memory"] },
|
||||||
shared.id,
|
shared.id,
|
||||||
);
|
);
|
||||||
expect(
|
const ownerMemory = ownerStatus.recipients.find((recipient) => recipient.use === "memory");
|
||||||
await handles.prisma.routine.findUnique({ where: { id: ownerRoutine.id } }),
|
const memberMemory = memberStatus.recipients.find((recipient) => recipient.use === "memory");
|
||||||
).not.toBeNull();
|
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 () => {
|
it("keeps space data and computers behind the selected space boundary", async () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user