Skip to content

Commit 7b34ebe

Browse files
Merge branch 'main' into email-phone-filter-improvements
2 parents 4dfcb1e + 6d0dcee commit 7b34ebe

124 files changed

Lines changed: 5262 additions & 1656 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"@types/qrcode": "^1.5.6",
5757
"@zapier/secret-scrubber": "^1.1.6",
5858
"argon2": "0.44.0",
59-
"axios": "^1.13.6",
59+
"axios": "^1.15.0",
6060
"base32-encode": "^2.0.0",
6161
"basic-auth": "2.0.1",
6262
"bcrypt": "6.0.0",

backend/src/entities/ai/ai.service.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,12 +301,26 @@ IMPORTANT:
301301
const tableInfo = tablesInformation.find((t) => t.table_name === tableSettings.table_name);
302302
const validColumnNames = tableInfo?.structure.map((col) => col.column_name) || [];
303303

304+
const requiredFieldsWithoutDefault = new Set(
305+
tableInfo?.structure
306+
.filter((col) => !col.allow_null && col.column_default === null && !checkFieldAutoincrement(col.column_default, col.extra))
307+
.map((col) => col.column_name) || [],
308+
);
309+
304310
const settings = new TableSettingsEntity();
305311
settings.table_name = tableSettings.table_name;
306312
settings.display_name = tableSettings.display_name;
307313
settings.search_fields = this.filterValidColumns(tableSettings.search_fields, validColumnNames);
308-
settings.readonly_fields = this.filterValidColumns(tableSettings.readonly_fields, validColumnNames);
309-
settings.columns_view = this.filterValidColumns(tableSettings.columns_view, validColumnNames);
314+
settings.readonly_fields = this.filterValidColumns(tableSettings.readonly_fields, validColumnNames).filter(
315+
(field) => !requiredFieldsWithoutDefault.has(field),
316+
);
317+
const filteredColumnsView = this.filterValidColumns(tableSettings.columns_view, validColumnNames);
318+
for (const requiredField of requiredFieldsWithoutDefault) {
319+
if (!filteredColumnsView.includes(requiredField)) {
320+
filteredColumnsView.push(requiredField);
321+
}
322+
}
323+
settings.columns_view = filteredColumnsView;
310324
settings.ordering = this.mapOrdering(tableSettings.ordering);
311325
settings.ordering_field = validColumnNames.includes(tableSettings.ordering_field)
312326
? tableSettings.ordering_field

backend/src/entities/cedar-authorization/cedar-action-map.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ export enum CedarAction {
1111
DashboardCreate = 'dashboard:create',
1212
DashboardEdit = 'dashboard:edit',
1313
DashboardDelete = 'dashboard:delete',
14+
PanelRead = 'panel:read',
15+
PanelCreate = 'panel:create',
16+
PanelEdit = 'panel:edit',
17+
PanelDelete = 'panel:delete',
1418
}
1519

1620
export enum CedarResourceType {
1721
Connection = 'RocketAdmin::Connection',
1822
Group = 'RocketAdmin::Group',
1923
Table = 'RocketAdmin::Table',
2024
Dashboard = 'RocketAdmin::Dashboard',
25+
Panel = 'RocketAdmin::Panel',
2126
}
2227

2328
export const CEDAR_ACTION_TYPE = 'RocketAdmin::Action';
@@ -31,4 +36,5 @@ export interface CedarValidationRequest {
3136
groupId?: string;
3237
tableName?: string;
3338
dashboardId?: string;
39+
panelId?: string;
3440
}

backend/src/entities/cedar-authorization/cedar-authorization.service.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On
3434
}
3535

3636
async validate(request: CedarValidationRequest): Promise<boolean> {
37-
const { userId, action, groupId, tableName, dashboardId } = request;
37+
const { userId, action, groupId, tableName, dashboardId, panelId } = request;
3838
let { connectionId } = request;
3939

4040
const actionPrefix = action.split(':')[0];
@@ -61,13 +61,20 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On
6161
const needsSentinel = action === CedarAction.DashboardCreate || !dashboardId;
6262
const effectiveDashboardId = needsSentinel ? '__new__' : dashboardId;
6363
resourceId = `${connectionId}/${effectiveDashboardId}`;
64-
return this.evaluate(userId, connectionId, action, resourceType, resourceId, tableName, effectiveDashboardId);
64+
return this.evaluate(userId, connectionId, action, resourceType, resourceId, tableName, effectiveDashboardId, undefined);
65+
}
66+
case 'panel': {
67+
resourceType = CedarResourceType.Panel;
68+
const needsSentinel = action === CedarAction.PanelCreate || !panelId;
69+
const effectivePanelId = needsSentinel ? '__new__' : panelId;
70+
resourceId = `${connectionId}/${effectivePanelId}`;
71+
return this.evaluate(userId, connectionId, action, resourceType, resourceId, tableName, undefined, effectivePanelId);
6572
}
6673
default:
6774
return false;
6875
}
6976

70-
return this.evaluate(userId, connectionId, action, resourceType, resourceId, tableName, dashboardId);
77+
return this.evaluate(userId, connectionId, action, resourceType, resourceId, tableName, dashboardId, undefined);
7178
}
7279

7380
invalidatePolicyCacheForConnection(connectionId: string): void {
@@ -169,6 +176,7 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On
169176
resourceId: string,
170177
tableName?: string,
171178
dashboardId?: string,
179+
panelId?: string,
172180
): Promise<boolean> {
173181
await this.assertUserNotSuspended(userId);
174182

@@ -178,7 +186,7 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On
178186
const groupPolicies = this.loadPoliciesPerGroup(userGroups);
179187
if (groupPolicies.length === 0) return false;
180188

181-
const entities = buildCedarEntities(userId, userGroups, connectionId, tableName, dashboardId);
189+
const entities = buildCedarEntities(userId, userGroups, connectionId, tableName, dashboardId, panelId);
182190

183191
for (const policy of groupPolicies) {
184192
const call = {
@@ -303,6 +311,19 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On
303311
);
304312
}
305313
}
314+
315+
const panelResourceIds = [...cedarPolicy.matchAll(/resource\s*==\s*RocketAdmin::Panel::"([^"]+)"/g)].map(
316+
(m) => m[1],
317+
);
318+
319+
for (const panelRef of panelResourceIds) {
320+
if (!panelRef.startsWith(`${connectionId}/`)) {
321+
throw new HttpException(
322+
{ message: Messages.CEDAR_POLICY_REFERENCES_FOREIGN_CONNECTION },
323+
HttpStatus.BAD_REQUEST,
324+
);
325+
}
326+
}
306327
}
307328

308329
}

backend/src/entities/cedar-authorization/cedar-entity-builder.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function buildCedarEntities(
1212
connectionId: string,
1313
tableName?: string,
1414
dashboardId?: string,
15+
panelId?: string,
1516
): Array<CedarEntityRecord> {
1617
const entities: Array<CedarEntityRecord> = [];
1718

@@ -58,5 +59,13 @@ export function buildCedarEntities(
5859
});
5960
}
6061

62+
if (panelId) {
63+
entities.push({
64+
uid: { type: 'RocketAdmin::Panel', id: `${connectionId}/${panelId}` },
65+
attrs: { connectionId: connectionId },
66+
parents: [{ type: 'RocketAdmin::Connection', id: connectionId }],
67+
});
68+
}
69+
6170
return entities;
6271
}

backend/src/entities/cedar-authorization/cedar-permissions.service.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
1+
import * as cedarWasm from '@cedar-policy/cedar-wasm/nodejs';
12
import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';
3+
import { IGlobalDatabaseContext } from '../../common/application/global-database-context.interface.js';
4+
import { BaseType } from '../../common/data-injection.tokens.js';
25
import { AccessLevelEnum } from '../../enums/index.js';
36
import { Messages } from '../../exceptions/text/messages.js';
47
import { Cacher } from '../../helpers/cache/cacher.js';
5-
import { IGlobalDatabaseContext } from '../../common/application/global-database-context.interface.js';
6-
import { BaseType } from '../../common/data-injection.tokens.js';
78
import { GroupEntity } from '../group/group.entity.js';
89
import { ITablePermissionData } from '../permission/permission.interface.js';
9-
import { CedarAction, CedarResourceType, CEDAR_ACTION_TYPE, CEDAR_USER_TYPE } from './cedar-action-map.js';
10+
import { IUserAccessRepository } from '../user-access/repository/user-access.repository.interface.js';
11+
import { CEDAR_ACTION_TYPE, CEDAR_USER_TYPE, CedarAction, CedarResourceType } from './cedar-action-map.js';
1012
import { buildCedarEntities } from './cedar-entity-builder.js';
1113
import { CEDAR_SCHEMA } from './cedar-schema.js';
12-
import * as cedarWasm from '@cedar-policy/cedar-wasm/nodejs';
13-
import { IUserAccessRepository } from '../user-access/repository/user-access.repository.interface.js';
1414

1515
interface EvalContext {
1616
userGroups: Array<GroupEntity>;
@@ -435,5 +435,4 @@ export class CedarPermissionsService implements IUserAccessRepository {
435435

436436
return { userGroups, policies };
437437
}
438-
439438
}

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,46 @@ export function generateCedarPolicyForGroup(
8787
}
8888
}
8989

90+
if (permissions.panels) {
91+
let hasPanelCreatePermission = false;
92+
let hasPanelReadPermission = false;
93+
for (const panel of permissions.panels) {
94+
const panelRef = `RocketAdmin::Panel::"${connectionId}/${panel.panelId}"`;
95+
const access = panel.accessLevel;
96+
97+
if (access.read) {
98+
hasPanelReadPermission = true;
99+
policies.push(
100+
`permit(\n principal,\n action == RocketAdmin::Action::"panel:read",\n resource == ${panelRef}\n);`,
101+
);
102+
}
103+
if (access.create) {
104+
hasPanelCreatePermission = true;
105+
}
106+
if (access.edit) {
107+
policies.push(
108+
`permit(\n principal,\n action == RocketAdmin::Action::"panel:edit",\n resource == ${panelRef}\n);`,
109+
);
110+
}
111+
if (access.delete) {
112+
policies.push(
113+
`permit(\n principal,\n action == RocketAdmin::Action::"panel:delete",\n resource == ${panelRef}\n);`,
114+
);
115+
}
116+
}
117+
const newPanelRef = `RocketAdmin::Panel::"${connectionId}/__new__"`;
118+
if (hasPanelReadPermission) {
119+
policies.push(
120+
`permit(\n principal,\n action == RocketAdmin::Action::"panel:read",\n resource == ${newPanelRef}\n);`,
121+
);
122+
}
123+
if (hasPanelCreatePermission) {
124+
policies.push(
125+
`permit(\n principal,\n action == RocketAdmin::Action::"panel:create",\n resource == ${newPanelRef}\n);`,
126+
);
127+
}
128+
}
129+
90130
for (const table of permissions.tables) {
91131
const tableRef = `RocketAdmin::Table::"${connectionId}/${table.tableName}"`;
92132
const access = table.accessLevel;

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AccessLevelEnum } from '../../enums/index.js';
22
import {
33
IComplexPermission,
44
IDashboardPermissionData,
5+
IPanelPermissionData,
56
ITablePermissionData,
67
} from '../permission/permission.interface.js';
78

@@ -24,10 +25,12 @@ export function parseCedarPolicyToClassicalPermissions(
2425
group: { groupId, accessLevel: AccessLevelEnum.none },
2526
tables: [],
2627
dashboards: [],
28+
panels: [],
2729
};
2830

2931
const tableMap = new Map<string, ITablePermissionData>();
3032
const dashboardMap = new Map<string, IDashboardPermissionData>();
33+
const panelMap = new Map<string, IPanelPermissionData>();
3134

3235
for (const permit of permits) {
3336
if (permit.isWildcard) {
@@ -75,6 +78,16 @@ export function parseCedarPolicyToClassicalPermissions(
7578
applyDashboardAction(dashboardEntry, permit.action);
7679
break;
7780
}
81+
case 'panel:read':
82+
case 'panel:create':
83+
case 'panel:edit':
84+
case 'panel:delete': {
85+
const panelId = extractPanelId(permit.resourceId, connectionId);
86+
if (!panelId) break;
87+
const panelEntry = getOrCreatePanelEntry(panelMap, panelId);
88+
applyPanelAction(panelEntry, permit.action);
89+
break;
90+
}
7891
}
7992
}
8093

@@ -84,6 +97,7 @@ export function parseCedarPolicyToClassicalPermissions(
8497
a.readonly = a.visibility && !a.add && !a.edit && !a.delete;
8598
}
8699
result.dashboards = Array.from(dashboardMap.values());
100+
result.panels = Array.from(panelMap.values());
87101

88102
return result;
89103
}
@@ -268,3 +282,49 @@ function applyDashboardAction(entry: IDashboardPermissionData, action: string):
268282
break;
269283
}
270284
}
285+
286+
function extractPanelId(resourceId: string | null, connectionId: string): string | null {
287+
if (!resourceId) return null;
288+
const prefix = `${connectionId}/`;
289+
if (resourceId.startsWith(prefix)) {
290+
return resourceId.slice(prefix.length);
291+
}
292+
return resourceId;
293+
}
294+
295+
function getOrCreatePanelEntry(
296+
map: Map<string, IPanelPermissionData>,
297+
panelId: string,
298+
): IPanelPermissionData {
299+
let entry = map.get(panelId);
300+
if (!entry) {
301+
entry = {
302+
panelId,
303+
accessLevel: {
304+
read: false,
305+
create: false,
306+
edit: false,
307+
delete: false,
308+
},
309+
};
310+
map.set(panelId, entry);
311+
}
312+
return entry;
313+
}
314+
315+
function applyPanelAction(entry: IPanelPermissionData, action: string): void {
316+
switch (action) {
317+
case 'panel:read':
318+
entry.accessLevel.read = true;
319+
break;
320+
case 'panel:create':
321+
entry.accessLevel.create = true;
322+
break;
323+
case 'panel:edit':
324+
entry.accessLevel.edit = true;
325+
break;
326+
case 'panel:delete':
327+
entry.accessLevel.delete = true;
328+
break;
329+
}
330+
}

backend/src/entities/cedar-authorization/cedar-schema.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,15 @@ export const CEDAR_SCHEMA = {
4545
},
4646
},
4747
},
48+
Panel: {
49+
memberOfTypes: ['Connection'],
50+
shape: {
51+
type: 'Record',
52+
attributes: {
53+
connectionId: { type: 'String' },
54+
},
55+
},
56+
},
4857
},
4958
actions: {
5059
'connection:read': {
@@ -119,6 +128,30 @@ export const CEDAR_SCHEMA = {
119128
resourceTypes: ['Dashboard'],
120129
},
121130
},
131+
'panel:read': {
132+
appliesTo: {
133+
principalTypes: ['User'],
134+
resourceTypes: ['Panel'],
135+
},
136+
},
137+
'panel:create': {
138+
appliesTo: {
139+
principalTypes: ['User'],
140+
resourceTypes: ['Panel'],
141+
},
142+
},
143+
'panel:edit': {
144+
appliesTo: {
145+
principalTypes: ['User'],
146+
resourceTypes: ['Panel'],
147+
},
148+
},
149+
'panel:delete': {
150+
appliesTo: {
151+
principalTypes: ['User'],
152+
resourceTypes: ['Panel'],
153+
},
154+
},
122155
},
123156
},
124157
};

backend/src/entities/connection/connection.entity.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js';
12
import { Expose } from 'class-transformer';
23
import { nanoid } from 'nanoid';
34
import {
@@ -12,7 +13,6 @@ import {
1213
PrimaryColumn,
1314
Relation,
1415
} from 'typeorm';
15-
import { ConnectionTypesEnum } from '@rocketadmin/shared-code/dist/src/shared/enums/connection-types-enum.js';
1616
import { Encryptor } from '../../helpers/encryption/encryptor.js';
1717
import { isConnectionTypeAgent } from '../../helpers/index.js';
1818
import { AgentEntity } from '../agent/agent.entity.js';

0 commit comments

Comments
 (0)