Skip to content

Commit 726e252

Browse files
Merge pull request #1667 from rocket-admin/cedar-policy-editor
Cedar policy editor
2 parents 4d4c1e5 + 83060fd commit 726e252

28 files changed

Lines changed: 1538 additions & 830 deletions

backend/src/entities/connection/connection.controller.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,7 @@ import { AmplitudeEventTypeEnum, InTransactionEnum } from '../../enums/index.js'
2222
import { Messages } from '../../exceptions/text/messages.js';
2323
import { processExceptionMessage } from '../../exceptions/utils/process-exception-message.js';
2424
import { ConnectionEditGuard, ConnectionReadGuard } from '../../guards/index.js';
25-
import {
26-
isConnectionTypeAgent,
27-
slackPostMessage,
28-
toPrettyErrorsMsg,
29-
} from '../../helpers/index.js';
25+
import { isConnectionTypeAgent, slackPostMessage, toPrettyErrorsMsg } from '../../helpers/index.js';
3026
import { SentryInterceptor } from '../../interceptors/index.js';
3127
import { SuccessResponse } from '../../microservices/saas-microservice/data-structures/common-responce.ds.js';
3228
import { AmplitudeService } from '../amplitude/amplitude.service.js';
@@ -689,5 +685,4 @@ export class ConnectionController {
689685
}
690686
return await this.unfreezeConnectionUseCase.execute({ connectionId, userId }, InTransactionEnum.ON);
691687
}
692-
693688
}

backend/src/entities/connection/use-cases/create-group-in-connection.use.case.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,11 @@ export class CreateGroupInConnectionUseCase
3636
const foundUser = await this._dbContext.userRepository.findOneUserById(cognitoUserName);
3737
const newGroupEntity = buildNewGroupEntityForConnectionWithUser(connectionToUpdate, foundUser, title);
3838
const savedGroup = await this._dbContext.groupRepository.saveNewOrUpdatedGroup(newGroupEntity);
39-
savedGroup.cedarPolicy = generateCedarPolicyForGroup(
40-
connectionId,
41-
false,
42-
{
43-
connection: { connectionId, accessLevel: AccessLevelEnum.none },
44-
group: { groupId: savedGroup.id, accessLevel: AccessLevelEnum.none },
45-
tables: [],
46-
},
47-
);
39+
savedGroup.cedarPolicy = generateCedarPolicyForGroup(connectionId, false, {
40+
connection: { connectionId, accessLevel: AccessLevelEnum.none },
41+
group: { groupId: savedGroup.id, accessLevel: AccessLevelEnum.none },
42+
tables: [],
43+
});
4844
await this._dbContext.groupRepository.saveNewOrUpdatedGroup(savedGroup);
4945
Cacher.invalidateCedarPolicyCache(connectionId);
5046
return buildFoundGroupResponseDto(savedGroup);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
.editor-mode-toggle {
2+
display: flex;
3+
align-items: center;
4+
justify-content: space-between;
5+
margin-bottom: 12px;
6+
}
7+
8+
.cedar-hint {
9+
margin: 0 0 8px;
10+
font-size: 12px;
11+
color: var(--mdc-theme-text-secondary-on-background, rgba(0, 0, 0, 0.54));
12+
}
13+
14+
.cedar-hint a {
15+
color: inherit;
16+
text-decoration: underline;
17+
}
18+
19+
.form-parse-warning {
20+
display: flex;
21+
align-items: center;
22+
gap: 8px;
23+
padding: 12px;
24+
margin-bottom: 12px;
25+
border-radius: 4px;
26+
background: var(--mdc-theme-warning-container, #fff3e0);
27+
color: var(--mdc-theme-on-warning-container, #e65100);
28+
font-size: 13px;
29+
}
30+
31+
.form-parse-warning mat-icon {
32+
flex-shrink: 0;
33+
}
34+
35+
.code-editor-box {
36+
height: 300px;
37+
border: 1px solid var(--mdc-outlined-text-field-outline-color, rgba(0, 0, 0, 0.38));
38+
border-radius: 4px;
39+
overflow: hidden;
40+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<h1 mat-dialog-title>Policy — {{ data.groupTitle }}</h1>
2+
<mat-dialog-content>
3+
<div class="editor-mode-toggle">
4+
<mat-button-toggle-group [value]="editorMode" (change)="onEditorModeChange($event.value)">
5+
<mat-button-toggle value="form">Form</mat-button-toggle>
6+
<mat-button-toggle value="code">Code</mat-button-toggle>
7+
</mat-button-toggle-group>
8+
</div>
9+
10+
<div *ngIf="formParseError" class="form-parse-warning">
11+
<mat-icon>warning</mat-icon>
12+
<span>This policy uses advanced Cedar syntax that cannot be represented in form mode. Please use the code editor.</span>
13+
</div>
14+
15+
<div *ngIf="editorMode === 'form' && !formParseError">
16+
<app-cedar-policy-list
17+
[policies]="policyItems"
18+
[availableTables]="availableTables"
19+
[availableDashboards]="availableDashboards"
20+
[loading]="loading"
21+
(policiesChange)="onPolicyItemsChange($event)">
22+
</app-cedar-policy-list>
23+
</div>
24+
25+
<div *ngIf="editorMode === 'code'">
26+
<p class="cedar-hint">Edit policy in <a href="https://www.cedarpolicy.com/en" target="_blank" rel="noopener">Cedar</a> format</p>
27+
<div class="code-editor-box">
28+
<ngs-code-editor
29+
[theme]="codeEditorTheme"
30+
[codeModel]="cedarPolicyModel"
31+
[options]="codeEditorOptions"
32+
(valueChanged)="onCedarPolicyChange($event)">
33+
</ngs-code-editor>
34+
</div>
35+
</div>
36+
</mat-dialog-content>
37+
<mat-dialog-actions align="end">
38+
<button type="button" mat-flat-button mat-dialog-close>Cancel</button>
39+
<button type="button" mat-flat-button color="primary"
40+
[disabled]="submitting"
41+
(click)="savePolicy()">
42+
Save
43+
</button>
44+
</mat-dialog-actions>
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { provideHttpClient } from '@angular/common/http';
2+
import { NO_ERRORS_SCHEMA, signal } from '@angular/core';
3+
import { ComponentFixture, TestBed } from '@angular/core/testing';
4+
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
5+
import { MatSnackBarModule } from '@angular/material/snack-bar';
6+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
7+
import { provideRouter } from '@angular/router';
8+
import { CodeEditorModule } from '@ngstack/code-editor';
9+
import { Angulartics2Module } from 'angulartics2';
10+
import { of } from 'rxjs';
11+
import { DashboardsService } from 'src/app/services/dashboards.service';
12+
import { TablesService } from 'src/app/services/tables.service';
13+
import { MockCodeEditorComponent } from 'src/app/testing/code-editor.mock';
14+
import { CedarPolicyEditorDialogComponent } from './cedar-policy-editor-dialog.component';
15+
16+
describe('CedarPolicyEditorDialogComponent', () => {
17+
let component: CedarPolicyEditorDialogComponent;
18+
let fixture: ComponentFixture<CedarPolicyEditorDialogComponent>;
19+
let tablesService: TablesService;
20+
let dashboardsService: Partial<DashboardsService>;
21+
22+
const mockDialogRef = {
23+
close: () => {},
24+
};
25+
26+
const fakeTables = [
27+
{
28+
table: 'customers',
29+
display_name: 'Customers',
30+
permissions: { visibility: true, readonly: false, add: true, delete: true, edit: true },
31+
},
32+
{
33+
table: 'orders',
34+
display_name: 'Orders',
35+
permissions: { visibility: true, readonly: false, add: true, delete: false, edit: true },
36+
},
37+
];
38+
39+
const cedarPolicyWithConnection = [
40+
'permit(',
41+
' principal,',
42+
' action == RocketAdmin::Action::"connection:read",',
43+
' resource == RocketAdmin::Connection::"conn-123"',
44+
');',
45+
].join('\n');
46+
47+
beforeEach(() => {
48+
dashboardsService = {
49+
dashboards: signal([
50+
{ id: 'dash-1', name: 'Sales', description: null, connection_id: 'conn-123', created_at: '', updated_at: '' },
51+
]).asReadonly(),
52+
setActiveConnection: vi.fn(),
53+
};
54+
55+
TestBed.configureTestingModule({
56+
imports: [
57+
MatDialogModule,
58+
MatSnackBarModule,
59+
BrowserAnimationsModule,
60+
Angulartics2Module.forRoot({}),
61+
CedarPolicyEditorDialogComponent,
62+
],
63+
providers: [
64+
provideHttpClient(),
65+
provideRouter([]),
66+
{
67+
provide: MAT_DIALOG_DATA,
68+
useValue: { groupId: 'group-123', groupTitle: 'Test Group', cedarPolicy: cedarPolicyWithConnection },
69+
},
70+
{ provide: MatDialogRef, useValue: mockDialogRef },
71+
{ provide: DashboardsService, useValue: dashboardsService },
72+
],
73+
})
74+
.overrideComponent(CedarPolicyEditorDialogComponent, {
75+
remove: { imports: [CodeEditorModule] },
76+
add: { imports: [MockCodeEditorComponent], schemas: [NO_ERRORS_SCHEMA] },
77+
})
78+
.compileComponents();
79+
80+
tablesService = TestBed.inject(TablesService);
81+
vi.spyOn(tablesService, 'fetchTables').mockReturnValue(of(fakeTables));
82+
83+
fixture = TestBed.createComponent(CedarPolicyEditorDialogComponent);
84+
component = fixture.componentInstance;
85+
fixture.detectChanges();
86+
});
87+
88+
it('should create', () => {
89+
expect(component).toBeTruthy();
90+
});
91+
92+
it('should load tables on init', () => {
93+
expect(tablesService.fetchTables).toHaveBeenCalled();
94+
expect(component.allTables.length).toBe(2);
95+
expect(component.availableTables.length).toBe(2);
96+
expect(component.loading).toBe(false);
97+
});
98+
99+
it('should pre-populate policy items from existing cedar policy', () => {
100+
expect(component.policyItems.length).toBeGreaterThan(0);
101+
expect(component.policyItems.some((item) => item.action === 'connection:read')).toBe(true);
102+
});
103+
104+
it('should start in form mode', () => {
105+
expect(component.editorMode).toBe('form');
106+
});
107+
108+
it('should switch to code mode', () => {
109+
component.onEditorModeChange('code');
110+
expect(component.editorMode).toBe('code');
111+
expect(component.cedarPolicy).toBeTruthy();
112+
});
113+
});

0 commit comments

Comments
 (0)