From 00cc53769cda8de2b3505071365c6a1034147f8d Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Mon, 5 Oct 2026 22:12:12 -0400 Subject: [PATCH] ENG_2359 Speed up my_contents and the other access views to prevent plugin timeouts --- .../features/contentViewAccess.feature | 256 ++++++++++++++++++ .../features/step-definitions/stepdefs.ts | 187 ++++++++++++- packages/database/src/dbTypes.ts | 76 ++++++ .../20261005140000_access_views_speedup.sql | 103 +++++++ packages/database/supabase/schemas/assets.sql | 14 +- .../database/supabase/schemas/content.sql | 31 ++- .../database/supabase/schemas/embedding.sql | 13 +- 7 files changed, 650 insertions(+), 30 deletions(-) create mode 100644 packages/database/features/contentViewAccess.feature create mode 100644 packages/database/supabase/migrations/20261005140000_access_views_speedup.sql diff --git a/packages/database/features/contentViewAccess.feature b/packages/database/features/contentViewAccess.feature new file mode 100644 index 0000000000..9b5e08b262 --- /dev/null +++ b/packages/database/features/contentViewAccess.feature @@ -0,0 +1,256 @@ +Feature: Content view access + User story: + * As a user of a plugin + * Logged in through a given space's anonymous account + * I want to read the documents, contents, file references and embeddings of my own space, and those shared with me + from other spaces + * In order to sync my space and import from others + + Acceptance criteria: + * my_documents, my_contents, my_file_references and the embedding view return every row of a space where I have reader + or editor access + * They return only the rows of individually shared resources in a space where I have partial access + * They return nothing from a space I have no access to + * Overlapping grants through several groups do not duplicate rows + * The embedding view never returns obsolete embeddings + * Both for own-space reads and for import listings + * my_documents, my_contents and my_file_references are read-only for clients + + Background: + Given the database is blank + And the user user1 opens the Roam plugin in space s1 + And the user user2 opens the Roam plugin in space s2 + And the user user3 opens the Roam plugin in space s3 + And Document are added to the database: + | $id | _space_id | _author_id | source_local_id | created | last_modified | + | d1 | s1 | user1 | ln1 | 2025/01/01 | 2025/01/01 | + | d2 | s1 | user1 | ln2 | 2025/01/01 | 2025/01/01 | + | d3 | s2 | user2 | ln3 | 2025/01/01 | 2025/01/01 | + And Content are added to the database: + | $id | _document_id | _space_id | _author_id | source_local_id | variant | scale | text | created | last_modified | + | c1 | d1 | s1 | user1 | ln1 | direct | document | claim 1 | 2025/01/01 | 2025/01/01 | + | c1f | d1 | s1 | user1 | ln1 | full | document | claim 1 | 2025/01/01 | 2025/01/01 | + | c2 | d2 | s1 | user1 | ln2 | direct | document | claim 2 | 2025/01/01 | 2025/01/01 | + | c2f | d2 | s1 | user1 | ln2 | full | document | claim 2 | 2025/01/01 | 2025/01/01 | + | c3 | d3 | s2 | user2 | ln3 | direct | document | claim 3 | 2025/01/01 | 2025/01/01 | + | c3f | d3 | s2 | user2 | ln3 | full | document | claim 3 | 2025/01/01 | 2025/01/01 | + And FileReference are added to the database: + | _space_id | source_local_id | filepath | filehash | created | last_modified | + | s1 | ln1 | one.png | h1 | 2025/01/01 | 2025/01/01 | + | s1 | ln2 | two.png | h2 | 2025/01/01 | 2025/01/01 | + | s2 | ln3 | three.png | h3 | 2025/01/01 | 2025/01/01 | + And these contents have embeddings: + | content | obsolete | + | c1 | false | + | c1f | true | + | c2 | false | + | c3 | false | + And user of space s1 creates group g1 + And user of space s1 adds space s2 to group g1 + + Scenario: Without a grant, a space sees only its own rows + Then a user logged in space s1 should see these content view rows in space s1: + | view | row | + | my_documents | d1 | + | my_documents | d2 | + | my_contents | c1 | + | my_contents | c1f | + | my_contents | c2 | + | my_contents | c2f | + | my_file_references | one.png | + | my_file_references | two.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c2 | + And a user logged in space s2 should see these content view rows in space s2: + | view | row | + | my_documents | d3 | + | my_contents | c3 | + | my_contents | c3f | + | my_file_references | three.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c3 | + And a user logged in space s2 should see these content view rows in space s1: + | view | row | + And a user logged in space s2 should see these content view rows outside space s2: + | view | row | + And a user logged in space s3 should see these content view rows outside space s3: + | view | row | + + Scenario: Reader access shows every row of the space + When SpaceAccess are added to the database: + | _account_uid | _space_id | permissions | + | g1 | s1 | reader | + Then a user logged in space s2 should see these content view rows outside space s2: + | view | row | + | my_documents | d1 | + | my_documents | d2 | + | my_contents | c1 | + | my_contents | c1f | + | my_contents | c2 | + | my_contents | c2f | + | my_file_references | one.png | + | my_file_references | two.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c2 | + And a user logged in space s2 should see these content view rows in space s1: + | view | row | + | my_documents | d1 | + | my_documents | d2 | + | my_contents | c1 | + | my_contents | c1f | + | my_contents | c2 | + | my_contents | c2f | + | my_file_references | one.png | + | my_file_references | two.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c2 | + And a user logged in space s3 should see these content view rows outside space s3: + | view | row | + + Scenario: Editor access shows every row of the space + When SpaceAccess are added to the database: + | _account_uid | _space_id | permissions | + | g1 | s1 | editor | + Then a user logged in space s2 should see these content view rows outside space s2: + | view | row | + | my_documents | d1 | + | my_documents | d2 | + | my_contents | c1 | + | my_contents | c1f | + | my_contents | c2 | + | my_contents | c2f | + | my_file_references | one.png | + | my_file_references | two.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c2 | + And a user logged in space s2 should see these content view rows in space s1: + | view | row | + | my_documents | d1 | + | my_documents | d2 | + | my_contents | c1 | + | my_contents | c1f | + | my_contents | c2 | + | my_contents | c2f | + | my_file_references | one.png | + | my_file_references | two.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c2 | + + Scenario: Partial access shows only the rows of individually shared resources + When SpaceAccess are added to the database: + | _account_uid | _space_id | permissions | + | g1 | s1 | partial | + And ResourceAccess are added to the database: + | _account_uid | _space_id | source_local_id | + | g1 | s1 | ln1 | + Then a user logged in space s2 should see these content view rows outside space s2: + | view | row | + | my_documents | d1 | + | my_contents | c1 | + | my_contents | c1f | + | my_file_references | one.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + And a user logged in space s2 should see these content view rows in space s1: + | view | row | + | my_documents | d1 | + | my_contents | c1 | + | my_contents | c1f | + | my_file_references | one.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + And a user logged in space s1 should see these content view rows in space s1: + | view | row | + | my_documents | d1 | + | my_documents | d2 | + | my_contents | c1 | + | my_contents | c1f | + | my_contents | c2 | + | my_contents | c2f | + | my_file_references | one.png | + | my_file_references | two.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c2 | + And a user logged in space s3 should see these content view rows outside space s3: + | view | row | + + Scenario: Partial and reader grants through two groups do not duplicate rows + When user of space s1 creates group g2 + And user of space s1 adds space s2 to group g2 + And SpaceAccess are added to the database: + | _account_uid | _space_id | permissions | + | g1 | s1 | partial | + | g2 | s1 | reader | + And ResourceAccess are added to the database: + | _account_uid | _space_id | source_local_id | + | g1 | s1 | ln1 | + Then a user logged in space s2 should see these content view rows outside space s2: + | view | row | + | my_documents | d1 | + | my_documents | d2 | + | my_contents | c1 | + | my_contents | c1f | + | my_contents | c2 | + | my_contents | c2f | + | my_file_references | one.png | + | my_file_references | two.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c2 | + And a user logged in space s2 should see these content view rows in space s1: + | view | row | + | my_documents | d1 | + | my_documents | d2 | + | my_contents | c1 | + | my_contents | c1f | + | my_contents | c2 | + | my_contents | c2f | + | my_file_references | one.png | + | my_file_references | two.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c2 | + + Scenario: The same resource shared through two groups appears once + When user of space s1 creates group g2 + And user of space s1 adds space s2 to group g2 + And SpaceAccess are added to the database: + | _account_uid | _space_id | permissions | + | g1 | s1 | partial | + | g2 | s1 | partial | + And ResourceAccess are added to the database: + | _account_uid | _space_id | source_local_id | + | g1 | s1 | ln1 | + | g2 | s1 | ln1 | + Then a user logged in space s2 should see these content view rows outside space s2: + | view | row | + | my_documents | d1 | + | my_contents | c1 | + | my_contents | c1f | + | my_file_references | one.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + And a user logged in space s2 should see these content view rows in space s1: + | view | row | + | my_documents | d1 | + | my_contents | c1 | + | my_contents | c1f | + | my_file_references | one.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + + Scenario: Clients cannot write through the single-table content views, even in their own space + Then a user logged in space s1 cannot insert documents through my_documents + And a user logged in space s1 cannot update documents through my_documents + And a user logged in space s1 cannot delete documents through my_documents + And a user logged in space s1 cannot insert contents through my_contents + And a user logged in space s1 cannot update contents through my_contents + And a user logged in space s1 cannot delete contents through my_contents + And a user logged in space s1 cannot insert file references through my_file_references + And a user logged in space s1 cannot update file references through my_file_references + And a user logged in space s1 cannot delete file references through my_file_references + And a user logged in space s1 should see these content view rows in space s1: + | view | row | + | my_documents | d1 | + | my_documents | d2 | + | my_contents | c1 | + | my_contents | c1f | + | my_contents | c2 | + | my_contents | c2f | + | my_file_references | one.png | + | my_file_references | two.png | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c1 | + | my_contents_with_embedding_openai_text_embedding_3_small_1536 | c2 | diff --git a/packages/database/features/step-definitions/stepdefs.ts b/packages/database/features/step-definitions/stepdefs.ts index b88b3a3709..90ef71f397 100644 --- a/packages/database/features/step-definitions/stepdefs.ts +++ b/packages/database/features/step-definitions/stepdefs.ts @@ -199,6 +199,22 @@ Given( }, ); +// Access does not depend on the vector, so every embedding gets the same one. +Given("these contents have embeddings:", async (table: DataTable) => { + const client = getServiceClient(); + const localRefs = (world.localRefs || {}) as LocalRefsType; + const vector = JSON.stringify(Array(1536).fill(0.1)); + const rows = table.hashes().map(({ content, obsolete }) => { + const targetId = localRefs[content!]; + if (typeof targetId !== "number") assert.fail(`unknown content ${content}`); + return { target_id: targetId, vector, obsolete: obsolete === "true" }; + }); + const response = await client + .from("ContentEmbedding_openai_text_embedding_3_small_1536") + .insert(rows); + assert.equal(response.error, null); +}); + const userEmail = (userAccountId: string) => `${userAccountId}@example.com`; // Invoke the edge function to log an account into a database. @@ -334,18 +350,29 @@ Then( const INSUFFICIENT_PRIVILEGE = "42501"; -// Exact list comparison, so duplicated rows fail. -const expectVisibleConcepts = async ({ - viewerSpaceName, - filterSpaceName, - inFilterSpace, - table, -}: { +// The column that identifies a row of each view in a visibility table. +const CONTENT_VIEW_KEYS = { + my_documents: "id", + my_contents: "id", + my_file_references: "filepath", + my_contents_with_embedding_openai_text_embedding_3_small_1536: "id", +} as const; +type ContentView = keyof typeof CONTENT_VIEW_KEYS; + +type VisibilityCheck = { viewerSpaceName: string; filterSpaceName: string; inFilterSpace: boolean; table: DataTable; -}): Promise => { +}; + +const getViewerClient = async ( + viewerSpaceName: string, + filterSpaceName: string, +): Promise<{ + filterSpaceId: number; + client: Awaited>; +}> => { const localRefs = (world.localRefs || {}) as LocalRefsType; const viewerSpaceId = localRefs[viewerSpaceName]; const filterSpaceId = localRefs[filterSpaceName]; @@ -353,7 +380,21 @@ const expectVisibleConcepts = async ({ assert.fail("viewer spaceId not a number"); if (typeof filterSpaceId !== "number") assert.fail("filter spaceId not a number"); - const client = await getLoggedinDatabase(viewerSpaceId); + return { filterSpaceId, client: await getLoggedinDatabase(viewerSpaceId) }; +}; + +// Exact list comparison, so duplicated rows fail. +const expectVisibleConcepts = async ({ + viewerSpaceName, + filterSpaceName, + inFilterSpace, + table, +}: VisibilityCheck): Promise => { + const localRefs = (world.localRefs || {}) as LocalRefsType; + const { filterSpaceId, client } = await getViewerClient( + viewerSpaceName, + filterSpaceName, + ); const query = client.from("my_concepts").select("id"); const response = await (inFilterSpace ? query.eq("space_id", filterSpaceId) @@ -380,6 +421,64 @@ Then( }), ); +// Exact list comparison per view, so duplicated rows fail; a view absent from the table must show no rows. +// A row is a local reference, or the literal key value when it is not one. +const expectVisibleContentViewRows = async ({ + viewerSpaceName, + filterSpaceName, + inFilterSpace, + table, +}: VisibilityCheck): Promise => { + const localRefs = (world.localRefs || {}) as LocalRefsType; + const { filterSpaceId, client } = await getViewerClient( + viewerSpaceName, + filterSpaceName, + ); + const views = Object.keys(CONTENT_VIEW_KEYS) as ContentView[]; + const expected = Object.fromEntries( + views.map((view) => [view, [] as string[]]), + ) as Record; + for (const { view, row } of table.hashes()) { + if (!Object.hasOwn(CONTENT_VIEW_KEYS, view!)) + assert.fail(`unknown view ${view}`); + expected[view as ContentView].push(String(localRefs[row!] ?? row)); + } + for (const view of views) { + const key = CONTENT_VIEW_KEYS[view]; + const query = client.from(view).select(key); + const response = await (inFilterSpace + ? query.eq("space_id", filterSpaceId) + : query.neq("space_id", filterSpaceId)); + assert.equal(response.error, null, view); + // With view ranging over four views, the generated select types do not resolve. + const rows = (response.data || []) as unknown as Record[]; + const visible = rows.map((row) => String(row[key])); + assert.deepEqual(visible.sort(), expected[view].sort(), view); + } +}; + +Then( + "a user logged in space {word} should see these content view rows in space {word}:", + (viewerSpaceName: string, filterSpaceName: string, table: DataTable) => + expectVisibleContentViewRows({ + viewerSpaceName, + filterSpaceName, + inFilterSpace: true, + table, + }), +); + +Then( + "a user logged in space {word} should see these content view rows outside space {word}:", + (viewerSpaceName: string, filterSpaceName: string, table: DataTable) => + expectVisibleContentViewRows({ + viewerSpaceName, + filterSpaceName, + inFilterSpace: false, + table, + }), +); + type ViewWrites = Record< "insert" | "update" | "delete", () => PromiseLike> @@ -471,6 +570,76 @@ Then( }, ); +Then( + "a user logged in space {word} cannot {word} documents through my_documents", + async (spaceName: string, operation: string) => { + const { spaceId, client } = await getSpaceClient(spaceName); + const view = client.from("my_documents"); + const now = new Date().toISOString(); + await expectWriteRejected(operation, { + insert: () => + view.insert({ + source_local_id: "written-through-my-documents", + space_id: spaceId, + created: now, + last_modified: now, + }), + update: () => + view + .update({ url: "https://example.com/written-through-my-documents" }) + .eq("space_id", spaceId), + delete: () => view.delete().eq("space_id", spaceId), + }); + }, +); + +Then( + "a user logged in space {word} cannot {word} contents through my_contents", + async (spaceName: string, operation: string) => { + const { spaceId, client } = await getSpaceClient(spaceName); + const view = client.from("my_contents"); + const now = new Date().toISOString(); + await expectWriteRejected(operation, { + insert: () => + view.insert({ + text: "written through my_contents", + space_id: spaceId, + created: now, + last_modified: now, + }), + update: () => + view + .update({ text: "written through my_contents" }) + .eq("space_id", spaceId), + delete: () => view.delete().eq("space_id", spaceId), + }); + }, +); + +Then( + "a user logged in space {word} cannot {word} file references through my_file_references", + async (spaceName: string, operation: string) => { + const { spaceId, client } = await getSpaceClient(spaceName); + const view = client.from("my_file_references"); + const now = new Date().toISOString(); + await expectWriteRejected(operation, { + insert: () => + view.insert({ + filepath: "written-through-my-file-references.png", + filehash: "written-through-my-file-references", + space_id: spaceId, + created: now, + last_modified: now, + }), + update: () => + view + .update({ filehash: "written-through-my-file-references" }) + .eq("space_id", spaceId), + delete: () => view.delete().eq("space_id", spaceId), + }); + }, +); + Then( "a user logged in space {word} should see these concepts outside space {word}:", (viewerSpaceName: string, filterSpaceName: string, table: DataTable) => diff --git a/packages/database/src/dbTypes.ts b/packages/database/src/dbTypes.ts index 41382d92f0..d52583fe5a 100644 --- a/packages/database/src/dbTypes.ts +++ b/packages/database/src/dbTypes.ts @@ -1122,6 +1122,40 @@ export type Database = { text: string | null variant: Database["public"]["Enums"]["ContentVariant"] | null } + Insert: { + author_id?: number | null + content_type?: string | null + created?: string | null + creator_id?: number | null + document_id?: number | null + id?: number | null + last_modified?: string | null + metadata?: Json | null + original?: boolean | null + part_of_id?: number | null + scale?: Database["public"]["Enums"]["Scale"] | null + source_local_id?: string | null + space_id?: number | null + text?: string | null + variant?: Database["public"]["Enums"]["ContentVariant"] | null + } + Update: { + author_id?: number | null + content_type?: string | null + created?: string | null + creator_id?: number | null + document_id?: number | null + id?: number | null + last_modified?: string | null + metadata?: Json | null + original?: boolean | null + part_of_id?: number | null + scale?: Database["public"]["Enums"]["Scale"] | null + source_local_id?: string | null + space_id?: number | null + text?: string | null + variant?: Database["public"]["Enums"]["ContentVariant"] | null + } Relationships: [ { foreignKeyName: "Content_author_id_fkey" @@ -1343,6 +1377,30 @@ export type Database = { space_id: number | null url: string | null } + Insert: { + author_id?: number | null + content_type?: string | null + contents?: unknown + created?: string | null + id?: number | null + last_modified?: string | null + metadata?: Json | null + source_local_id?: string | null + space_id?: number | null + url?: string | null + } + Update: { + author_id?: number | null + content_type?: string | null + contents?: unknown + created?: string | null + id?: number | null + last_modified?: string | null + metadata?: Json | null + source_local_id?: string | null + space_id?: number | null + url?: string | null + } Relationships: [ { foreignKeyName: "Document_author_id_fkey" @@ -1391,6 +1449,24 @@ export type Database = { source_path: string | null space_id: number | null } + Insert: { + created?: string | null + filehash?: string | null + filepath?: string | null + last_modified?: string | null + source_local_id?: string | null + source_path?: string | null + space_id?: number | null + } + Update: { + created?: string | null + filehash?: string | null + filepath?: string | null + last_modified?: string | null + source_local_id?: string | null + source_path?: string | null + space_id?: number | null + } Relationships: [] } my_groups: { diff --git a/packages/database/supabase/migrations/20261005140000_access_views_speedup.sql b/packages/database/supabase/migrations/20261005140000_access_views_speedup.sql new file mode 100644 index 0000000000..d48626edc3 --- /dev/null +++ b/packages/database/supabase/migrations/20261005140000_access_views_speedup.sql @@ -0,0 +1,103 @@ +-- Same form as my_concepts: a single SELECT from the base table, since PostgREST cannot infer foreign keys through a UNION. +-- (SELECT ...) runs each space lookup once per query, not per row; ::bigint [] keeps = any() an array comparison. +CREATE OR REPLACE VIEW public.my_documents AS +SELECT + id, + space_id, + source_local_id, + url, + "created", + metadata, + last_modified, + author_id, + contents, + content_type +FROM public."Document" +WHERE + space_id = any((SELECT public.my_space_ids('reader'))::bigint []) + OR ( + space_id = any((SELECT public.my_space_ids('partial'))::bigint []) + AND (space_id, source_local_id) IN (SELECT space_id, source_local_id FROM public.my_accessible_resources()) + ); + +-- Single-table views are writable, and writes run as the view owner, bypassing Document's RLS. +REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON public.my_documents FROM anon, authenticated; + +CREATE OR REPLACE VIEW public.my_contents AS +SELECT + id, + document_id, + source_local_id, + variant, + author_id, + creator_id, + created, + text, + metadata, + scale, + space_id, + last_modified, + part_of_id, + content_type, + original +FROM public."Content" +WHERE + space_id = any((SELECT public.my_space_ids('reader'))::bigint []) + OR ( + space_id = any((SELECT public.my_space_ids('partial'))::bigint []) + AND (space_id, source_local_id) IN (SELECT space_id, source_local_id FROM public.my_accessible_resources()) + ); + +-- Single-table views are writable, and writes run as the view owner, bypassing Content's RLS. +REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON public.my_contents FROM anon, authenticated; + +CREATE OR REPLACE VIEW public.my_file_references AS +SELECT + source_local_id, + space_id, + filepath, + filehash, + created, + last_modified, + source_path +FROM public."FileReference" +WHERE + space_id = any((SELECT public.my_space_ids('reader'))::bigint []) + OR ( + space_id = any((SELECT public.my_space_ids('partial'))::bigint []) + AND (space_id, source_local_id) IN (SELECT space_id, source_local_id FROM public.my_accessible_resources()) + ); + +-- Single-table views are writable, and writes run as the view owner, bypassing FileReference's RLS. +REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON public.my_file_references FROM anon, authenticated; + +CREATE OR REPLACE VIEW public.my_contents_with_embedding_openai_text_embedding_3_small_1536 AS +SELECT +ct.id, +ct.document_id, +ct.source_local_id, +ct.variant, +ct.author_id, +ct.creator_id, +ct.created, +ct.text, +ct.metadata, +ct.scale, +ct.space_id, +ct.last_modified, +ct.part_of_id, +ct.content_type, +ct.original, +emb.model, +emb.vector +FROM public."Content" AS ct +JOIN public."ContentEmbedding_openai_text_embedding_3_small_1536" AS emb ON (ct.id = emb.target_id) +WHERE +( +ct.space_id = any((SELECT public.my_space_ids('reader'))::bigint []) +OR ( +ct.space_id = any((SELECT public.my_space_ids('partial'))::bigint []) +AND (ct.space_id, ct.source_local_id) IN (SELECT space_id, source_local_id FROM public.my_accessible_resources()) +) +) +AND NOT emb.obsolete; diff --git a/packages/database/supabase/schemas/assets.sql b/packages/database/supabase/schemas/assets.sql index 8a2d203bd3..0c533dbe76 100644 --- a/packages/database/supabase/schemas/assets.sql +++ b/packages/database/supabase/schemas/assets.sql @@ -37,11 +37,15 @@ SELECT last_modified, source_path FROM public."FileReference" - LEFT OUTER JOIN public.my_accessible_resources() AS ra USING (space_id, source_local_id) -WHERE ( - space_id = any(public.my_space_ids('reader')) - OR (space_id = any(public.my_space_ids('partial')) AND ra.space_id IS NOT NULL) -); +WHERE + space_id = any((SELECT public.my_space_ids('reader'))::bigint []) + OR ( + space_id = any((SELECT public.my_space_ids('partial'))::bigint []) + AND (space_id, source_local_id) IN (SELECT space_id, source_local_id FROM public.my_accessible_resources()) + ); + +-- Single-table views are writable, and writes run as the view owner, bypassing FileReference's RLS. +REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON public.my_file_references FROM anon, authenticated; GRANT ALL ON TABLE public."FileReference" TO authenticated; GRANT ALL ON TABLE public."FileReference" TO service_role; diff --git a/packages/database/supabase/schemas/content.sql b/packages/database/supabase/schemas/content.sql index b6f9cdbb73..5745a8c772 100644 --- a/packages/database/supabase/schemas/content.sql +++ b/packages/database/supabase/schemas/content.sql @@ -248,13 +248,16 @@ SELECT author_id, contents, content_type -FROM - public."Document" - LEFT OUTER JOIN public.my_accessible_resources() AS ra USING (space_id, source_local_id) -WHERE ( - space_id = any(public.my_space_ids('reader')) - OR (space_id = any(public.my_space_ids('partial')) AND ra.space_id IS NOT NULL) -); +FROM public."Document" +WHERE + space_id = any((SELECT public.my_space_ids('reader'))::bigint []) + OR ( + space_id = any((SELECT public.my_space_ids('partial'))::bigint []) + AND (space_id, source_local_id) IN (SELECT space_id, source_local_id FROM public.my_accessible_resources()) + ); + +-- Single-table views are writable, and writes run as the view owner, bypassing Document's RLS. +REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON public.my_documents FROM anon, authenticated; CREATE OR REPLACE VIEW public.my_contents AS SELECT @@ -274,11 +277,15 @@ SELECT content_type, original FROM public."Content" - LEFT OUTER JOIN public.my_accessible_resources() AS ra USING (space_id, source_local_id) -WHERE ( - space_id = any(public.my_space_ids('reader')) - OR (space_id = any(public.my_space_ids('partial')) AND ra.space_id IS NOT NULL) -); +WHERE + space_id = any((SELECT public.my_space_ids('reader'))::bigint []) + OR ( + space_id = any((SELECT public.my_space_ids('partial'))::bigint []) + AND (space_id, source_local_id) IN (SELECT space_id, source_local_id FROM public.my_accessible_resources()) + ); + +-- Single-table views are writable, and writes run as the view owner, bypassing Content's RLS. +REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON public.my_contents FROM anon, authenticated; CREATE OR REPLACE FUNCTION public.document_of_content(content public.my_contents) RETURNS SETOF public.my_documents STRICT STABLE diff --git a/packages/database/supabase/schemas/embedding.sql b/packages/database/supabase/schemas/embedding.sql index eb129fe472..c7183fa07d 100644 --- a/packages/database/supabase/schemas/embedding.sql +++ b/packages/database/supabase/schemas/embedding.sql @@ -49,10 +49,15 @@ emb.model, emb.vector FROM public."Content" AS ct JOIN public."ContentEmbedding_openai_text_embedding_3_small_1536" AS emb ON (ct.id = emb.target_id) -LEFT OUTER JOIN public.my_accessible_resources () AS ra USING (space_id, source_local_id) -WHERE (ct.space_id = any (public.my_space_ids ('reader')) -OR (ct.space_id = any (public.my_space_ids ('partial')) AND ra.space_id IS NOT NULL)) -AND NOT emb.obsolete ; +WHERE +( +ct.space_id = any((SELECT public.my_space_ids('reader'))::bigint []) +OR ( +ct.space_id = any((SELECT public.my_space_ids('partial'))::bigint []) +AND (ct.space_id, ct.source_local_id) IN (SELECT space_id, source_local_id FROM public.my_accessible_resources()) +) +) +AND NOT emb.obsolete; set search_path to public, extensions ;