From a16dccc1c026898af0a20c7fd73a0eb901803334 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Apr 2026 11:07:32 +0000 Subject: [PATCH 1/2] fix: prevent shared users from deleting or renaming collections Co-authored-by: Vitalii Melnychuk --- src/server/access/collections.test.ts | 14 ++++++++++++++ src/server/access/collections.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 src/server/access/collections.test.ts diff --git a/src/server/access/collections.test.ts b/src/server/access/collections.test.ts new file mode 100644 index 0000000..a76cda5 --- /dev/null +++ b/src/server/access/collections.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { canRenameOrDeleteCollection } from "./collections"; + +describe("canRenameOrDeleteCollection", () => { + it("allows owners and creators", () => { + expect(canRenameOrDeleteCollection({ kind: "owner" })).toBe(true); + expect(canRenameOrDeleteCollection({ kind: "creator" })).toBe(true); + }); + + it("denies grants and non-members", () => { + expect(canRenameOrDeleteCollection({ kind: "grant" })).toBe(false); + expect(canRenameOrDeleteCollection({ kind: "none" })).toBe(false); + }); +}); diff --git a/src/server/access/collections.ts b/src/server/access/collections.ts index 641c425..112d116 100644 --- a/src/server/access/collections.ts +++ b/src/server/access/collections.ts @@ -13,7 +13,7 @@ export type CollectionAccessState = export function canRenameOrDeleteCollection( state: CollectionAccessState, ): boolean { - return state.kind !== "none"; + return state.kind === "owner" || state.kind === "creator"; } export async function loadCollectionAccessState(params: { From c20ce08d02a5a510373a40c0cb48f06a092676e8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 2 Apr 2026 11:09:09 +0000 Subject: [PATCH 2/2] test: cover collection destructive permission checks --- src/server/access/collections.test.ts | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/server/access/collections.test.ts b/src/server/access/collections.test.ts index a76cda5..14d69b3 100644 --- a/src/server/access/collections.test.ts +++ b/src/server/access/collections.test.ts @@ -1,13 +1,26 @@ -import { describe, expect, it } from "vitest"; -import { canRenameOrDeleteCollection } from "./collections"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; + +const prevDatabaseUrl = process.env.DATABASE_URL; + +beforeAll(() => { + process.env.DATABASE_URL = + process.env.DATABASE_URL ?? "postgresql://user:pass@localhost:5432/test"; +}); + +afterAll(() => { + if (prevDatabaseUrl === undefined) delete process.env.DATABASE_URL; + else process.env.DATABASE_URL = prevDatabaseUrl; +}); describe("canRenameOrDeleteCollection", () => { - it("allows owners and creators", () => { + it("allows owners and creators", async () => { + const { canRenameOrDeleteCollection } = await import("./collections"); expect(canRenameOrDeleteCollection({ kind: "owner" })).toBe(true); expect(canRenameOrDeleteCollection({ kind: "creator" })).toBe(true); }); - it("denies grants and non-members", () => { + it("denies grants and non-members", async () => { + const { canRenameOrDeleteCollection } = await import("./collections"); expect(canRenameOrDeleteCollection({ kind: "grant" })).toBe(false); expect(canRenameOrDeleteCollection({ kind: "none" })).toBe(false); });