fix(api): gate memory/providerConfig read behind Space owner authz
Non-owner Space members could read provider settings (mode, baseUrl, endpoint metadata) while connect/disconnect/setDefaultScope already required owner role. Align the read path with write authz. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
be9c310e1e
commit
c8b06ec7a8
@ -134,6 +134,15 @@ 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,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type { PrismaClient } from "@rakazo/db";
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import {
|
import {
|
||||||
disconnectMemoryProvider,
|
disconnectMemoryProvider,
|
||||||
|
getMemoryProviderConfig,
|
||||||
persistMemoryProviderConfig,
|
persistMemoryProviderConfig,
|
||||||
updateMemoryProviderDefaultScope,
|
updateMemoryProviderDefaultScope,
|
||||||
} from "./memory-provider-config.js";
|
} from "./memory-provider-config.js";
|
||||||
@ -366,6 +367,36 @@ describe("disconnectMemoryProvider", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getMemoryProviderConfig", () => {
|
||||||
|
it("rejects non-owners before reading provider configuration", async () => {
|
||||||
|
const { deps, findUnique } = makeDeps({
|
||||||
|
existing: { id: "cfg-1", secretId: "secret-existing" },
|
||||||
|
spaceOwner: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(getMemoryProviderConfig(deps, actor)).rejects.toMatchObject({ code: "FORBIDDEN" });
|
||||||
|
expect(findUnique).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns serialized config for Space owners", async () => {
|
||||||
|
const config = {
|
||||||
|
provider: "supermemory",
|
||||||
|
settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" },
|
||||||
|
defaultMemoryScope: "isolated",
|
||||||
|
updatedAt: new Date("2026-08-19T00:00:00.000Z"),
|
||||||
|
};
|
||||||
|
const { deps } = makeDeps({ existing: { id: "cfg-1", secretId: "secret-existing" } });
|
||||||
|
deps.prisma.spaceMemoryConfig.findUnique = vi.fn().mockResolvedValue(config);
|
||||||
|
|
||||||
|
await expect(getMemoryProviderConfig(deps, actor)).resolves.toEqual({
|
||||||
|
provider: "supermemory",
|
||||||
|
settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" },
|
||||||
|
defaultMemoryScope: "isolated",
|
||||||
|
updatedAt: "2026-08-19T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("updateMemoryProviderDefaultScope", () => {
|
describe("updateMemoryProviderDefaultScope", () => {
|
||||||
it("accepts owners with additional Better Auth roles", async () => {
|
it("accepts owners with additional Better Auth roles", async () => {
|
||||||
const { deps, update } = makeDeps({
|
const { deps, update } = makeDeps({
|
||||||
|
|||||||
@ -117,7 +117,6 @@ import {
|
|||||||
findDefaultModelCredential,
|
findDefaultModelCredential,
|
||||||
findDefaultVoiceCredential,
|
findDefaultVoiceCredential,
|
||||||
findModelCredential,
|
findModelCredential,
|
||||||
findSpaceMemoryConfig,
|
|
||||||
formatMessagingLinkCode,
|
formatMessagingLinkCode,
|
||||||
InvalidSpaceNameError,
|
InvalidSpaceNameError,
|
||||||
IsolationError,
|
IsolationError,
|
||||||
@ -155,8 +154,8 @@ import { searchIntegrationCatalog } from "./integration-catalog.js";
|
|||||||
import { buildMcpUpdateMaterial } from "./mcp-material.js";
|
import { buildMcpUpdateMaterial } from "./mcp-material.js";
|
||||||
import {
|
import {
|
||||||
disconnectMemoryProvider,
|
disconnectMemoryProvider,
|
||||||
|
getMemoryProviderConfig,
|
||||||
persistMemoryProviderConfig,
|
persistMemoryProviderConfig,
|
||||||
serializeSpaceMemoryConfig,
|
|
||||||
updateMemoryProviderDefaultScope,
|
updateMemoryProviderDefaultScope,
|
||||||
} from "./memory-provider-config.js";
|
} from "./memory-provider-config.js";
|
||||||
import {
|
import {
|
||||||
@ -2366,10 +2365,9 @@ export function createRouter(deps: RouterDeps) {
|
|||||||
});
|
});
|
||||||
return docs.map((d) => `# ${d.path}\n\n${d.content}`).join("\n\n");
|
return docs.map((d) => `# ${d.path}\n\n${d.content}`).join("\n\n");
|
||||||
}),
|
}),
|
||||||
providerConfig: authed.memory.providerConfig.handler(async ({ context }) => {
|
providerConfig: authed.memory.providerConfig.handler(async ({ context }) =>
|
||||||
const config = await findSpaceMemoryConfig(deps.prisma, context.actor.spaceId);
|
getMemoryProviderConfig(deps, context.actor),
|
||||||
return config ? serializeSpaceMemoryConfig(config) : null;
|
),
|
||||||
}),
|
|
||||||
connectProvider: authed.memory.connectProvider.handler(async ({ context, input }) =>
|
connectProvider: authed.memory.connectProvider.handler(async ({ context, input }) =>
|
||||||
persistMemoryProviderConfig(deps, context.actor, input),
|
persistMemoryProviderConfig(deps, context.actor, input),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -608,6 +608,65 @@ describeWithDatabase("API authorization and resource isolation", () => {
|
|||||||
).not.toBeNull();
|
).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps memory provider configuration readable only by Space owners", async () => {
|
||||||
|
const owner = await signup(app, `memory-owner-${stamp}@rakazo.test`, "Memory Owner");
|
||||||
|
const ownerActor = await rpc<Actor>(app, owner, "me");
|
||||||
|
const shared = await rpc<Space>(app, owner, "spaces/create", { name: "Memory 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, `memory-member-${stamp}@rakazo.test`, "Memory 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: `memory-org-member-${stamp}`,
|
||||||
|
organizationId: sharedRow.organizationId,
|
||||||
|
userId: memberActor.userId,
|
||||||
|
role: "member",
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await handles.prisma.spaceMember.create({
|
||||||
|
data: {
|
||||||
|
id: `memory-space-member-${stamp}`,
|
||||||
|
spaceId: shared.id,
|
||||||
|
organizationId: sharedRow.organizationId,
|
||||||
|
userId: memberActor.userId,
|
||||||
|
role: "member",
|
||||||
|
createdAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await rpc(app, owner, "memory/providerConfig", {}, shared.id)).toMatchObject({
|
||||||
|
provider: "supermemory",
|
||||||
|
settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" },
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
raw(app, member, "memory/providerConfig", {}, shared.id),
|
||||||
|
).resolves.toMatchObject({ status: 403 });
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps space data and computers behind the selected space boundary", async () => {
|
it("keeps space data and computers behind the selected space boundary", async () => {
|
||||||
const cookie = await signup(app, `spaces-${stamp}@rakazo.test`, "Space Owner");
|
const cookie = await signup(app, `spaces-${stamp}@rakazo.test`, "Space Owner");
|
||||||
const original = await rpc<Actor>(app, cookie, "me");
|
const original = await rpc<Actor>(app, cookie, "me");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user