From e83dd8d7603bd78f3b1456c75be29ee85c4ab120 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 12 Feb 2026 23:08:20 -0800 Subject: [PATCH] feat(api): scope API keys to organization in Electric sync Filter apikeys by organizationId from metadata JSON instead of userId, so users only see keys belonging to the active organization. --- .../src/app/api/electric/[...path]/utils.ts | 7 ++- .../CollectionsProvider/collections.ts | 60 +++++++++---------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/apps/api/src/app/api/electric/[...path]/utils.ts b/apps/api/src/app/api/electric/[...path]/utils.ts index 68a4ca82479..280551d59fb 100644 --- a/apps/api/src/app/api/electric/[...path]/utils.ts +++ b/apps/api/src/app/api/electric/[...path]/utils.ts @@ -1,7 +1,6 @@ import { db } from "@superset/db/client"; import { agentCommands, - apikeys, devicePresence, integrationConnections, invitations, @@ -109,8 +108,10 @@ export async function buildWhereClause( case "agent_commands": return build(agentCommands, agentCommands.organizationId, organizationId); - case "auth.apikeys": - return build(apikeys, apikeys.userId, userId); + case "auth.apikeys": { + const fragment = `"metadata"::jsonb->>'organizationId' = $1`; + return { fragment, params: [organizationId] }; + } case "integration_connections": return build( diff --git a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts index 6dfb832f15b..9af5b435a7e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/providers/CollectionsProvider/collections.ts @@ -25,6 +25,16 @@ import { z } from "zod"; const columnMapper = snakeCamelMapper(); const electricUrl = `${env.NEXT_PUBLIC_API_URL}/api/electric/v1/shape`; +const apiKeyDisplaySchema = z.object({ + id: z.string(), + name: z.string().nullable(), + start: z.string().nullable(), + createdAt: z.coerce.date(), + lastRequest: z.coerce.date().nullable(), +}); + +type ApiKeyDisplay = z.infer; + interface OrgCollections { tasks: Collection; taskStatuses: Collection; @@ -36,6 +46,7 @@ interface OrgCollections { devicePresence: Collection; integrationConnections: Collection; subscriptions: Collection; + apiKeys: Collection; } // Per-org collections cache @@ -73,34 +84,6 @@ const organizationsCollection = createCollection( }), ); -const apiKeyDisplaySchema = z.object({ - id: z.string(), - name: z.string().nullable(), - start: z.string().nullable(), - createdAt: z.coerce.date(), - lastRequest: z.coerce.date().nullable(), -}); - -type ApiKeyDisplay = z.infer; - -const apiKeysCollection = createCollection( - electricCollectionOptions({ - id: "apikeys", - shapeOptions: { - url: electricUrl, - params: { table: "auth.apikeys" }, - headers: { - Authorization: () => { - const token = getAuthToken(); - return token ? `Bearer ${token}` : ""; - }, - }, - columnMapper, - }, - getKey: (item) => item.id, - }), -); - function createOrgCollections(organizationId: string): OrgCollections { const headers = { Authorization: () => { @@ -313,6 +296,22 @@ function createOrgCollections(organizationId: string): OrgCollections { }), ); + const apiKeys = createCollection( + electricCollectionOptions({ + id: `apikeys-${organizationId}`, + shapeOptions: { + url: electricUrl, + params: { + table: "auth.apikeys", + organizationId, + }, + headers, + columnMapper, + }, + getKey: (item) => item.id, + }), + ); + return { tasks, taskStatuses, @@ -324,6 +323,7 @@ function createOrgCollections(organizationId: string): OrgCollections { devicePresence, integrationConnections, subscriptions, + apiKeys, }; } @@ -335,8 +335,7 @@ function createOrgCollections(organizationId: string): OrgCollections { export async function preloadCollections( organizationId: string, ): Promise { - const { organizations, apiKeys, ...orgCollections } = - getCollections(organizationId); + const { organizations, ...orgCollections } = getCollections(organizationId); await Promise.allSettled( Object.values(orgCollections).map((c) => (c as Collection).preload(), @@ -363,6 +362,5 @@ export function getCollections(organizationId: string) { return { ...orgCollections, organizations: organizationsCollection, - apiKeys: apiKeysCollection, }; }