Skip to content

Commit 63026b6

Browse files
guguclaude
andcommitted
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) <noreply@anthropic.com>
1 parent b4dfb28 commit 63026b6

3 files changed

Lines changed: 82 additions & 20 deletions

File tree

backend/src/entities/cedar-authorization/cedar-policy-parser.ts

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77

88
interface ParsedPermitStatement {
99
action: string | null;
10+
actions: string[] | null;
1011
resourceType: string | null;
1112
resourceId: string | null;
1213
isWildcard: boolean;
@@ -55,21 +56,41 @@ export function parseCedarPolicyToClassicalPermissions(
5556
case 'group:edit':
5657
result.group.accessLevel = AccessLevelEnum.edit;
5758
break;
59+
case 'table:*': {
60+
const wildcardTableName = permit.resourceId ? extractTableName(permit.resourceId, connectionId) : '*';
61+
if (!wildcardTableName) break;
62+
const wildcardTableEntry = getOrCreateTableEntry(tableMap, wildcardTableName);
63+
applyTableAction(wildcardTableEntry, 'table:read');
64+
applyTableAction(wildcardTableEntry, 'table:add');
65+
applyTableAction(wildcardTableEntry, 'table:edit');
66+
applyTableAction(wildcardTableEntry, 'table:delete');
67+
break;
68+
}
5869
case 'table:read':
5970
case 'table:add':
6071
case 'table:edit':
6172
case 'table:delete': {
62-
const tableName = extractTableName(permit.resourceId, connectionId);
73+
const tableName = permit.resourceId ? extractTableName(permit.resourceId, connectionId) : '*';
6374
if (!tableName) break;
6475
const tableEntry = getOrCreateTableEntry(tableMap, tableName);
6576
applyTableAction(tableEntry, permit.action);
6677
break;
6778
}
79+
case 'dashboard:*': {
80+
const wildcardDashboardId = permit.resourceId ? extractDashboardId(permit.resourceId, connectionId) : '*';
81+
if (!wildcardDashboardId) break;
82+
const wildcardDashboardEntry = getOrCreateDashboardEntry(dashboardMap, wildcardDashboardId);
83+
applyDashboardAction(wildcardDashboardEntry, 'dashboard:read');
84+
applyDashboardAction(wildcardDashboardEntry, 'dashboard:create');
85+
applyDashboardAction(wildcardDashboardEntry, 'dashboard:edit');
86+
applyDashboardAction(wildcardDashboardEntry, 'dashboard:delete');
87+
break;
88+
}
6889
case 'dashboard:read':
6990
case 'dashboard:create':
7091
case 'dashboard:edit':
7192
case 'dashboard:delete': {
72-
const dashboardId = extractDashboardId(permit.resourceId, connectionId);
93+
const dashboardId = permit.resourceId ? extractDashboardId(permit.resourceId, connectionId) : '*';
7394
if (!dashboardId) break;
7495
const dashboardEntry = getOrCreateDashboardEntry(dashboardMap, dashboardId);
7596
applyDashboardAction(dashboardEntry, permit.action);
@@ -95,7 +116,10 @@ function extractPermitStatements(policyText: string): ParsedPermitStatement[] {
95116

96117
let i = permitIndex + permitKeyword.length;
97118
// Skip whitespace after "permit"
98-
while (i < policyText.length && (policyText[i] === ' ' || policyText[i] === '\t' || policyText[i] === '\n' || policyText[i] === '\r')) {
119+
while (
120+
i < policyText.length &&
121+
(policyText[i] === ' ' || policyText[i] === '\t' || policyText[i] === '\n' || policyText[i] === '\r')
122+
) {
99123
i++;
100124
}
101125

@@ -122,7 +146,10 @@ function extractPermitStatements(policyText: string): ParsedPermitStatement[] {
122146
const body = policyText.slice(bodyStart, i);
123147
// Skip past ')' and optional whitespace, expect ';'
124148
let j = i + 1;
125-
while (j < policyText.length && (policyText[j] === ' ' || policyText[j] === '\t' || policyText[j] === '\n' || policyText[j] === '\r')) {
149+
while (
150+
j < policyText.length &&
151+
(policyText[j] === ' ' || policyText[j] === '\t' || policyText[j] === '\n' || policyText[j] === '\r')
152+
) {
126153
j++;
127154
}
128155

@@ -134,28 +161,39 @@ function extractPermitStatements(policyText: string): ParsedPermitStatement[] {
134161
}
135162
}
136163

137-
return results;
164+
return results.flatMap(expandActionIn);
165+
}
166+
167+
function expandActionIn(stmt: ParsedPermitStatement): ParsedPermitStatement[] {
168+
if (!stmt.actions || stmt.actions.length === 0) return [stmt];
169+
return stmt.actions.map((action) => ({ ...stmt, action, actions: null }));
138170
}
139171

140172
function parsePermitBody(body: string): ParsedPermitStatement {
141173
const result: ParsedPermitStatement = {
142174
action: null,
175+
actions: null,
143176
resourceType: null,
144177
resourceId: null,
145178
isWildcard: false,
146179
};
147180

148-
const actionMatch = body.match(/action\s*==\s*RocketAdmin::Action::"([^"]+)"/);
181+
const actionMatch = body.match(/action\s*(?:==|like)\s*RocketAdmin::Action::"([^"]+)"/);
149182
if (actionMatch) {
150183
result.action = actionMatch[1];
151184
} else {
152-
const actionClause = body.match(/,\s*(action)\s*,/);
153-
if (actionClause) {
154-
result.isWildcard = true;
185+
const actionInMatch = body.match(/action\s+in\s*\[([^\]]+)\]/);
186+
if (actionInMatch) {
187+
result.actions = [...actionInMatch[1].matchAll(/RocketAdmin::Action::"([^"]+)"/g)].map((m) => m[1]);
188+
} else {
189+
const actionClause = body.match(/,\s*(action)\s*,/);
190+
if (actionClause) {
191+
result.isWildcard = true;
192+
}
155193
}
156194
}
157195

158-
const resourceMatch = body.match(/resource\s*==\s*(RocketAdmin::\w+)::"([^"]+)"/);
196+
const resourceMatch = body.match(/resource\s*(?:==|like)\s*(RocketAdmin::\w+)::"([^"]+)"/);
159197
if (resourceMatch) {
160198
result.resourceType = resourceMatch[1];
161199
result.resourceId = resourceMatch[2];

frontend/src/app/lib/cedar-policy-items.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,7 @@ export function policyItemsToCedarPolicy(items: CedarPolicyItem[], connectionId:
132132
return policies.join('\n\n');
133133
}
134134

135-
const actionRef =
136-
item.action === 'table:*' || item.action === 'dashboard:*'
137-
? `action like RocketAdmin::Action::"${item.action}"`
138-
: `action == RocketAdmin::Action::"${item.action}"`;
135+
const actionRef = buildActionRef(item.action);
139136
let resource: string;
140137

141138
if (item.action.startsWith('table:')) {
@@ -154,9 +151,24 @@ export function policyItemsToCedarPolicy(items: CedarPolicyItem[], connectionId:
154151
return policies.join('\n\n');
155152
}
156153

154+
const TABLE_ACTIONS = ['table:read', 'table:add', 'table:edit', 'table:delete'];
155+
const DASHBOARD_ACTIONS = ['dashboard:read', 'dashboard:create', 'dashboard:edit', 'dashboard:delete'];
156+
157+
function buildActionRef(action: string): string {
158+
if (action === 'table:*') {
159+
const list = TABLE_ACTIONS.map((a) => `RocketAdmin::Action::"${a}"`).join(', ');
160+
return `action in [${list}]`;
161+
}
162+
if (action === 'dashboard:*') {
163+
const list = DASHBOARD_ACTIONS.map((a) => `RocketAdmin::Action::"${a}"`).join(', ');
164+
return `action in [${list}]`;
165+
}
166+
return `action == RocketAdmin::Action::"${action}"`;
167+
}
168+
157169
function buildResourceRef(type: string, connectionId: string, id: string | undefined): string {
158170
if (id === '*') {
159-
return `resource like RocketAdmin::${type}::"${connectionId}/*"`;
171+
return `resource`;
160172
}
161173
return `resource == RocketAdmin::${type}::"${connectionId}/${id}"`;
162174
}

frontend/src/app/lib/cedar-policy-parser.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { CedarPolicyItem } from './cedar-policy-items';
33

44
interface ParsedPermitStatement {
55
action: string | null;
6+
actions: string[] | null;
67
resourceType: string | null;
78
resourceId: string | null;
89
isWildcard: boolean;
@@ -225,12 +226,18 @@ function extractPermitStatements(policyText: string): ParsedPermitStatement[] {
225226
}
226227
}
227228

228-
return results;
229+
return results.flatMap(expandActionIn);
230+
}
231+
232+
function expandActionIn(stmt: ParsedPermitStatement): ParsedPermitStatement[] {
233+
if (!stmt.actions || stmt.actions.length === 0) return [stmt];
234+
return stmt.actions.map((action) => ({ ...stmt, action, actions: null }));
229235
}
230236

231237
function parsePermitBody(body: string): ParsedPermitStatement {
232238
const result: ParsedPermitStatement = {
233239
action: null,
240+
actions: null,
234241
resourceType: null,
235242
resourceId: null,
236243
isWildcard: false,
@@ -240,9 +247,14 @@ function parsePermitBody(body: string): ParsedPermitStatement {
240247
if (actionMatch) {
241248
result.action = actionMatch[1];
242249
} else {
243-
const actionClause = body.match(/,\s*(action)\s*,/);
244-
if (actionClause) {
245-
result.isWildcard = true;
250+
const actionInMatch = body.match(/action\s+in\s*\[([^\]]+)\]/);
251+
if (actionInMatch) {
252+
result.actions = [...actionInMatch[1].matchAll(/RocketAdmin::Action::"([^"]+)"/g)].map((m) => m[1]);
253+
} else {
254+
const actionClause = body.match(/,\s*(action)\s*,/);
255+
if (actionClause) {
256+
result.isWildcard = true;
257+
}
246258
}
247259
}
248260

@@ -261,7 +273,7 @@ function parsePermitBody(body: string): ParsedPermitStatement {
261273
}
262274

263275
function extractResourceSuffix(resourceId: string | null, connectionId: string): string | null {
264-
if (!resourceId) return null;
276+
if (!resourceId) return '*';
265277
const prefix = `${connectionId}/`;
266278
if (resourceId.startsWith(prefix)) {
267279
return resourceId.slice(prefix.length);

0 commit comments

Comments
 (0)