Skip to content

Commit 59902aa

Browse files
authored
Merge pull request #1754 from rocket-admin/backend_ceadr_diagram_permission
feat: add connection diagram action and permissions to Cedar authorization
2 parents f850e56 + 0336542 commit 59902aa

9 files changed

Lines changed: 358 additions & 3 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export enum CedarAction {
22
ConnectionRead = 'connection:read',
33
ConnectionEdit = 'connection:edit',
4+
ConnectionDiagram = 'connection:diagram',
45
GroupRead = 'group:read',
56
GroupEdit = 'group:edit',
67
TableRead = 'table:read',

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export function generateCedarPolicyForGroup(
2323
policies.push(
2424
`permit(\n principal,\n action == RocketAdmin::Action::"connection:edit",\n resource == ${connectionRef}\n);`,
2525
);
26+
policies.push(
27+
`permit(\n principal,\n action == RocketAdmin::Action::"connection:diagram",\n resource == ${connectionRef}\n);`,
28+
);
2629
} else if (connAccess === AccessLevelEnum.readonly) {
2730
policies.push(
2831
`permit(\n principal,\n action == RocketAdmin::Action::"connection:read",\n resource == ${connectionRef}\n);`,

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ export function parseCedarPolicyToClassicalPermissions(
5050
case 'connection:edit':
5151
result.connection.accessLevel = AccessLevelEnum.edit;
5252
break;
53+
case 'connection:diagram':
54+
if (result.connection.accessLevel === AccessLevelEnum.none) {
55+
result.connection.accessLevel = AccessLevelEnum.readonly;
56+
}
57+
break;
5358
case 'group:read':
5459
if (result.group.accessLevel === AccessLevelEnum.none) {
5560
result.group.accessLevel = AccessLevelEnum.readonly;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@
5959
"resourceTypes": ["Connection"]
6060
}
6161
},
62+
"connection:diagram": {
63+
"appliesTo": {
64+
"principalTypes": ["User"],
65+
"resourceTypes": ["Connection"]
66+
}
67+
},
6268
"group:read": {
6369
"appliesTo": {
6470
"principalTypes": ["User"],

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ export const CEDAR_SCHEMA = {
6868
resourceTypes: ['Connection'],
6969
},
7070
},
71+
'connection:diagram': {
72+
appliesTo: {
73+
principalTypes: ['User'],
74+
resourceTypes: ['Connection'],
75+
},
76+
},
7177
'group:read': {
7278
appliesTo: {
7379
principalTypes: ['User'],

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { AmplitudeEventTypeEnum } from '../../enums/amplitude-event-type.enum.js
2727
import { InTransactionEnum } from '../../enums/in-transaction.enum.js';
2828
import { Messages } from '../../exceptions/text/messages.js';
2929
import { processExceptionMessage } from '../../exceptions/utils/process-exception-message.js';
30+
import { ConnectionDiagramGuard } from '../../guards/connection-diagram.guard.js';
3031
import { ConnectionEditGuard } from '../../guards/connection-edit.guard.js';
3132
import { ConnectionReadGuard } from '../../guards/connection-read.guard.js';
3233
import { isConnectionTypeAgent } from '../../helpers/is-connection-entity-agent.js';
@@ -735,7 +736,7 @@ export class ConnectionController {
735736
status: 200,
736737
type: ConnectionDiagramResponseDTO,
737738
})
738-
@UseGuards(ConnectionEditGuard)
739+
@UseGuards(ConnectionDiagramGuard)
739740
@Get('/connection/diagram/:connectionId')
740741
async getConnectionDiagram(
741742
@SlugUuid('connectionId') connectionId: string,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { BadRequestException, CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
2+
import { Observable } from 'rxjs';
3+
import { IRequestWithCognitoInfo } from '../authorization/cognito-decoded.interface.js';
4+
import { CedarAction } from '../entities/cedar-authorization/cedar-action-map.js';
5+
import { CedarAuthorizationService } from '../entities/cedar-authorization/cedar-authorization.service.js';
6+
import { Messages } from '../exceptions/text/messages.js';
7+
import { ValidationHelper } from '../helpers/validators/validation-helper.js';
8+
import { validateUuidByRegex } from './utils/validate-uuid-by-regex.js';
9+
10+
@Injectable()
11+
export class ConnectionDiagramGuard implements CanActivate {
12+
constructor(private readonly cedarAuthService: CedarAuthorizationService) {}
13+
14+
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
15+
return new Promise(async (resolve, reject) => {
16+
const request: IRequestWithCognitoInfo = context.switchToHttp().getRequest();
17+
const cognitoUserName = request.decoded.sub;
18+
let connectionId: string = request.query.connectionId;
19+
if (!connectionId || (!validateUuidByRegex(connectionId) && !ValidationHelper.isValidNanoId(connectionId))) {
20+
connectionId = request.params?.slug || request.params?.connectionId;
21+
}
22+
if (!connectionId || (!validateUuidByRegex(connectionId) && !ValidationHelper.isValidNanoId(connectionId))) {
23+
reject(new BadRequestException(Messages.CONNECTION_ID_MISSING));
24+
return;
25+
}
26+
27+
try {
28+
const allowed = await this.cedarAuthService.validate({
29+
userId: cognitoUserName,
30+
action: CedarAction.ConnectionDiagram,
31+
connectionId,
32+
});
33+
if (allowed) {
34+
resolve(true);
35+
return;
36+
}
37+
reject(new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS));
38+
} catch (e) {
39+
reject(e);
40+
}
41+
});
42+
}
43+
}

backend/test/ava-tests/non-saas-tests/non-saas-cedar-policy-generator.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ test('isMain=true generates a single wildcard permit', (t) => {
2525
t.is(permits.length, 1);
2626
});
2727

28-
test('connection:edit generates ONLY connection:read + connection:edit (not wildcard)', (t) => {
28+
test('connection:edit generates ONLY connection:read + connection:edit + connection:diagram (not wildcard)', (t) => {
2929
const result = generateCedarPolicyForGroup(
3030
connectionId,
3131
false,
@@ -35,6 +35,7 @@ test('connection:edit generates ONLY connection:read + connection:edit (not wild
3535
);
3636
t.true(result.includes('action == RocketAdmin::Action::"connection:read"'));
3737
t.true(result.includes('action == RocketAdmin::Action::"connection:edit"'));
38+
t.true(result.includes('action == RocketAdmin::Action::"connection:diagram"'));
3839
// Must NOT contain wildcard `action,` on its own line (which would grant table access)
3940
t.false(result.includes('action,\n resource\n'));
4041
// Must NOT contain table actions
@@ -43,7 +44,7 @@ test('connection:edit generates ONLY connection:read + connection:edit (not wild
4344
t.false(result.includes('table:edit'));
4445
t.false(result.includes('table:delete'));
4546
const permits = result.match(/permit\(/g);
46-
t.is(permits.length, 2);
47+
t.is(permits.length, 3);
4748
});
4849

4950
test('connection:readonly generates only connection:read', (t) => {

0 commit comments

Comments
 (0)