Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6419ea3
Add Cedar policy editor to group creation and edit dialogs
gugu Mar 13, 2026
5849a54
Replace permissions form with list-based Cedar policy editor
gugu Mar 13, 2026
7ddf75d
Add "All tables" wildcard option and generate cedar policy directly f…
gugu Mar 13, 2026
991759e
Use like operator for wildcard table resource in cedar policy
gugu Mar 14, 2026
66f35c2
Add optgroups for actions and table:* (all table actions) option
gugu Mar 14, 2026
451b572
Rename table full access label and use action like for table:*
gugu Mar 14, 2026
418d633
Add group prefix to policy action labels for clarity
gugu Mar 14, 2026
d939e07
Merge branch 'main' into cedar-policy-editor
gugu Mar 16, 2026
9aa4182
Merge branch 'main' into cedar-policy-editor
gugu Mar 17, 2026
1ff63a2
Merge branch 'main' into cedar-policy-editor
gugu Mar 17, 2026
d024506
Merge branch 'main' into cedar-policy-editor
gugu Mar 17, 2026
0bbe8a2
refactor: separate cedar policy editor into dedicated dialog using ne…
gugu Mar 17, 2026
ae49d0b
feat: add dashboard permissions to policy form and improve editor UX
gugu Mar 17, 2026
bffa5cf
revert: remove all backend changes from cedar-policy-editor branch
gugu Mar 17, 2026
e8cbf68
feat: add dashboard:* full access action and use "All dashboards" wil…
gugu Mar 17, 2026
9e18c17
fix: replace remaining "New dashboards" with "All dashboards" in add …
gugu Mar 17, 2026
b6c4670
refactor: simplify cedar policy code — remove dead code, fix leaks, d…
gugu Mar 17, 2026
f17ecba
fix: open cedar policy form after group creation and refresh data aft…
gugu Mar 18, 2026
67e0df3
fix: show warning instead of form when cedar policy uses unsupported …
gugu Mar 18, 2026
83060fd
Merge branch 'main' into cedar-policy-editor
lyubov-voloshko Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions backend/src/entities/connection/connection.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,7 @@ import { AmplitudeEventTypeEnum, InTransactionEnum } from '../../enums/index.js'
import { Messages } from '../../exceptions/text/messages.js';
import { processExceptionMessage } from '../../exceptions/utils/process-exception-message.js';
import { ConnectionEditGuard, ConnectionReadGuard } from '../../guards/index.js';
import {
isConnectionTypeAgent,
slackPostMessage,
toPrettyErrorsMsg,
} from '../../helpers/index.js';
import { isConnectionTypeAgent, slackPostMessage, toPrettyErrorsMsg } from '../../helpers/index.js';
import { SentryInterceptor } from '../../interceptors/index.js';
import { SuccessResponse } from '../../microservices/saas-microservice/data-structures/common-responce.ds.js';
import { AmplitudeService } from '../amplitude/amplitude.service.js';
Expand Down Expand Up @@ -689,5 +685,4 @@ export class ConnectionController {
}
return await this.unfreezeConnectionUseCase.execute({ connectionId, userId }, InTransactionEnum.ON);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,11 @@ export class CreateGroupInConnectionUseCase
const foundUser = await this._dbContext.userRepository.findOneUserById(cognitoUserName);
const newGroupEntity = buildNewGroupEntityForConnectionWithUser(connectionToUpdate, foundUser, title);
const savedGroup = await this._dbContext.groupRepository.saveNewOrUpdatedGroup(newGroupEntity);
savedGroup.cedarPolicy = generateCedarPolicyForGroup(
connectionId,
false,
{
connection: { connectionId, accessLevel: AccessLevelEnum.none },
group: { groupId: savedGroup.id, accessLevel: AccessLevelEnum.none },
tables: [],
},
);
savedGroup.cedarPolicy = generateCedarPolicyForGroup(connectionId, false, {
connection: { connectionId, accessLevel: AccessLevelEnum.none },
group: { groupId: savedGroup.id, accessLevel: AccessLevelEnum.none },
tables: [],
});
await this._dbContext.groupRepository.saveNewOrUpdatedGroup(savedGroup);
Cacher.invalidateCedarPolicyCache(connectionId);
return buildFoundGroupResponseDto(savedGroup);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.editor-mode-toggle {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}

.cedar-hint {
margin: 0 0 8px;
font-size: 12px;
color: var(--mdc-theme-text-secondary-on-background, rgba(0, 0, 0, 0.54));
}

.cedar-hint a {
color: inherit;
text-decoration: underline;
}

.form-parse-warning {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
margin-bottom: 12px;
border-radius: 4px;
background: var(--mdc-theme-warning-container, #fff3e0);
color: var(--mdc-theme-on-warning-container, #e65100);
font-size: 13px;
}

.form-parse-warning mat-icon {
flex-shrink: 0;
}

.code-editor-box {
height: 300px;
border: 1px solid var(--mdc-outlined-text-field-outline-color, rgba(0, 0, 0, 0.38));
border-radius: 4px;
overflow: hidden;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<h1 mat-dialog-title>Policy — {{ data.groupTitle }}</h1>
<mat-dialog-content>
<div class="editor-mode-toggle">
<mat-button-toggle-group [value]="editorMode" (change)="onEditorModeChange($event.value)">
<mat-button-toggle value="form">Form</mat-button-toggle>
<mat-button-toggle value="code">Code</mat-button-toggle>
</mat-button-toggle-group>
</div>

<div *ngIf="formParseError" class="form-parse-warning">
<mat-icon>warning</mat-icon>
<span>This policy uses advanced Cedar syntax that cannot be represented in form mode. Please use the code editor.</span>
</div>

<div *ngIf="editorMode === 'form' && !formParseError">
<app-cedar-policy-list
[policies]="policyItems"
[availableTables]="availableTables"
[availableDashboards]="availableDashboards"
[loading]="loading"
(policiesChange)="onPolicyItemsChange($event)">
</app-cedar-policy-list>
</div>

<div *ngIf="editorMode === 'code'">
<p class="cedar-hint">Edit policy in <a href="https://www.cedarpolicy.com/en" target="_blank" rel="noopener">Cedar</a> format</p>
<div class="code-editor-box">
<ngs-code-editor
[theme]="codeEditorTheme"
[codeModel]="cedarPolicyModel"
[options]="codeEditorOptions"
(valueChanged)="onCedarPolicyChange($event)">
</ngs-code-editor>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button type="button" mat-flat-button mat-dialog-close>Cancel</button>
<button type="button" mat-flat-button color="primary"
[disabled]="submitting"
(click)="savePolicy()">
Save
</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { provideHttpClient } from '@angular/common/http';
import { NO_ERRORS_SCHEMA, signal } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { provideRouter } from '@angular/router';
import { CodeEditorModule } from '@ngstack/code-editor';
import { Angulartics2Module } from 'angulartics2';
import { of } from 'rxjs';
import { DashboardsService } from 'src/app/services/dashboards.service';
import { TablesService } from 'src/app/services/tables.service';
import { MockCodeEditorComponent } from 'src/app/testing/code-editor.mock';
import { CedarPolicyEditorDialogComponent } from './cedar-policy-editor-dialog.component';

describe('CedarPolicyEditorDialogComponent', () => {
let component: CedarPolicyEditorDialogComponent;
let fixture: ComponentFixture<CedarPolicyEditorDialogComponent>;
let tablesService: TablesService;
let dashboardsService: Partial<DashboardsService>;

const mockDialogRef = {
close: () => {},
};

const fakeTables = [
{
table: 'customers',
display_name: 'Customers',
permissions: { visibility: true, readonly: false, add: true, delete: true, edit: true },
},
{
table: 'orders',
display_name: 'Orders',
permissions: { visibility: true, readonly: false, add: true, delete: false, edit: true },
},
];

const cedarPolicyWithConnection = [
'permit(',
' principal,',
' action == RocketAdmin::Action::"connection:read",',
' resource == RocketAdmin::Connection::"conn-123"',
');',
].join('\n');

beforeEach(() => {
dashboardsService = {
dashboards: signal([
{ id: 'dash-1', name: 'Sales', description: null, connection_id: 'conn-123', created_at: '', updated_at: '' },
]).asReadonly(),
setActiveConnection: vi.fn(),
};

TestBed.configureTestingModule({
imports: [
MatDialogModule,
MatSnackBarModule,
BrowserAnimationsModule,
Angulartics2Module.forRoot({}),
CedarPolicyEditorDialogComponent,
],
providers: [
provideHttpClient(),
provideRouter([]),
{
provide: MAT_DIALOG_DATA,
useValue: { groupId: 'group-123', groupTitle: 'Test Group', cedarPolicy: cedarPolicyWithConnection },
},
{ provide: MatDialogRef, useValue: mockDialogRef },
{ provide: DashboardsService, useValue: dashboardsService },
],
})
.overrideComponent(CedarPolicyEditorDialogComponent, {
remove: { imports: [CodeEditorModule] },
add: { imports: [MockCodeEditorComponent], schemas: [NO_ERRORS_SCHEMA] },
})
.compileComponents();

tablesService = TestBed.inject(TablesService);
vi.spyOn(tablesService, 'fetchTables').mockReturnValue(of(fakeTables));

fixture = TestBed.createComponent(CedarPolicyEditorDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should load tables on init', () => {
expect(tablesService.fetchTables).toHaveBeenCalled();
expect(component.allTables.length).toBe(2);
expect(component.availableTables.length).toBe(2);
expect(component.loading).toBe(false);
});

it('should pre-populate policy items from existing cedar policy', () => {
expect(component.policyItems.length).toBeGreaterThan(0);
expect(component.policyItems.some((item) => item.action === 'connection:read')).toBe(true);
});

it('should start in form mode', () => {
expect(component.editorMode).toBe('form');
});

it('should switch to code mode', () => {
component.onEditorModeChange('code');
expect(component.editorMode).toBe('code');
expect(component.cedarPolicy).toBeTruthy();
});
});
Loading
Loading