fix(api): scope routine remove to acting user in shared Spaces (#61)
* fix(api): scope routine remove to acting user in shared Spaces routines/remove only filtered by spaceId, so any member of a shared Space could delete another member's routine by ID. Match update/testRun by also requiring userId in the lookup. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * test: drop unused ownerActor in routine remove authz case Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
parent
492e0f0965
commit
db39fe7752
@ -2431,7 +2431,11 @@ export function createRouter(deps: RouterDeps) {
|
||||
}),
|
||||
remove: authed.routines.remove.handler(async ({ context, input }) => {
|
||||
const existing = await deps.prisma.routine.findFirst({
|
||||
where: { id: input.routineId, spaceId: context.actor.spaceId },
|
||||
where: {
|
||||
id: input.routineId,
|
||||
spaceId: context.actor.spaceId,
|
||||
userId: context.actor.userId,
|
||||
},
|
||||
});
|
||||
if (!existing) throw new IsolationError();
|
||||
await deps.prisma.routine.delete({ where: { id: existing.id } });
|
||||
|
||||
18
apps/api/src/routine-remove-authz.test.ts
Normal file
18
apps/api/src/routine-remove-authz.test.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("routine remove authorization wiring", () => {
|
||||
it("scopes remove lookups to the acting user like update and testRun", () => {
|
||||
const routerSource = readFileSync(
|
||||
resolve(dirname(fileURLToPath(import.meta.url)), "router.ts"),
|
||||
"utf8",
|
||||
);
|
||||
const removeHandler = routerSource.match(
|
||||
/remove: authed\.routines\.remove\.handler\(async \(\{ context, input \}\) => \{[\s\S]*?\}\),/,
|
||||
)?.[0];
|
||||
expect(removeHandler).toBeTruthy();
|
||||
expect(removeHandler).toContain("userId: context.actor.userId");
|
||||
});
|
||||
});
|
||||
@ -128,7 +128,10 @@ describeWithDatabase("API authorization and resource isolation", () => {
|
||||
["memory/update", { documentId: "missing-memory", content: "Nope" }],
|
||||
["memory/exportMarkdown", {}],
|
||||
["memory/providerConfig"],
|
||||
["memory/connectProvider", { provider: "test", settings: {}, credentials: {}, defaultMemoryScope: "isolated" }],
|
||||
[
|
||||
"memory/connectProvider",
|
||||
{ provider: "test", settings: {}, credentials: {}, defaultMemoryScope: "isolated" },
|
||||
],
|
||||
["memory/setDefaultScope", { defaultMemoryScope: "shared" }],
|
||||
["memory/disconnectProvider"],
|
||||
["routines/list", { botId: "missing-bot" }],
|
||||
@ -545,6 +548,53 @@ describeWithDatabase("API authorization and resource isolation", () => {
|
||||
expect(computer.controlBotId).toBe(ownerBot.id);
|
||||
});
|
||||
|
||||
it("prevents one shared-space member from deleting another member's routine", 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" });
|
||||
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: `routine-member-org-${stamp}`,
|
||||
organizationId: sharedRow.organizationId,
|
||||
userId: memberActor.userId,
|
||||
role: "member",
|
||||
createdAt: new Date(),
|
||||
},
|
||||
});
|
||||
await handles.prisma.spaceMember.create({
|
||||
data: {
|
||||
id: `routine-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 ownerRoutine = await rpc<{ id: string }>(
|
||||
app,
|
||||
owner,
|
||||
"routines/create",
|
||||
routineInput(ownerBot.id),
|
||||
shared.id,
|
||||
);
|
||||
|
||||
await expectDenied(app, member, "routines/remove", { routineId: ownerRoutine.id }, shared.id);
|
||||
expect(
|
||||
await handles.prisma.routine.findUnique({ where: { id: ownerRoutine.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");
|
||||
@ -910,13 +960,25 @@ describeWithDatabase("API authorization and resource isolation", () => {
|
||||
});
|
||||
|
||||
await expectForbidden(app, member, "memory/providerConfig", {}, shared.id);
|
||||
await expectForbidden(app, member, "memory/connectProvider", {
|
||||
provider: "supermemory",
|
||||
settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" },
|
||||
credentials: { apiKey: "not-a-real-key" },
|
||||
defaultMemoryScope: "isolated",
|
||||
}, shared.id);
|
||||
await expectForbidden(app, member, "memory/setDefaultScope", { defaultMemoryScope: "shared" }, shared.id);
|
||||
await expectForbidden(
|
||||
app,
|
||||
member,
|
||||
"memory/connectProvider",
|
||||
{
|
||||
provider: "supermemory",
|
||||
settings: { mode: "local", baseUrl: "http://127.0.0.1:8123" },
|
||||
credentials: { apiKey: "not-a-real-key" },
|
||||
defaultMemoryScope: "isolated",
|
||||
},
|
||||
shared.id,
|
||||
);
|
||||
await expectForbidden(
|
||||
app,
|
||||
member,
|
||||
"memory/setDefaultScope",
|
||||
{ defaultMemoryScope: "shared" },
|
||||
shared.id,
|
||||
);
|
||||
await expectForbidden(app, member, "memory/disconnectProvider", {}, shared.id);
|
||||
|
||||
expect(await rpc(app, owner, "memory/providerConfig", {}, shared.id)).toEqual({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user