Skip to content

Commit 66f35c2

Browse files
guguclaude
andcommitted
Add optgroups for actions and table:* (all table actions) option
- Group actions into optgroups: General, Connection, Group, Table - Add table:* action that expands to all four table permits - Collapse per-table full access into single table:* policy item - Use needsTable from POLICY_ACTIONS lookup instead of string prefix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 991759e commit 66f35c2

4 files changed

Lines changed: 91 additions & 30 deletions

File tree

frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.html

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@
3131
<mat-form-field appearance="outline" class="policy-field">
3232
<mat-label>Action</mat-label>
3333
<mat-select [(ngModel)]="editAction" [ngModelOptions]="{standalone: true}">
34-
<mat-option *ngFor="let action of availableActions" [value]="action.value">
35-
{{ action.label }}
36-
</mat-option>
34+
<mat-optgroup *ngFor="let group of actionGroups" [label]="group.group">
35+
<mat-option *ngFor="let action of group.actions" [value]="action.value">
36+
{{ action.label }}
37+
</mat-option>
38+
</mat-optgroup>
3739
</mat-select>
3840
</mat-form-field>
3941

@@ -64,9 +66,11 @@
6466
<mat-form-field appearance="outline" class="policy-field">
6567
<mat-label>Action</mat-label>
6668
<mat-select [(ngModel)]="newAction" [ngModelOptions]="{standalone: true}">
67-
<mat-option *ngFor="let action of availableActions" [value]="action.value">
68-
{{ action.label }}
69-
</mat-option>
69+
<mat-optgroup *ngFor="let group of actionGroups" [label]="group.group">
70+
<mat-option *ngFor="let action of group.actions" [value]="action.value">
71+
{{ action.label }}
72+
</mat-option>
73+
</mat-optgroup>
7074
</mat-select>
7175
</mat-form-field>
7276

frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.spec.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,9 @@ describe('CedarPolicyListComponent', () => {
117117

118118
it('should return correct action labels', () => {
119119
expect(component.getActionLabel('*')).toBe('Full access (all permissions)');
120-
expect(component.getActionLabel('connection:read')).toBe('Connection: Read');
121-
expect(component.getActionLabel('table:edit')).toBe('Table: Edit');
120+
expect(component.getActionLabel('connection:read')).toBe('Read');
121+
expect(component.getActionLabel('table:edit')).toBe('Edit');
122+
expect(component.getActionLabel('table:*')).toBe('Full access');
122123
});
123124

124125
it('should return correct table display names', () => {
@@ -133,6 +134,9 @@ describe('CedarPolicyListComponent', () => {
133134

134135
component.newAction = 'table:read';
135136
expect(component.needsTable).toBe(true);
137+
138+
component.newAction = 'table:*';
139+
expect(component.needsTable).toBe(true);
136140
});
137141

138142
it('should reset add form', () => {

frontend/src/app/components/users/cedar-policy-list/cedar-policy-list.component.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
66
import { MatIconModule } from '@angular/material/icon';
77
import { MatSelectModule } from '@angular/material/select';
88
import { MatTooltipModule } from '@angular/material/tooltip';
9-
import { CedarPolicyItem, POLICY_ACTIONS } from 'src/app/lib/cedar-policy-items';
9+
import { CedarPolicyItem, POLICY_ACTION_GROUPS, POLICY_ACTIONS } from 'src/app/lib/cedar-policy-items';
1010
import { ContentLoaderComponent } from '../../ui-components/content-loader/content-loader.component';
1111

1212
export interface AvailableTable {
@@ -44,13 +44,14 @@ export class CedarPolicyListComponent {
4444
editTableName = '';
4545

4646
availableActions = POLICY_ACTIONS;
47+
actionGroups = POLICY_ACTION_GROUPS;
4748

4849
get needsTable(): boolean {
49-
return this.newAction.startsWith('table:');
50+
return this.availableActions.find((a) => a.value === this.newAction)?.needsTable ?? false;
5051
}
5152

5253
get editNeedsTable(): boolean {
53-
return this.editAction.startsWith('table:');
54+
return this.availableActions.find((a) => a.value === this.editAction)?.needsTable ?? false;
5455
}
5556

5657
getActionLabel(action: string): string {

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

Lines changed: 71 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,50 @@ export interface CedarPolicyItem {
55
tableName?: string;
66
}
77

8-
export const POLICY_ACTIONS = [
9-
{ value: '*', label: 'Full access (all permissions)', needsTable: false },
10-
{ value: 'connection:read', label: 'Connection: Read', needsTable: false },
11-
{ value: 'connection:edit', label: 'Connection: Full access', needsTable: false },
12-
{ value: 'group:read', label: 'Group: Read', needsTable: false },
13-
{ value: 'group:edit', label: 'Group: Manage', needsTable: false },
14-
{ value: 'table:read', label: 'Table: Read', needsTable: true },
15-
{ value: 'table:add', label: 'Table: Add', needsTable: true },
16-
{ value: 'table:edit', label: 'Table: Edit', needsTable: true },
17-
{ value: 'table:delete', label: 'Table: Delete', needsTable: true },
8+
export interface PolicyAction {
9+
value: string;
10+
label: string;
11+
needsTable: boolean;
12+
}
13+
14+
export interface PolicyActionGroup {
15+
group: string;
16+
actions: PolicyAction[];
17+
}
18+
19+
export const POLICY_ACTION_GROUPS: PolicyActionGroup[] = [
20+
{
21+
group: 'General',
22+
actions: [{ value: '*', label: 'Full access (all permissions)', needsTable: false }],
23+
},
24+
{
25+
group: 'Connection',
26+
actions: [
27+
{ value: 'connection:read', label: 'Read', needsTable: false },
28+
{ value: 'connection:edit', label: 'Full access', needsTable: false },
29+
],
30+
},
31+
{
32+
group: 'Group',
33+
actions: [
34+
{ value: 'group:read', label: 'Read', needsTable: false },
35+
{ value: 'group:edit', label: 'Manage', needsTable: false },
36+
],
37+
},
38+
{
39+
group: 'Table',
40+
actions: [
41+
{ value: 'table:*', label: 'Full access', needsTable: true },
42+
{ value: 'table:read', label: 'Read', needsTable: true },
43+
{ value: 'table:add', label: 'Add', needsTable: true },
44+
{ value: 'table:edit', label: 'Edit', needsTable: true },
45+
{ value: 'table:delete', label: 'Delete', needsTable: true },
46+
],
47+
},
1848
];
1949

50+
export const POLICY_ACTIONS: PolicyAction[] = POLICY_ACTION_GROUPS.flatMap((g) => g.actions);
51+
2052
export function permissionsToPolicyItems(permissions: Permissions): CedarPolicyItem[] {
2153
const items: CedarPolicyItem[] = [];
2254

@@ -47,11 +79,7 @@ export function permissionsToPolicyItems(permissions: Permissions): CedarPolicyI
4779
const anyTableAccess = allRead || allAdd || allEdit || allDelete;
4880

4981
if (anyTableAccess && allRead && allAdd && allEdit && allDelete) {
50-
// All tables have full access — single wildcard per action
51-
items.push({ action: 'table:read', tableName: '*' });
52-
items.push({ action: 'table:add', tableName: '*' });
53-
items.push({ action: 'table:edit', tableName: '*' });
54-
items.push({ action: 'table:delete', tableName: '*' });
82+
items.push({ action: 'table:*', tableName: '*' });
5583
} else if (anyTableAccess && (allRead || allAdd || allEdit || allDelete)) {
5684
// Some actions apply to all tables, others are per-table
5785
if (allRead) items.push({ action: 'table:read', tableName: '*' });
@@ -68,10 +96,14 @@ export function permissionsToPolicyItems(permissions: Permissions): CedarPolicyI
6896
} else {
6997
for (const table of tables) {
7098
const access = table.accessLevel;
71-
if (access.visibility) items.push({ action: 'table:read', tableName: table.tableName });
72-
if (access.add) items.push({ action: 'table:add', tableName: table.tableName });
73-
if (access.edit) items.push({ action: 'table:edit', tableName: table.tableName });
74-
if (access.delete) items.push({ action: 'table:delete', tableName: table.tableName });
99+
if (access.visibility && access.add && access.edit && access.delete) {
100+
items.push({ action: 'table:*', tableName: table.tableName });
101+
} else {
102+
if (access.visibility) items.push({ action: 'table:read', tableName: table.tableName });
103+
if (access.add) items.push({ action: 'table:add', tableName: table.tableName });
104+
if (access.edit) items.push({ action: 'table:edit', tableName: table.tableName });
105+
if (access.delete) items.push({ action: 'table:delete', tableName: table.tableName });
106+
}
75107
}
76108
}
77109
}
@@ -88,6 +120,19 @@ export function policyItemsToCedarPolicy(items: CedarPolicyItem[], connectionId:
88120
return policies.join('\n\n');
89121
}
90122

123+
// table:* expands to 4 individual table action permits
124+
if (item.action === 'table:*') {
125+
const tableResource =
126+
item.tableName === '*'
127+
? `resource like RocketAdmin::Table::"${connectionId}/*"`
128+
: `resource == RocketAdmin::Table::"${connectionId}/${item.tableName}"`;
129+
for (const subAction of ['table:read', 'table:add', 'table:edit', 'table:delete']) {
130+
const ref = `RocketAdmin::Action::"${subAction}"`;
131+
policies.push(`permit(\n principal,\n action == ${ref},\n ${tableResource}\n);`);
132+
}
133+
continue;
134+
}
135+
91136
const actionRef = `RocketAdmin::Action::"${item.action}"`;
92137
let resource: string;
93138

@@ -151,6 +196,7 @@ export function policyItemsToPermissions(
151196
case 'group:edit':
152197
result.group.accessLevel = AccessLevel.Edit;
153198
break;
199+
case 'table:*':
154200
case 'table:read':
155201
case 'table:add':
156202
case 'table:edit':
@@ -161,6 +207,12 @@ export function policyItemsToPermissions(
161207
for (const table of targets) {
162208
table.accessLevel.visibility = true;
163209
switch (item.action) {
210+
case 'table:*':
211+
table.accessLevel.readonly = true;
212+
table.accessLevel.add = true;
213+
table.accessLevel.edit = true;
214+
table.accessLevel.delete = true;
215+
break;
164216
case 'table:read':
165217
table.accessLevel.readonly = true;
166218
break;

0 commit comments

Comments
 (0)