From 12c480a14b80c0e7c8e5886e2f85364c512ecf3e Mon Sep 17 00:00:00 2001 From: stefanbaxter Date: Mon, 12 Oct 2026 23:05:11 +0000 Subject: [PATCH] feat(cubejs): exempt configured partitions from query rewrite row rules The platform owner's team (partition fftech.is) must see every partition, but the rule-based row filters scope every team to its own settings.partition. CUBEJS_ROW_RULES_EXEMPT_PARTITIONS (comma-separated) lists partitions whose teams skip Step 1 of queryRewrite. Partition pruning and the field-level access list still apply. The value is deployment config, and teams cannot write settings through Hasura (owners may update only `name`), so a tenant cannot exempt itself. Scope is queryRewrite only, by owner decision; profileTable, smartGenerate and runSql keep applying the rules. Verified on the local stack through /api/v1/dry-run and /api/v1/load as one user who is a member of fftech.is and admin of bonus.is: with the variable set, fftech.is gets no partition filter and reads eight partitions while bonus.is stays filtered to bonus.is; unset, both are filtered as before. Co-Authored-By: Claude Opus 5.5 --- services/cubejs/src/utils/queryRewrite.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/services/cubejs/src/utils/queryRewrite.js b/services/cubejs/src/utils/queryRewrite.js index 84e42e4a..33a54d9f 100644 --- a/services/cubejs/src/utils/queryRewrite.js +++ b/services/cubejs/src/utils/queryRewrite.js @@ -47,6 +47,18 @@ function loadPartitionPruningPolicy() { const PARTITION_PRUNING = loadPartitionPruningPolicy(); +// --- Row-rule exemption (owner rule, 2026-10-12) --- +// Teams whose `settings.partition` is listed here skip the rule-based row +// filters (Step 1) and see every partition: the platform owner (fftech.is). +// Deployment config only — teams cannot write `settings` through Hasura. +// Comma-separated: CUBEJS_ROW_RULES_EXEMPT_PARTITIONS=fftech.is +const ROW_RULES_EXEMPT_PARTITIONS = new Set( + String(process.env.CUBEJS_ROW_RULES_EXEMPT_PARTITIONS || "") + .split(",") + .map((p) => p.trim()) + .filter(Boolean), +); + const DAY_MS = 24 * 60 * 60 * 1000; function parseExplicitDate(value) { @@ -308,6 +320,7 @@ function extractCubeNames(query) { * 1. Apply rule-based row filtering (all roles, including owner/admin) * Rules are defined by source table name — they apply to ALL cubes * backed by that table, not just cubes with a matching name. + * Skipped for teams in CUBEJS_ROW_RULES_EXEMPT_PARTITIONS. * 2. Apply field-level access list check (non-owner/non-admin only) */ const queryRewrite = async (query, { securityContext }) => { @@ -352,8 +365,9 @@ const queryRewrite = async (query, { securityContext }) => { // --- Step 1: Rule-based row filtering (applies to ALL roles) --- const rules = await loadRules(); + const exempt = ROW_RULES_EXEMPT_PARTITIONS.has(teamProperties?.partition); - if (rules.length > 0) { + if (rules.length > 0 && !exempt) { const queryCubeNames = extractCubeNames(query); // Index rules by table name for fast lookup