From 63026b6448efac42632bbeaf767f9657dc6d2112 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Thu, 19 Mar 2026 15:52:51 +0000 Subject: [PATCH 1/2] fix: use action in [...] and remove resource filter for all-tables/all-dashboards policies Replace `action like` with `action in [...]` for wildcard table/dashboard actions, and use bare `resource` instead of `resource like .../*` when targeting all tables or all dashboards. Both frontend and backend parsers updated to handle the new syntax with backwards compatibility for `like`. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cedar-policy-parser.ts | 58 +++++++++++++++---- frontend/src/app/lib/cedar-policy-items.ts | 22 +++++-- frontend/src/app/lib/cedar-policy-parser.ts | 22 +++++-- 3 files changed, 82 insertions(+), 20 deletions(-) diff --git a/backend/src/entities/cedar-authorization/cedar-policy-parser.ts b/backend/src/entities/cedar-authorization/cedar-policy-parser.ts index 7e08cfd2f..226955893 100644 --- a/backend/src/entities/cedar-authorization/cedar-policy-parser.ts +++ b/backend/src/entities/cedar-authorization/cedar-policy-parser.ts @@ -7,6 +7,7 @@ import { interface ParsedPermitStatement { action: string | null; + actions: string[] | null; resourceType: string | null; resourceId: string | null; isWildcard: boolean; @@ -55,21 +56,41 @@ export function parseCedarPolicyToClassicalPermissions( case 'group:edit': result.group.accessLevel = AccessLevelEnum.edit; break; + case 'table:*': { + const wildcardTableName = permit.resourceId ? extractTableName(permit.resourceId, connectionId) : '*'; + if (!wildcardTableName) break; + const wildcardTableEntry = getOrCreateTableEntry(tableMap, wildcardTableName); + applyTableAction(wildcardTableEntry, 'table:read'); + applyTableAction(wildcardTableEntry, 'table:add'); + applyTableAction(wildcardTableEntry, 'table:edit'); + applyTableAction(wildcardTableEntry, 'table:delete'); + break; + } case 'table:read': case 'table:add': case 'table:edit': case 'table:delete': { - const tableName = extractTableName(permit.resourceId, connectionId); + const tableName = permit.resourceId ? extractTableName(permit.resourceId, connectionId) : '*'; if (!tableName) break; const tableEntry = getOrCreateTableEntry(tableMap, tableName); applyTableAction(tableEntry, permit.action); break; } + case 'dashboard:*': { + const wildcardDashboardId = permit.resourceId ? extractDashboardId(permit.resourceId, connectionId) : '*'; + if (!wildcardDashboardId) break; + const wildcardDashboardEntry = getOrCreateDashboardEntry(dashboardMap, wildcardDashboardId); + applyDashboardAction(wildcardDashboardEntry, 'dashboard:read'); + applyDashboardAction(wildcardDashboardEntry, 'dashboard:create'); + applyDashboardAction(wildcardDashboardEntry, 'dashboard:edit'); + applyDashboardAction(wildcardDashboardEntry, 'dashboard:delete'); + break; + } case 'dashboard:read': case 'dashboard:create': case 'dashboard:edit': case 'dashboard:delete': { - const dashboardId = extractDashboardId(permit.resourceId, connectionId); + const dashboardId = permit.resourceId ? extractDashboardId(permit.resourceId, connectionId) : '*'; if (!dashboardId) break; const dashboardEntry = getOrCreateDashboardEntry(dashboardMap, dashboardId); applyDashboardAction(dashboardEntry, permit.action); @@ -95,7 +116,10 @@ function extractPermitStatements(policyText: string): ParsedPermitStatement[] { let i = permitIndex + permitKeyword.length; // Skip whitespace after "permit" - while (i < policyText.length && (policyText[i] === ' ' || policyText[i] === '\t' || policyText[i] === '\n' || policyText[i] === '\r')) { + while ( + i < policyText.length && + (policyText[i] === ' ' || policyText[i] === '\t' || policyText[i] === '\n' || policyText[i] === '\r') + ) { i++; } @@ -122,7 +146,10 @@ function extractPermitStatements(policyText: string): ParsedPermitStatement[] { const body = policyText.slice(bodyStart, i); // Skip past ')' and optional whitespace, expect ';' let j = i + 1; - while (j < policyText.length && (policyText[j] === ' ' || policyText[j] === '\t' || policyText[j] === '\n' || policyText[j] === '\r')) { + while ( + j < policyText.length && + (policyText[j] === ' ' || policyText[j] === '\t' || policyText[j] === '\n' || policyText[j] === '\r') + ) { j++; } @@ -134,28 +161,39 @@ function extractPermitStatements(policyText: string): ParsedPermitStatement[] { } } - return results; + return results.flatMap(expandActionIn); +} + +function expandActionIn(stmt: ParsedPermitStatement): ParsedPermitStatement[] { + if (!stmt.actions || stmt.actions.length === 0) return [stmt]; + return stmt.actions.map((action) => ({ ...stmt, action, actions: null })); } function parsePermitBody(body: string): ParsedPermitStatement { const result: ParsedPermitStatement = { action: null, + actions: null, resourceType: null, resourceId: null, isWildcard: false, }; - const actionMatch = body.match(/action\s*==\s*RocketAdmin::Action::"([^"]+)"/); + const actionMatch = body.match(/action\s*(?:==|like)\s*RocketAdmin::Action::"([^"]+)"/); if (actionMatch) { result.action = actionMatch[1]; } else { - const actionClause = body.match(/,\s*(action)\s*,/); - if (actionClause) { - result.isWildcard = true; + const actionInMatch = body.match(/action\s+in\s*\[([^\]]+)\]/); + if (actionInMatch) { + result.actions = [...actionInMatch[1].matchAll(/RocketAdmin::Action::"([^"]+)"/g)].map((m) => m[1]); + } else { + const actionClause = body.match(/,\s*(action)\s*,/); + if (actionClause) { + result.isWildcard = true; + } } } - const resourceMatch = body.match(/resource\s*==\s*(RocketAdmin::\w+)::"([^"]+)"/); + const resourceMatch = body.match(/resource\s*(?:==|like)\s*(RocketAdmin::\w+)::"([^"]+)"/); if (resourceMatch) { result.resourceType = resourceMatch[1]; result.resourceId = resourceMatch[2]; diff --git a/frontend/src/app/lib/cedar-policy-items.ts b/frontend/src/app/lib/cedar-policy-items.ts index 2b29237d9..cc2aeb652 100644 --- a/frontend/src/app/lib/cedar-policy-items.ts +++ b/frontend/src/app/lib/cedar-policy-items.ts @@ -132,10 +132,7 @@ export function policyItemsToCedarPolicy(items: CedarPolicyItem[], connectionId: return policies.join('\n\n'); } - const actionRef = - item.action === 'table:*' || item.action === 'dashboard:*' - ? `action like RocketAdmin::Action::"${item.action}"` - : `action == RocketAdmin::Action::"${item.action}"`; + const actionRef = buildActionRef(item.action); let resource: string; if (item.action.startsWith('table:')) { @@ -154,9 +151,24 @@ export function policyItemsToCedarPolicy(items: CedarPolicyItem[], connectionId: return policies.join('\n\n'); } +const TABLE_ACTIONS = ['table:read', 'table:add', 'table:edit', 'table:delete']; +const DASHBOARD_ACTIONS = ['dashboard:read', 'dashboard:create', 'dashboard:edit', 'dashboard:delete']; + +function buildActionRef(action: string): string { + if (action === 'table:*') { + const list = TABLE_ACTIONS.map((a) => `RocketAdmin::Action::"${a}"`).join(', '); + return `action in [${list}]`; + } + if (action === 'dashboard:*') { + const list = DASHBOARD_ACTIONS.map((a) => `RocketAdmin::Action::"${a}"`).join(', '); + return `action in [${list}]`; + } + return `action == RocketAdmin::Action::"${action}"`; +} + function buildResourceRef(type: string, connectionId: string, id: string | undefined): string { if (id === '*') { - return `resource like RocketAdmin::${type}::"${connectionId}/*"`; + return `resource`; } return `resource == RocketAdmin::${type}::"${connectionId}/${id}"`; } diff --git a/frontend/src/app/lib/cedar-policy-parser.ts b/frontend/src/app/lib/cedar-policy-parser.ts index 4b3ce21c0..8f938297d 100644 --- a/frontend/src/app/lib/cedar-policy-parser.ts +++ b/frontend/src/app/lib/cedar-policy-parser.ts @@ -3,6 +3,7 @@ import { CedarPolicyItem } from './cedar-policy-items'; interface ParsedPermitStatement { action: string | null; + actions: string[] | null; resourceType: string | null; resourceId: string | null; isWildcard: boolean; @@ -225,12 +226,18 @@ function extractPermitStatements(policyText: string): ParsedPermitStatement[] { } } - return results; + return results.flatMap(expandActionIn); +} + +function expandActionIn(stmt: ParsedPermitStatement): ParsedPermitStatement[] { + if (!stmt.actions || stmt.actions.length === 0) return [stmt]; + return stmt.actions.map((action) => ({ ...stmt, action, actions: null })); } function parsePermitBody(body: string): ParsedPermitStatement { const result: ParsedPermitStatement = { action: null, + actions: null, resourceType: null, resourceId: null, isWildcard: false, @@ -240,9 +247,14 @@ function parsePermitBody(body: string): ParsedPermitStatement { if (actionMatch) { result.action = actionMatch[1]; } else { - const actionClause = body.match(/,\s*(action)\s*,/); - if (actionClause) { - result.isWildcard = true; + const actionInMatch = body.match(/action\s+in\s*\[([^\]]+)\]/); + if (actionInMatch) { + result.actions = [...actionInMatch[1].matchAll(/RocketAdmin::Action::"([^"]+)"/g)].map((m) => m[1]); + } else { + const actionClause = body.match(/,\s*(action)\s*,/); + if (actionClause) { + result.isWildcard = true; + } } } @@ -261,7 +273,7 @@ function parsePermitBody(body: string): ParsedPermitStatement { } function extractResourceSuffix(resourceId: string | null, connectionId: string): string | null { - if (!resourceId) return null; + if (!resourceId) return '*'; const prefix = `${connectionId}/`; if (resourceId.startsWith(prefix)) { return resourceId.slice(prefix.length); From d815756516e525da255f71d7e9866d7cfb94a425 Mon Sep 17 00:00:00 2001 From: Andrii Kostenko Date: Thu, 19 Mar 2026 15:54:28 +0000 Subject: [PATCH 2/2] revert: remove backend cedar-policy-parser changes Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cedar-policy-parser.ts | 48 ++++--------------- 1 file changed, 8 insertions(+), 40 deletions(-) diff --git a/backend/src/entities/cedar-authorization/cedar-policy-parser.ts b/backend/src/entities/cedar-authorization/cedar-policy-parser.ts index 226955893..f8475434c 100644 --- a/backend/src/entities/cedar-authorization/cedar-policy-parser.ts +++ b/backend/src/entities/cedar-authorization/cedar-policy-parser.ts @@ -7,7 +7,6 @@ import { interface ParsedPermitStatement { action: string | null; - actions: string[] | null; resourceType: string | null; resourceId: string | null; isWildcard: boolean; @@ -56,41 +55,21 @@ export function parseCedarPolicyToClassicalPermissions( case 'group:edit': result.group.accessLevel = AccessLevelEnum.edit; break; - case 'table:*': { - const wildcardTableName = permit.resourceId ? extractTableName(permit.resourceId, connectionId) : '*'; - if (!wildcardTableName) break; - const wildcardTableEntry = getOrCreateTableEntry(tableMap, wildcardTableName); - applyTableAction(wildcardTableEntry, 'table:read'); - applyTableAction(wildcardTableEntry, 'table:add'); - applyTableAction(wildcardTableEntry, 'table:edit'); - applyTableAction(wildcardTableEntry, 'table:delete'); - break; - } case 'table:read': case 'table:add': case 'table:edit': case 'table:delete': { - const tableName = permit.resourceId ? extractTableName(permit.resourceId, connectionId) : '*'; + const tableName = extractTableName(permit.resourceId, connectionId); if (!tableName) break; const tableEntry = getOrCreateTableEntry(tableMap, tableName); applyTableAction(tableEntry, permit.action); break; } - case 'dashboard:*': { - const wildcardDashboardId = permit.resourceId ? extractDashboardId(permit.resourceId, connectionId) : '*'; - if (!wildcardDashboardId) break; - const wildcardDashboardEntry = getOrCreateDashboardEntry(dashboardMap, wildcardDashboardId); - applyDashboardAction(wildcardDashboardEntry, 'dashboard:read'); - applyDashboardAction(wildcardDashboardEntry, 'dashboard:create'); - applyDashboardAction(wildcardDashboardEntry, 'dashboard:edit'); - applyDashboardAction(wildcardDashboardEntry, 'dashboard:delete'); - break; - } case 'dashboard:read': case 'dashboard:create': case 'dashboard:edit': case 'dashboard:delete': { - const dashboardId = permit.resourceId ? extractDashboardId(permit.resourceId, connectionId) : '*'; + const dashboardId = extractDashboardId(permit.resourceId, connectionId); if (!dashboardId) break; const dashboardEntry = getOrCreateDashboardEntry(dashboardMap, dashboardId); applyDashboardAction(dashboardEntry, permit.action); @@ -161,39 +140,28 @@ function extractPermitStatements(policyText: string): ParsedPermitStatement[] { } } - return results.flatMap(expandActionIn); -} - -function expandActionIn(stmt: ParsedPermitStatement): ParsedPermitStatement[] { - if (!stmt.actions || stmt.actions.length === 0) return [stmt]; - return stmt.actions.map((action) => ({ ...stmt, action, actions: null })); + return results; } function parsePermitBody(body: string): ParsedPermitStatement { const result: ParsedPermitStatement = { action: null, - actions: null, resourceType: null, resourceId: null, isWildcard: false, }; - const actionMatch = body.match(/action\s*(?:==|like)\s*RocketAdmin::Action::"([^"]+)"/); + const actionMatch = body.match(/action\s*==\s*RocketAdmin::Action::"([^"]+)"/); if (actionMatch) { result.action = actionMatch[1]; } else { - const actionInMatch = body.match(/action\s+in\s*\[([^\]]+)\]/); - if (actionInMatch) { - result.actions = [...actionInMatch[1].matchAll(/RocketAdmin::Action::"([^"]+)"/g)].map((m) => m[1]); - } else { - const actionClause = body.match(/,\s*(action)\s*,/); - if (actionClause) { - result.isWildcard = true; - } + const actionClause = body.match(/,\s*(action)\s*,/); + if (actionClause) { + result.isWildcard = true; } } - const resourceMatch = body.match(/resource\s*(?:==|like)\s*(RocketAdmin::\w+)::"([^"]+)"/); + const resourceMatch = body.match(/resource\s*==\s*(RocketAdmin::\w+)::"([^"]+)"/); if (resourceMatch) { result.resourceType = resourceMatch[1]; result.resourceId = resourceMatch[2];