From 2b0aa849b47d6fd41a1ad6560862f48807dffda0 Mon Sep 17 00:00:00 2001 From: stefanbaxter Date: Sun, 26 Apr 2026 10:49:00 +0000 Subject: [PATCH] fix(cubejs/meta-all): widen partition match to team.name OR team.settings.partition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keep meta-all scoped to the JWT's active team — but make the matching robust against `team.settings.partition` drift. Previous behaviour required `team.settings.partition === jwt.partition`. That's a soft setting that can fall out of sync with the canonical team identity (`team.name`, set at creation time by `deriveTeamName(email, partition)`). Real-world failure: a team named "blue.is" had `settings.partition` left at "bluecar.is" after a migration. The user was a member-owner of the right team, request-time `defineUserScope` would have accepted any cube on it, but meta-all returned an empty catalog because the soft setting didn't agree with the JWT — so the agent truthfully reported "no cubes" with nothing to query. Match a team to the JWT partition if EITHER condition holds: - `team.name === partition` (canonical — what `deriveTeamName` writes) - `team.settings.partition === partition` (soft — back-compat for teams that set it explicitly without changing the name) Either alone is sufficient. Same scope semantics as before (single team per JWT), just no longer brittle on the soft setting. Also threads `team.name` through the `findUser` GraphQL projection so the match has the canonical field available. Co-Authored-By: Claude Opus 4.7 (1M context) --- services/cubejs/src/routes/metaAll.js | 36 +++++++++++++------ .../cubejs/src/utils/dataSourceHelpers.js | 1 + 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/services/cubejs/src/routes/metaAll.js b/services/cubejs/src/routes/metaAll.js index 792cee89..b8c070a9 100644 --- a/services/cubejs/src/routes/metaAll.js +++ b/services/cubejs/src/routes/metaAll.js @@ -11,7 +11,7 @@ import { provisionUserFromFraiOS, } from "../utils/dataSourceHelpers.js"; import { compileMetaForBranch } from "../utils/metaForBranch.js"; -import { extractCubes, resolvePartitionTeamIds } from "./discover.js"; +import { extractCubes } from "./discover.js"; function getRequestId(req) { return ( @@ -129,11 +129,14 @@ async function metaForDatasource( /** * GET /api/v1/meta-all * - * Aggregated cube catalog across every datasource the caller can see. - * One request walks all partition-filtered datasources, resolves their - * active branch + latest version, compiles each, and returns a summary - * per cube (name, title, description, measures, dimensions, segments, meta, - * `dataschema_id`, `file_name`). + * Aggregated cube catalog scoped to the JWT's active team (partition). + * The caller may be a member of multiple teams across orgs; this endpoint + * returns only the datasources of the team that matches the JWT partition, + * matched by `team.name === partition` OR `team.settings.partition === + * partition`. The dual match avoids the failure mode where a team's + * `settings.partition` drifted from its name (e.g. after a copy/migration) + * and the user — though a member of the right-named team — got an empty + * catalog because the soft setting didn't agree with the JWT. * * Auth: WorkOS RS256 or FraiOS HS256 Bearer token (same as /discover). * @@ -189,11 +192,24 @@ export default async function metaAll(req, res, cubejs) { return res.json({ datasources: [] }); } - const partitionTeamIds = resolvePartitionTeamIds( - user.members, - payload.partition + // Scope to the partition's team. A team matches the JWT partition if + // `team.name === partition` (canonical, set at team-creation by + // deriveTeamName) OR `team.settings.partition === partition` (soft + // setting). Either is sufficient — see route docstring for why both. + const partition = payload.partition; + const partitionTeamIds = new Set( + (user.members || []) + .filter((m) => { + const t = m?.team; + if (!t) return false; + if (!partition) return false; + return ( + t.name === partition || t.settings?.partition === partition + ); + }) + .map((m) => m.team_id) ); - const filtered = partitionTeamIds + const filtered = partition ? user.dataSources.filter((ds) => partitionTeamIds.has(ds.team_id)) : user.dataSources; diff --git a/services/cubejs/src/utils/dataSourceHelpers.js b/services/cubejs/src/utils/dataSourceHelpers.js index 45a70137..4391dfee 100644 --- a/services/cubejs/src/utils/dataSourceHelpers.js +++ b/services/cubejs/src/utils/dataSourceHelpers.js @@ -139,6 +139,7 @@ const userQuery = ` team_id properties team { + name settings datasources { ${sourceFragment}