Skip to content

Commit db43a36

Browse files
authored
Merge pull request #1660 from rocket-admin/backend_ceadr_bare_principal
Refactor Cedar policy tests to remove group principal references
2 parents 772a995 + 9b30f7d commit db43a36

15 files changed

Lines changed: 148 additions & 262 deletions

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

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On
107107
throw new HttpException({ message: Messages.CANNOT_CHANGE_ADMIN_GROUP }, HttpStatus.BAD_REQUEST);
108108
}
109109

110-
await this.validatePolicyReferences(cedarPolicy, connectionId, groupId);
110+
await this.validatePolicyReferences(cedarPolicy, connectionId);
111111

112112
const classicalPermissions = parseCedarPolicyToClassicalPermissions(cedarPolicy, connectionId, groupId);
113113

@@ -186,7 +186,8 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On
186186
const userGroups = await this.globalDbContext.groupRepository.findAllUserGroupsInConnection(connectionId, userId);
187187
if (userGroups.length === 0) return false;
188188

189-
const policies = await this.loadPoliciesForConnection(connectionId);
189+
const userGroupIds = userGroups.map((g) => g.id);
190+
const policies = await this.loadPoliciesForUser(connectionId, userId, userGroupIds);
190191
if (!policies) return false;
191192

192193
const entities = buildCedarEntities(userId, userGroups, connectionId, tableName, dashboardId);
@@ -210,17 +211,21 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On
210211
return false;
211212
}
212213

213-
private async loadPoliciesForConnection(connectionId: string): Promise<string | null> {
214-
const cached = Cacher.getCedarPolicyCache(connectionId);
214+
private async loadPoliciesForUser(connectionId: string, userId: string, userGroupIds: string[]): Promise<string | null> {
215+
const cached = Cacher.getCedarPolicyCache(connectionId, userId);
215216
if (cached !== null) return cached;
216217

217218
const groups = await this.globalDbContext.groupRepository.findAllGroupsInConnection(connectionId);
218-
const policyTexts = groups.map((g) => g.cedarPolicy).filter(Boolean);
219+
const userGroupIdSet = new Set(userGroupIds);
220+
const policyTexts = groups
221+
.filter((g) => userGroupIdSet.has(g.id))
222+
.map((g) => g.cedarPolicy)
223+
.filter(Boolean);
219224

220225
if (policyTexts.length === 0) return null;
221226

222227
const combined = policyTexts.join('\n\n');
223-
Cacher.setCedarPolicyCache(connectionId, combined);
228+
Cacher.setCedarPolicyCache(connectionId, userId, combined);
224229
return combined;
225230
}
226231

@@ -268,22 +273,7 @@ export class CedarAuthorizationService implements ICedarAuthorizationService, On
268273
private async validatePolicyReferences(
269274
cedarPolicy: string,
270275
connectionId: string,
271-
groupId: string,
272276
): Promise<void> {
273-
274-
const principalGroupIds = [
275-
...cedarPolicy.matchAll(/principal\s+in\s+RocketAdmin::Group::"([^"]+)"/g),
276-
].map((m) => m[1]);
277-
278-
for (const principalGroupId of principalGroupIds) {
279-
if (principalGroupId !== groupId) {
280-
throw new HttpException(
281-
{ message: Messages.CEDAR_POLICY_REFERENCES_FOREIGN_PRINCIPAL },
282-
HttpStatus.BAD_REQUEST,
283-
);
284-
}
285-
}
286-
287277
const connectionIds = [
288278
...cedarPolicy.matchAll(/resource\s*==\s*RocketAdmin::Connection::"([^"]+)"/g),
289279
].map((m) => m[1]);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export function buildCedarEntities(
1919
entities.push({
2020
uid: { type: 'RocketAdmin::User', id: userId },
2121
attrs: { suspended: false },
22-
parents: userGroups.map((g) => ({ type: 'RocketAdmin::Group', id: g.id })),
22+
parents: [],
2323
});
2424

2525
// Group entities

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

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,16 @@ import { AccessLevelEnum } from '../../enums/index.js';
22
import { IComplexPermission } from '../permission/permission.interface.js';
33

44
export function generateCedarPolicyForGroup(
5-
groupId: string,
65
connectionId: string,
76
isMain: boolean,
87
permissions: IComplexPermission,
98
): string {
109
const policies: Array<string> = [];
11-
const groupRef = `RocketAdmin::Group::"${groupId}"`;
1210
const connectionRef = `RocketAdmin::Connection::"${connectionId}"`;
1311

1412
if (isMain) {
1513
policies.push(
16-
`permit(\n principal in ${groupRef},\n action,\n resource\n);`,
14+
`permit(\n principal,\n action,\n resource\n);`,
1715
);
1816
return policies.join('\n\n');
1917
}
@@ -22,14 +20,14 @@ export function generateCedarPolicyForGroup(
2220
const connAccess = permissions.connection.accessLevel;
2321
if (connAccess === AccessLevelEnum.edit) {
2422
policies.push(
25-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"connection:read",\n resource == ${connectionRef}\n);`,
23+
`permit(\n principal,\n action == RocketAdmin::Action::"connection:read",\n resource == ${connectionRef}\n);`,
2624
);
2725
policies.push(
28-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"connection:edit",\n resource == ${connectionRef}\n);`,
26+
`permit(\n principal,\n action == RocketAdmin::Action::"connection:edit",\n resource == ${connectionRef}\n);`,
2927
);
3028
} else if (connAccess === AccessLevelEnum.readonly) {
3129
policies.push(
32-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"connection:read",\n resource == ${connectionRef}\n);`,
30+
`permit(\n principal,\n action == RocketAdmin::Action::"connection:read",\n resource == ${connectionRef}\n);`,
3331
);
3432
}
3533

@@ -38,14 +36,14 @@ export function generateCedarPolicyForGroup(
3836
const groupResourceRef = `RocketAdmin::Group::"${permissions.group.groupId}"`;
3937
if (groupAccess === AccessLevelEnum.edit) {
4038
policies.push(
41-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"group:read",\n resource == ${groupResourceRef}\n);`,
39+
`permit(\n principal,\n action == RocketAdmin::Action::"group:read",\n resource == ${groupResourceRef}\n);`,
4240
);
4341
policies.push(
44-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"group:edit",\n resource == ${groupResourceRef}\n);`,
42+
`permit(\n principal,\n action == RocketAdmin::Action::"group:edit",\n resource == ${groupResourceRef}\n);`,
4543
);
4644
} else if (groupAccess === AccessLevelEnum.readonly) {
4745
policies.push(
48-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"group:read",\n resource == ${groupResourceRef}\n);`,
46+
`permit(\n principal,\n action == RocketAdmin::Action::"group:read",\n resource == ${groupResourceRef}\n);`,
4947
);
5048
}
5149

@@ -59,32 +57,32 @@ export function generateCedarPolicyForGroup(
5957
if (access.read) {
6058
hasReadPermission = true;
6159
policies.push(
62-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"dashboard:read",\n resource == ${dashboardRef}\n);`,
60+
`permit(\n principal,\n action == RocketAdmin::Action::"dashboard:read",\n resource == ${dashboardRef}\n);`,
6361
);
6462
}
6563
if (access.create) {
6664
hasCreatePermission = true;
6765
}
6866
if (access.edit) {
6967
policies.push(
70-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"dashboard:edit",\n resource == ${dashboardRef}\n);`,
68+
`permit(\n principal,\n action == RocketAdmin::Action::"dashboard:edit",\n resource == ${dashboardRef}\n);`,
7169
);
7270
}
7371
if (access.delete) {
7472
policies.push(
75-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"dashboard:delete",\n resource == ${dashboardRef}\n);`,
73+
`permit(\n principal,\n action == RocketAdmin::Action::"dashboard:delete",\n resource == ${dashboardRef}\n);`,
7674
);
7775
}
7876
}
7977
const newDashboardRef = `RocketAdmin::Dashboard::"${connectionId}/__new__"`;
8078
if (hasReadPermission) {
8179
policies.push(
82-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"dashboard:read",\n resource == ${newDashboardRef}\n);`,
80+
`permit(\n principal,\n action == RocketAdmin::Action::"dashboard:read",\n resource == ${newDashboardRef}\n);`,
8381
);
8482
}
8583
if (hasCreatePermission) {
8684
policies.push(
87-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"dashboard:create",\n resource == ${newDashboardRef}\n);`,
85+
`permit(\n principal,\n action == RocketAdmin::Action::"dashboard:create",\n resource == ${newDashboardRef}\n);`,
8886
);
8987
}
9088
}
@@ -96,22 +94,22 @@ export function generateCedarPolicyForGroup(
9694
const hasAnyAccess = access.visibility || access.add || access.delete || access.edit;
9795
if (hasAnyAccess) {
9896
policies.push(
99-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"table:read",\n resource == ${tableRef}\n);`,
97+
`permit(\n principal,\n action == RocketAdmin::Action::"table:read",\n resource == ${tableRef}\n);`,
10098
);
10199
}
102100
if (access.add) {
103101
policies.push(
104-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"table:add",\n resource == ${tableRef}\n);`,
102+
`permit(\n principal,\n action == RocketAdmin::Action::"table:add",\n resource == ${tableRef}\n);`,
105103
);
106104
}
107105
if (access.edit) {
108106
policies.push(
109-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"table:edit",\n resource == ${tableRef}\n);`,
107+
`permit(\n principal,\n action == RocketAdmin::Action::"table:edit",\n resource == ${tableRef}\n);`,
110108
);
111109
}
112110
if (access.delete) {
113111
policies.push(
114-
`permit(\n principal in ${groupRef},\n action == RocketAdmin::Action::"table:delete",\n resource == ${tableRef}\n);`,
112+
`permit(\n principal,\n action == RocketAdmin::Action::"table:delete",\n resource == ${tableRef}\n);`,
115113
);
116114
}
117115
}

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

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import {
66
} from '../permission/permission.interface.js';
77

88
interface ParsedPermitStatement {
9-
groupId: string | null;
109
action: string | null;
1110
resourceType: string | null;
1211
resourceId: string | null;
@@ -140,18 +139,12 @@ function extractPermitStatements(policyText: string): ParsedPermitStatement[] {
140139

141140
function parsePermitBody(body: string): ParsedPermitStatement {
142141
const result: ParsedPermitStatement = {
143-
groupId: null,
144142
action: null,
145143
resourceType: null,
146144
resourceId: null,
147145
isWildcard: false,
148146
};
149147

150-
const principalMatch = body.match(/principal\s+in\s+RocketAdmin::Group::"([^"]+)"/);
151-
if (principalMatch) {
152-
result.groupId = principalMatch[1];
153-
}
154-
155148
const actionMatch = body.match(/action\s*==\s*RocketAdmin::Action::"([^"]+)"/);
156149
if (actionMatch) {
157150
result.action = actionMatch[1];

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const CEDAR_SCHEMA = {
22
RocketAdmin: {
33
entityTypes: {
44
User: {
5-
memberOfTypes: ['Group'],
5+
memberOfTypes: [],
66
shape: {
77
type: 'Record',
88
attributes: {

backend/src/entities/cedar-authorization/scripts/migrate-permissions-to-cedar.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export async function migratePermissionsToCedar(dataSource: DataSource): Promise
5050
tables: Array.from(tableMap.values()),
5151
};
5252

53-
const cedarPolicy = generateCedarPolicyForGroup(group.id, connection.id, group.isMain, complexPermission);
53+
const cedarPolicy = generateCedarPolicyForGroup(connection.id, group.isMain, complexPermission);
5454
group.cedarPolicy = cedarPolicy;
5555
await groupRepository.save(group);
5656
migratedCount++;

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@ export class CreateConnectionUseCase
101101
);
102102
await this._dbContext.permissionRepository.createdDefaultAdminPermissionsInGroup(createdAdminGroup);
103103
createdAdminGroup.cedarPolicy = generateCedarPolicyForGroup(
104-
createdAdminGroup.id,
105104
savedConnection.id,
106105
true,
107106
{

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export class CreateGroupInConnectionUseCase
3737
const newGroupEntity = buildNewGroupEntityForConnectionWithUser(connectionToUpdate, foundUser, title);
3838
const savedGroup = await this._dbContext.groupRepository.saveNewOrUpdatedGroup(newGroupEntity);
3939
savedGroup.cedarPolicy = generateCedarPolicyForGroup(
40-
savedGroup.id,
4140
connectionId,
4241
false,
4342
{

backend/src/entities/permission/use-cases/create-or-update-permissions.use.case.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export class CreateOrUpdatePermissionsUseCase
189189
);
190190

191191
// Generate and save Cedar policy for this group
192-
const cedarPolicy = generateCedarPolicyForGroup(groupId, connectionId, groupToUpdate.isMain, resultPermissions);
192+
const cedarPolicy = generateCedarPolicyForGroup(connectionId, groupToUpdate.isMain, resultPermissions);
193193
groupToUpdate.cedarPolicy = cedarPolicy;
194194
await this._dbContext.groupRepository.saveNewOrUpdatedGroup(groupToUpdate);
195195
Cacher.invalidateCedarPolicyCache(connectionId);

backend/src/helpers/cache/cacher.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,17 +66,23 @@ export class Cacher {
6666
return userInvitations <= 10 && groupInvitations <= 10;
6767
}
6868

69-
public static getCedarPolicyCache(connectionId: string): string | null {
70-
const cached = cedarPolicyCache.get(connectionId);
69+
public static getCedarPolicyCache(connectionId: string, userId: string): string | null {
70+
const cacheKey = `${connectionId}:${userId}`;
71+
const cached = cedarPolicyCache.get(cacheKey);
7172
return cached !== undefined ? cached : null;
7273
}
7374

74-
public static setCedarPolicyCache(connectionId: string, policies: string): void {
75-
cedarPolicyCache.set(connectionId, policies);
75+
public static setCedarPolicyCache(connectionId: string, userId: string, policies: string): void {
76+
const cacheKey = `${connectionId}:${userId}`;
77+
cedarPolicyCache.set(cacheKey, policies);
7678
}
7779

7880
public static invalidateCedarPolicyCache(connectionId: string): void {
79-
cedarPolicyCache.delete(connectionId);
81+
for (const key of cedarPolicyCache.keys()) {
82+
if (key.startsWith(`${connectionId}:`)) {
83+
cedarPolicyCache.delete(key);
84+
}
85+
}
8086
}
8187

8288
public static async clearAllCache(): Promise<void> {

0 commit comments

Comments
 (0)