Merge pull request #2422 from automatisch/aut-1515

feat(migrations): introduce manage permissions instead of create, update, delete, publish
This commit is contained in:
Ali BARIN
2025-04-15 11:15:27 +02:00
committed by GitHub
60 changed files with 581 additions and 556 deletions

View File

@@ -112,5 +112,6 @@
"src/"
],
"ext": "js"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

View File

@@ -74,7 +74,7 @@ describe('PATCH /api/v1/admin/roles/:roleId', () => {
it('should return the updated role with sanitized permissions', async () => {
const validPermission = {
action: 'create',
action: 'manage',
subject: 'Connection',
conditions: ['isCreator'],
};

View File

@@ -22,7 +22,7 @@ describe('POST /api/v1/apps/:appKey/connections', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Connection',
roleId: role.id,
});

View File

@@ -15,14 +15,7 @@ describe('DELETE /api/v1/connections/:connectionId', () => {
currentUserRole = await currentUser.$relatedQuery('role');
await createPermission({
action: 'delete',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -14,7 +14,7 @@ describe('POST /api/v1/connections/:connectionId/auth-url', () => {
currentUser = await createUser();
await createPermission({
action: 'create',
action: 'manage',
subject: 'Connection',
roleId: currentUser.roleId,
conditions: ['isCreator'],

View File

@@ -32,7 +32,7 @@ describe('POST /api/v1/connections/:connectionId/reset', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -68,7 +68,7 @@ describe('POST /api/v1/connections/:connectionId/reset', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
@@ -84,7 +84,7 @@ describe('POST /api/v1/connections/:connectionId/reset', () => {
const notExistingConnectionUUID = Crypto.randomUUID();
await createPermission({
action: 'create',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -98,7 +98,7 @@ describe('POST /api/v1/connections/:connectionId/reset', () => {
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'create',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -32,7 +32,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -63,7 +63,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
@@ -88,7 +88,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -109,7 +109,7 @@ describe('POST /api/v1/connections/:connectionId/test', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -34,7 +34,7 @@ describe('PATCH /api/v1/connections/:connectionId', () => {
const currentUserConnection = await createConnection(connectionData);
await createPermission({
action: 'update',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -72,7 +72,7 @@ describe('PATCH /api/v1/connections/:connectionId', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: [],
@@ -88,7 +88,7 @@ describe('PATCH /api/v1/connections/:connectionId', () => {
const notExistingConnectionUUID = Crypto.randomUUID();
await createPermission({
action: 'update',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -102,7 +102,7 @@ describe('PATCH /api/v1/connections/:connectionId', () => {
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -26,7 +26,7 @@ describe('POST /api/v1/connections/:connectionId/verify', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -54,7 +54,7 @@ describe('POST /api/v1/connections/:connectionId/verify', () => {
const notExistingConnectionUUID = Crypto.randomUUID();
await createPermission({
action: 'create',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -68,7 +68,7 @@ describe('POST /api/v1/connections/:connectionId/verify', () => {
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'create',
action: 'manage',
subject: 'Connection',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -20,7 +20,7 @@ describe('POST /api/v1/flows', () => {
it('should create an empty flow when no templateId is provided', async () => {
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -42,7 +42,7 @@ describe('POST /api/v1/flows', () => {
it('should create a flow from template when templateId is provided', async () => {
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -36,7 +36,7 @@ describe('POST /api/v1/flows/:flowId/steps', () => {
await createPermission({
roleId: currentUser.roleId,
subject: 'Flow',
action: 'update',
action: 'manage',
conditions: ['isCreator'],
});
@@ -78,7 +78,7 @@ describe('POST /api/v1/flows/:flowId/steps', () => {
await createPermission({
roleId: currentUser.roleId,
subject: 'Flow',
action: 'update',
action: 'manage',
conditions: [],
});
@@ -109,7 +109,7 @@ describe('POST /api/v1/flows/:flowId/steps', () => {
await createPermission({
roleId: currentUser.roleId,
subject: 'Flow',
action: 'update',
action: 'manage',
conditions: ['isCreator'],
});
@@ -133,7 +133,7 @@ describe('POST /api/v1/flows/:flowId/steps', () => {
await createPermission({
roleId: currentUser.roleId,
subject: 'Flow',
action: 'update',
action: 'manage',
conditions: ['isCreator'],
});
@@ -159,7 +159,7 @@ describe('POST /api/v1/flows/:flowId/steps', () => {
await createPermission({
roleId: currentUser.roleId,
subject: 'Flow',
action: 'update',
action: 'manage',
conditions: ['isCreator'],
});

View File

@@ -28,7 +28,7 @@ describe('DELETE /api/v1/flows/:flowId', () => {
});
await createPermission({
action: 'delete',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -52,7 +52,7 @@ describe('DELETE /api/v1/flows/:flowId', () => {
});
await createPermission({
action: 'delete',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -73,7 +73,7 @@ describe('DELETE /api/v1/flows/:flowId', () => {
});
await createPermission({
action: 'delete',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -96,7 +96,7 @@ describe('DELETE /api/v1/flows/:flowId', () => {
});
await createPermission({
action: 'delete',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -48,7 +48,7 @@ describe('POST /api/v1/flows/:flowId/duplicate', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -106,7 +106,7 @@ describe('POST /api/v1/flows/:flowId/duplicate', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -143,7 +143,7 @@ describe('POST /api/v1/flows/:flowId/duplicate', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -169,7 +169,7 @@ describe('POST /api/v1/flows/:flowId/duplicate', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -190,7 +190,7 @@ describe('POST /api/v1/flows/:flowId/duplicate', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -56,7 +56,7 @@ describe('POST /api/v1/flows/:flowId/export', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -113,7 +113,7 @@ describe('POST /api/v1/flows/:flowId/export', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -141,7 +141,7 @@ describe('POST /api/v1/flows/:flowId/export', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -167,7 +167,7 @@ describe('POST /api/v1/flows/:flowId/export', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -188,7 +188,7 @@ describe('POST /api/v1/flows/:flowId/export', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -48,7 +48,7 @@ describe('POST /api/v1/flows/import', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -124,7 +124,7 @@ describe('POST /api/v1/flows/import', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -206,7 +206,7 @@ describe('POST /api/v1/flows/import', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -279,7 +279,7 @@ describe('POST /api/v1/flows/import', () => {
});
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -330,7 +330,7 @@ describe('POST /api/v1/flows/import', () => {
const currentUserFlow = await createFlow({ userId: currentUser.id });
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -45,7 +45,7 @@ describe('PATCH /api/v1/flows/:flowId/folder', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -77,7 +77,7 @@ describe('PATCH /api/v1/flows/:flowId/folder', () => {
const anotherUserFlow = await createFlow({ userId: anotherUser.id });
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -96,7 +96,7 @@ describe('PATCH /api/v1/flows/:flowId/folder', () => {
const anotherUserFolder = await createFolder({ userId: anotherUser.id });
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -111,7 +111,7 @@ describe('PATCH /api/v1/flows/:flowId/folder', () => {
it('should return not found response for not existing flow UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -130,7 +130,7 @@ describe('PATCH /api/v1/flows/:flowId/folder', () => {
const flow = await createFlow({ userId: currentUser.id });
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -147,7 +147,7 @@ describe('PATCH /api/v1/flows/:flowId/folder', () => {
it('should return bad request response for invalid flow UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
});
@@ -160,7 +160,7 @@ describe('PATCH /api/v1/flows/:flowId/folder', () => {
it('should return bad request response for invalid folder UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
});

View File

@@ -51,7 +51,7 @@ describe('PATCH /api/v1/flows/:flowId/status', () => {
});
await createPermission({
action: 'publish',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -114,7 +114,7 @@ describe('PATCH /api/v1/flows/:flowId/status', () => {
});
await createPermission({
action: 'publish',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -152,7 +152,7 @@ describe('PATCH /api/v1/flows/:flowId/status', () => {
});
await createPermission({
action: 'publish',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -178,7 +178,7 @@ describe('PATCH /api/v1/flows/:flowId/status', () => {
});
await createPermission({
action: 'publish',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -199,7 +199,7 @@ describe('PATCH /api/v1/flows/:flowId/status', () => {
});
await createPermission({
action: 'publish',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -29,7 +29,7 @@ describe('PATCH /api/v1/flows/:flowId', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -65,7 +65,7 @@ describe('PATCH /api/v1/flows/:flowId', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -98,7 +98,7 @@ describe('PATCH /api/v1/flows/:flowId', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -121,7 +121,7 @@ describe('PATCH /api/v1/flows/:flowId', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -144,7 +144,7 @@ describe('PATCH /api/v1/flows/:flowId', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -18,7 +18,7 @@ describe('POST /api/v1/folders', () => {
it('should return created flow', async () => {
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],

View File

@@ -21,7 +21,7 @@ describe('DELETE /api/v1/folders/:folderId', () => {
const currentUserFolder = await createFolder({ userId: currentUser.id });
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
});
@@ -34,7 +34,7 @@ describe('DELETE /api/v1/folders/:folderId', () => {
it('should return not found response for not existing folder UUID', async () => {
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
});
@@ -49,7 +49,7 @@ describe('DELETE /api/v1/folders/:folderId', () => {
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
});

View File

@@ -22,7 +22,7 @@ describe('PATCH /api/v1/folders/:folderId', () => {
const currentUserFolder = await createFolder({ userId: currentUser.id });
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
});
@@ -47,7 +47,7 @@ describe('PATCH /api/v1/folders/:folderId', () => {
it('should return not found response for not existing folder UUID', async () => {
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
});
@@ -62,7 +62,7 @@ describe('PATCH /api/v1/folders/:folderId', () => {
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
});
@@ -77,7 +77,7 @@ describe('PATCH /api/v1/folders/:folderId', () => {
const currentUserFolder = await createFolder({ userId: currentUser.id });
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
});

View File

@@ -63,7 +63,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -102,7 +102,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -156,7 +156,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -177,7 +177,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -200,7 +200,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
it('should return not found response for existing step UUID without app key', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -223,7 +223,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-data', () => {
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],

View File

@@ -37,7 +37,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -78,7 +78,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -102,7 +102,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -125,7 +125,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
it('should return not found response for existing step UUID without app key', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -149,7 +149,7 @@ describe('POST /api/v1/steps/:stepId/dynamic-fields', () => {
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],

View File

@@ -41,7 +41,7 @@ describe('DELETE /api/v1/steps/:stepId', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -76,7 +76,7 @@ describe('DELETE /api/v1/steps/:stepId', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -90,7 +90,7 @@ describe('DELETE /api/v1/steps/:stepId', () => {
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -113,7 +113,7 @@ describe('DELETE /api/v1/steps/:stepId', () => {
it('should return bad request response for invalid step UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],

View File

@@ -54,7 +54,7 @@ describe('GET /api/v1/steps/:stepId/previous-steps', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -108,7 +108,7 @@ describe('GET /api/v1/steps/:stepId/previous-steps', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -129,7 +129,7 @@ describe('GET /api/v1/steps/:stepId/previous-steps', () => {
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -152,7 +152,7 @@ describe('GET /api/v1/steps/:stepId/previous-steps', () => {
it('should return bad request response for invalid UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],

View File

@@ -69,7 +69,7 @@ describe('POST /api/v1/steps/:stepId/test', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: ['isCreator'],
@@ -140,7 +140,7 @@ describe('POST /api/v1/steps/:stepId/test', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -165,7 +165,7 @@ describe('POST /api/v1/steps/:stepId/test', () => {
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -188,7 +188,7 @@ describe('POST /api/v1/steps/:stepId/test', () => {
it('should return bad request response for invalid step UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],

View File

@@ -46,7 +46,7 @@ describe('PATCH /api/v1/steps/:stepId', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUser.roleId,
conditions: ['isCreator'],
@@ -96,7 +96,7 @@ describe('PATCH /api/v1/steps/:stepId', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUser.roleId,
conditions: [],
@@ -145,7 +145,7 @@ describe('PATCH /api/v1/steps/:stepId', () => {
});
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUser.roleId,
conditions: ['isCreator'],
@@ -169,7 +169,7 @@ describe('PATCH /api/v1/steps/:stepId', () => {
it('should return not found response for not existing step UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUser.roleId,
conditions: [],
@@ -192,7 +192,7 @@ describe('PATCH /api/v1/steps/:stepId', () => {
it('should return bad request response for invalid step UUID', async () => {
await createPermission({
action: 'update',
action: 'manage',
subject: 'Flow',
roleId: currentUser.roleId,
conditions: [],

View File

@@ -24,7 +24,7 @@ describe('GET /api/v1/templates', () => {
it('should return templates when templates are enabled and user has create flow permission', async () => {
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
@@ -45,7 +45,7 @@ describe('GET /api/v1/templates', () => {
it('should return 403 when templates are disabled', async () => {
await createPermission({
action: 'create',
action: 'manage',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],

View File

@@ -0,0 +1,110 @@
export async function up(knex) {
const roles = await knex('roles').select('id', 'name');
// Define the required actions for each subject
const subjectActionMap = {
Connection: ['create', 'delete', 'update'],
Flow: ['create', 'delete', 'publish', 'update'],
User: ['create', 'delete', 'update'],
Role: ['create', 'delete', 'update'],
SamlAuthProvider: ['create', 'delete', 'update'],
Config: ['update'],
App: ['create', 'delete', 'update'],
};
for (const role of roles) {
for (const [subject, actions] of Object.entries(subjectActionMap)) {
const rolePermissions = await knex('permissions')
.where({ role_id: role.id, subject })
.whereIn('action', actions)
.select('id', 'action', 'conditions');
const actionCounts = rolePermissions.reduce((counts, perm) => {
counts[perm.action] = (counts[perm.action] || 0) + 1;
return counts;
}, {});
let allActionsExist = true;
for (const action of actions) {
if (actionCounts[action] !== 1) {
allActionsExist = false;
break;
}
}
// Determine if any of the permissions has the 'isCreator' condition
const hasIsCreatorCondition = rolePermissions.some(
(perm) => perm.conditions && perm.conditions.includes('isCreator')
);
// Delete the existing permissions for the required actions
await knex('permissions')
.where({ role_id: role.id, subject })
.whereIn('action', actions)
.del();
// If all required actions exist, insert a new permission with the 'manage' action
if (allActionsExist) {
await knex('permissions').insert({
role_id: role.id,
subject,
action: 'manage',
conditions: JSON.stringify(
hasIsCreatorCondition ? ['isCreator'] : []
),
});
}
}
}
return;
}
export async function down(knex) {
const roles = await knex('roles').select('id', 'name');
// Define the required actions for each subject
const subjectActionMap = {
Connection: ['create', 'delete', 'update'],
Flow: ['create', 'delete', 'publish', 'update'],
User: ['create', 'delete', 'update'],
Role: ['create', 'delete', 'update'],
SamlAuthProvider: ['create', 'delete', 'update'],
Config: ['update'],
App: ['create', 'delete', 'update'],
};
for (const role of roles) {
for (const [subject, actions] of Object.entries(subjectActionMap)) {
// Find the 'manage' permission for the subject
const managePermission = await knex('permissions')
.where({ role_id: role.id, subject, action: 'manage' })
.first();
if (managePermission) {
// Determine if the 'manage' permission has the 'isCreator' condition
const hasIsCreatorCondition =
managePermission.conditions.includes('isCreator');
// Delete the 'manage' permission
await knex('permissions')
.where({ role_id: role.id, subject, action: 'manage' })
.del();
// Restore the original permissions for the subject
const restoredPermissions = actions.map((action) => ({
role_id: role.id,
subject,
action,
conditions: JSON.stringify(
hasIsCreatorCondition ? ['isCreator'] : []
),
}));
await knex('permissions').insert(restoredPermissions);
}
}
}
return;
}

View File

@@ -22,19 +22,19 @@ const authorizationList = {
subject: 'Flow',
},
'POST /api/v1/flows/': {
action: 'create',
action: 'manage',
subject: 'Flow',
},
'PATCH /api/v1/flows/:flowId': {
action: 'update',
action: 'manage',
subject: 'Flow',
},
'DELETE /api/v1/flows/:flowId': {
action: 'delete',
action: 'manage',
subject: 'Flow',
},
'GET /api/v1/templates/': {
action: 'create',
action: 'manage',
subject: 'Flow',
},
'GET /api/v1/steps/:stepId/connection': {
@@ -42,23 +42,23 @@ const authorizationList = {
subject: 'Flow',
},
'PATCH /api/v1/steps/:stepId': {
action: 'update',
action: 'manage',
subject: 'Flow',
},
'POST /api/v1/steps/:stepId/test': {
action: 'update',
action: 'manage',
subject: 'Flow',
},
'GET /api/v1/steps/:stepId/previous-steps': {
action: 'update',
action: 'manage',
subject: 'Flow',
},
'POST /api/v1/steps/:stepId/dynamic-fields': {
action: 'update',
action: 'manage',
subject: 'Flow',
},
'POST /api/v1/steps/:stepId/dynamic-data': {
action: 'update',
action: 'manage',
subject: 'Flow',
},
'GET /api/v1/connections/:connectionId/flows': {
@@ -66,11 +66,11 @@ const authorizationList = {
subject: 'Flow',
},
'POST /api/v1/connections/:connectionId/test': {
action: 'update',
action: 'manage',
subject: 'Connection',
},
'POST /api/v1/connections/:connectionId/verify': {
action: 'create',
action: 'manage',
subject: 'Connection',
},
'GET /api/v1/apps/:appKey/flows': {
@@ -94,59 +94,59 @@ const authorizationList = {
subject: 'Execution',
},
'DELETE /api/v1/steps/:stepId': {
action: 'update',
action: 'manage',
subject: 'Flow',
},
'PATCH /api/v1/connections/:connectionId': {
action: 'update',
action: 'manage',
subject: 'Connection',
},
'DELETE /api/v1/connections/:connectionId': {
action: 'delete',
action: 'manage',
subject: 'Connection',
},
'POST /api/v1/connections/:connectionId/reset': {
action: 'create',
action: 'manage',
subject: 'Connection',
},
'PATCH /api/v1/flows/:flowId/status': {
action: 'publish',
action: 'manage',
subject: 'Flow',
},
'POST /api/v1/flows/:flowId/duplicate': {
action: 'create',
action: 'manage',
subject: 'Flow',
},
'POST /api/v1/flows/:flowId/export': {
action: 'update',
action: 'manage',
subject: 'Flow',
},
'POST /api/v1/flows/import': {
action: 'create',
action: 'manage',
subject: 'Flow',
},
'POST /api/v1/flows/:flowId/steps': {
action: 'update',
action: 'manage',
subject: 'Flow',
},
'POST /api/v1/apps/:appKey/connections': {
action: 'create',
action: 'manage',
subject: 'Connection',
},
'POST /api/v1/connections/:connectionId/auth-url': {
action: 'create',
action: 'manage',
subject: 'Connection',
},
'POST /api/v1/folders/': {
action: 'create',
action: 'manage',
subject: 'Flow',
},
'PATCH /api/v1/folders/:folderId': {
action: 'create',
action: 'manage',
subject: 'Flow',
},
'DELETE /api/v1/folders/:folderId': {
action: 'create',
action: 'manage',
subject: 'Flow',
},
'GET /api/v1/folders/': {
@@ -154,7 +154,7 @@ const authorizationList = {
subject: 'Flow',
},
'PATCH /api/v1/flows/:flowId/folder': {
action: 'update',
action: 'manage',
subject: 'Flow',
},
'GET /api/v1/flows/:flowId/folder': {

View File

@@ -17,56 +17,22 @@ const permissionCatalog = {
conditions: [
{
key: 'isCreator',
label: 'Is creator'
}
label: 'Is creator',
},
],
actions: [
{
label: 'Create',
key: 'create',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Read',
key: 'read',
subjects: [
Connection.key,
Execution.key,
Flow.key,
]
subjects: [Connection.key, Execution.key, Flow.key],
},
{
label: 'Update',
key: 'update',
subjects: [
Connection.key,
Flow.key,
]
label: 'Manage',
key: 'manage',
subjects: [Connection.key, Flow.key],
},
{
label: 'Delete',
key: 'delete',
subjects: [
Connection.key,
Flow.key,
]
},
{
label: 'Publish',
key: 'publish',
subjects: [
Flow.key,
]
}
],
subjects: [
Connection,
Flow,
Execution
]
subjects: [Connection, Flow, Execution],
};
export default permissionCatalog;

View File

@@ -14,10 +14,10 @@ describe('Permission model', () => {
it('filter should return only valid permissions based on permission catalog', () => {
const permissions = [
{ action: 'read', subject: 'Flow', conditions: ['isCreator'] },
{ action: 'delete', subject: 'Connection', conditions: [] },
{ action: 'publish', subject: 'Flow', conditions: ['isCreator'] },
{ action: 'update', subject: 'Execution', conditions: [] }, // Invalid subject
{ action: 'read', subject: 'Execution', conditions: ['invalid'] }, // Invalid condition
{ action: 'manage', subject: 'Connection', conditions: [] },
{ action: 'manage', subject: 'Flow', conditions: ['isCreator'] },
{ action: 'manage', subject: 'Execution', conditions: [] }, // Invalid subject
{ action: 'manage', subject: 'Execution', conditions: ['invalid'] }, // Invalid condition
{ action: 'invalid', subject: 'Execution', conditions: [] }, // Invalid action
];
@@ -25,15 +25,15 @@ describe('Permission model', () => {
expect(result).toStrictEqual([
{ action: 'read', subject: 'Flow', conditions: ['isCreator'] },
{ action: 'delete', subject: 'Connection', conditions: [] },
{ action: 'publish', subject: 'Flow', conditions: ['isCreator'] },
{ action: 'manage', subject: 'Connection', conditions: [] },
{ action: 'manage', subject: 'Flow', conditions: ['isCreator'] },
]);
});
describe('findAction', () => {
it('should return action from permission catalog', () => {
const action = Permission.findAction('create');
expect(action.key).toStrictEqual('create');
const action = Permission.findAction('manage');
expect(action.key).toStrictEqual('manage');
});
it('should return undefined for invalid actions', () => {
@@ -45,7 +45,7 @@ describe('Permission model', () => {
describe('isSubjectValid', () => {
it('should return true for valid subjects', () => {
const validAction = permissionCatalog.actions.find(
(action) => action.key === 'create'
(action) => action.key === 'manage'
);
const validSubject = Permission.isSubjectValid('Connection', validAction);
@@ -54,7 +54,7 @@ describe('Permission model', () => {
it('should return false for invalid subjects', () => {
const validAction = permissionCatalog.actions.find(
(action) => action.key === 'create'
(action) => action.key === 'manage'
);
const invalidSubject = Permission.isSubjectValid(

View File

@@ -166,7 +166,7 @@ describe('Role model', () => {
description: 'Updated description',
permissions: [
{
action: 'update',
action: 'manage',
subject: 'Flow',
conditions: [],
},

View File

@@ -1,31 +1,16 @@
const getPermissionsCatalogMock = async () => {
const data = {
actions: [
{
key: 'create',
label: 'Create',
subjects: ['Connection', 'Flow'],
},
{
key: 'read',
label: 'Read',
subjects: ['Connection', 'Execution', 'Flow'],
},
{
key: 'update',
label: 'Update',
key: 'manage',
label: 'Manage',
subjects: ['Connection', 'Flow'],
},
{
key: 'delete',
label: 'Delete',
subjects: ['Connection', 'Flow'],
},
{
key: 'publish',
label: 'Publish',
subjects: ['Flow'],
},
],
conditions: [
{

View File

@@ -14,98 +14,22 @@ export class AdminCreateRolePage extends AuthenticatedPage {
this.nameInput = page.getByTestId('name-input');
this.descriptionInput = page.getByTestId('description-input');
this.createButton = page.getByTestId('create-button');
this.connectionRow = page.getByTestId('Connection-permission-row');
this.executionRow = page.getByTestId('Execution-permission-row');
this.flowRow = page.getByTestId('Flow-permission-row');
this.pageTitle = page.getByTestId('create-role-title');
this.permissionsCatalog = page.getByTestId('permissions-catalog');
}
/**
* @param {('Connection'|'Execution'|'Flow')} subject
*/
getRoleConditionsModal(subject) {
return new RoleConditionsModal(this.page, subject);
}
async getPermissionConfigs() {
const subjects = ['Connection', 'Flow', 'Execution'];
const permissionConfigs = [];
for (let subject of subjects) {
const row = this.getSubjectRow(subject);
const actionInputs = await this.getRowInputs(row);
Object.keys(actionInputs).forEach((action) => {
permissionConfigs.push({
action,
locator: actionInputs[action],
subject,
row,
});
});
}
return permissionConfigs;
}
/**
*
* @param {(
* 'Connection' | 'Flow' | 'Execution'
* )} subject
*/
getSubjectRow(subject) {
const k = `${subject.toLowerCase()}Row`;
if (this[k]) {
return this[k];
} else {
throw 'Unknown row';
}
}
/**
* @param {import('@playwright/test').Locator} row
*/
async getRowInputs(row) {
const inputs = {
// settingsButton: row.getByTestId('permission-settings-button')
};
for (let input of ['create', 'read', 'update', 'delete', 'publish']) {
const testId = `${input}-checkbox`;
if ((await row.getByTestId(testId).count()) > 0) {
inputs[input] = row.getByTestId(testId).locator('input');
}
}
return inputs;
}
/**
* @param {import('@playwright/test').Locator} row
*/
async clickPermissionSettings(row) {
await row.getByTestId('permission-settings-button').click();
}
/**
*
* @param {string} subject
* @param {'create'|'read'|'update'|'delete'|'publish'} action
* @param {boolean} val
*/
async updateAction(subject, action, val) {
const row = await this.getSubjectRow(subject);
const inputs = await this.getRowInputs(row);
if (inputs[action]) {
if (await inputs[action].isChecked()) {
if (!val) {
await inputs[action].click();
}
} else {
if (val) {
await inputs[action].click();
}
}
} else {
throw new Error(`${subject} does not have action ${action}`);
}
this.connectionPermissionRow = page.getByTestId(
'Connection-permission-row'
);
this.flowPermissionRow = page.getByTestId('Flow-permission-row');
this.executionPermissionRow = page.getByTestId('Execution-permission-row');
this.isCreatorReadCheckbox = page
.getByTestId('isCreator-read-checkbox')
.locator('input');
this.readCheckbox = page.getByTestId('read-checkbox').locator('input');
this.isCreatorManageCheckbox = page
.getByTestId('isCreator-manage-checkbox')
.locator('input');
this.manageCheckbox = page.getByTestId('manage-checkbox').locator('input');
}
async waitForPermissionsCatalogToVisible() {

View File

@@ -1,69 +1,55 @@
const { test, expect } = require('../../fixtures/index');
test(
'Role permissions conform with role conditions ',
async({ adminRolesPage, adminCreateRolePage }) => {
await adminRolesPage.navigateTo();
await adminRolesPage.createRoleButton.click();
/*
example config: {
action: 'read',
subject: 'connection',
row: page.getByTestId('connection-permission-row'),
locator: row.getByTestId('read-checkbox')
}
*/
const permissionConfigs =
await adminCreateRolePage.getPermissionConfigs();
test('Check Own permissions when All are checked', async ({
adminRolesPage,
adminCreateRolePage,
}) => {
await adminRolesPage.navigateTo();
await adminRolesPage.createRoleButton.click();
await adminCreateRolePage.waitForPermissionsCatalogToVisible();
await test.step(
'Iterate over each permission config and make sure role conditions conform',
async () => {
for (let config of permissionConfigs) {
await config.locator.click();
await adminCreateRolePage.clickPermissionSettings(config.row);
const modal = adminCreateRolePage.getRoleConditionsModal(
config.subject
);
await expect(modal.modal).toBeVisible();
const conditions = await modal.getAvailableConditions();
for (let conditionAction of Object.keys(conditions)) {
if (conditionAction === config.action) {
await expect(conditions[conditionAction]).not.toBeDisabled();
} else {
await expect(conditions[conditionAction]).toBeDisabled();
}
}
await modal.close();
await config.locator.click();
}
}
);
}
);
await adminCreateRolePage.connectionPermissionRow
.locator(adminCreateRolePage.readCheckbox)
.check();
await expect(
adminCreateRolePage.connectionPermissionRow.locator(
adminCreateRolePage.isCreatorReadCheckbox
)
).toBeChecked();
test(
'Default role permissions conforms with role conditions',
async({ adminRolesPage, adminCreateRolePage }) => {
await adminRolesPage.navigateTo();
await adminRolesPage.createRoleButton.click();
await adminCreateRolePage.flowPermissionRow
.locator(adminCreateRolePage.readCheckbox)
.check();
await expect(
adminCreateRolePage.flowPermissionRow.locator(
adminCreateRolePage.isCreatorReadCheckbox
)
).toBeChecked();
const subjects = ['Connection', 'Execution', 'Flow'];
for (let subject of subjects) {
const row = adminCreateRolePage.getSubjectRow(subject);
const modal = adminCreateRolePage.getRoleConditionsModal(subject);
await adminCreateRolePage.clickPermissionSettings(row);
await expect(modal.modal).toBeVisible();
const availableConditions = await modal.getAvailableConditions();
const conditions = ['create', 'read', 'update', 'delete', 'publish'];
for (let condition of conditions) {
if (availableConditions[condition]) {
await expect(availableConditions[condition]).toBeDisabled();
}
}
await modal.close();
}
await adminCreateRolePage.executionPermissionRow
.locator(adminCreateRolePage.readCheckbox)
.check();
await expect(
adminCreateRolePage.executionPermissionRow.locator(
adminCreateRolePage.isCreatorReadCheckbox
)
).toBeChecked();
}
);
await adminCreateRolePage.connectionPermissionRow
.locator(adminCreateRolePage.manageCheckbox)
.check();
await expect(
adminCreateRolePage.connectionPermissionRow.locator(
adminCreateRolePage.isCreatorManageCheckbox
)
).toBeChecked();
await adminCreateRolePage.flowPermissionRow
.locator(adminCreateRolePage.manageCheckbox)
.check();
await expect(
adminCreateRolePage.flowPermissionRow.locator(
adminCreateRolePage.isCreatorManageCheckbox
)
).toBeChecked();
});

View File

@@ -31,7 +31,7 @@ export default (
<Route
path={URLS.CREATE_USER}
element={
<Can I="create" a="User">
<Can I="manage" a="User">
<CreateUser />
</Can>
}
@@ -40,7 +40,7 @@ export default (
<Route
path={URLS.USER_PATTERN}
element={
<Can I="update" a="User">
<Can I="manage" a="User">
<EditUser />
</Can>
}
@@ -58,7 +58,7 @@ export default (
<Route
path={URLS.CREATE_ROLE}
element={
<Can I="create" a="Role">
<Can I="manage" a="Role">
<CreateRole />
</Can>
}
@@ -67,7 +67,7 @@ export default (
<Route
path={URLS.ROLE_PATTERN}
element={
<Can I="update" a="Role">
<Can I="manage" a="Role">
<EditRole />
</Can>
}
@@ -76,7 +76,7 @@ export default (
<Route
path={URLS.USER_INTERFACE}
element={
<Can I="update" a="Config">
<Can I="manage" a="Config">
<UserInterface />
</Can>
}
@@ -86,8 +86,8 @@ export default (
path={URLS.AUTHENTICATION}
element={
<Can I="read" a="SamlAuthProvider">
<Can I="update" a="SamlAuthProvider">
<Can I="create" a="SamlAuthProvider">
<Can I="manage" a="SamlAuthProvider">
<Can I="manage" a="SamlAuthProvider">
<Authentication />
</Can>
</Can>
@@ -98,7 +98,7 @@ export default (
<Route
path={URLS.ADMIN_APPS}
element={
<Can I="update" a="App">
<Can I="manage" a="App">
<AdminApplications />
</Can>
}
@@ -107,7 +107,7 @@ export default (
<Route
path={`${URLS.ADMIN_APP_PATTERN}/*`}
element={
<Can I="update" a="App">
<Can I="manage" a="App">
<AdminApplication />
</Can>
}
@@ -116,7 +116,7 @@ export default (
<Route
path={`${URLS.ADMIN_TEMPLATES}/*`}
element={
<Can I="update" a="Config">
<Can I="manage" a="Config">
<AdminTemplates />
</Can>
}
@@ -125,7 +125,7 @@ export default (
<Route
path={`${URLS.ADMIN_CREATE_TEMPLATE_PATTERN}/*`}
element={
<Can I="update" a="Config">
<Can I="manage" a="Config">
<AdminCreateTemplate />
</Can>
}
@@ -134,7 +134,7 @@ export default (
<Route
path={`${URLS.ADMIN_UPDATE_TEMPLATE_PATTERN}/*`}
element={
<Can I="update" a="Config">
<Can I="manage" a="Config">
<AdminUpdateTemplate />
</Can>
}

View File

@@ -93,15 +93,15 @@ function SettingsLayout() {
const closeDrawer = () => setDrawerOpen(false);
const drawerLinks = createDrawerLinks({
canCreateFlows: currentUserAbility.can('create', 'Flow'),
canCreateFlows: currentUserAbility.can('manage', 'Flow'),
canReadUser: currentUserAbility.can('read', 'User'),
canReadRole: currentUserAbility.can('read', 'Role'),
canUpdateConfig: currentUserAbility.can('update', 'Config'),
canManageSamlAuthProvider:
currentUserAbility.can('read', 'SamlAuthProvider') &&
currentUserAbility.can('update', 'SamlAuthProvider') &&
currentUserAbility.can('create', 'SamlAuthProvider'),
canUpdateApp: currentUserAbility.can('update', 'App'),
canUpdateConfig: currentUserAbility.can('manage', 'Config'),
canManageSamlAuthProvider: currentUserAbility.can(
'manage',
'SamlAuthProvider',
),
canUpdateApp: currentUserAbility.can('manage', 'App'),
});
const drawerBottomLinks = [

View File

@@ -41,7 +41,7 @@ function AdminTemplateContextMenu(props) {
hideBackdrop={false}
anchorEl={anchorEl}
>
<Can I="delete" a="Flow" passThrough>
<Can I="manage" a="Flow" passThrough>
{(allowed) => (
<MenuItem disabled={!allowed} onClick={onTemplateDelete}>
{formatMessage('adminTemplateContextMenu.delete')}

View File

@@ -51,7 +51,7 @@ function ContextMenu(props) {
)}
</Can>
<Can I="update" a="Connection" passThrough>
<Can I="manage" a="Connection" passThrough>
{(allowed) => (
<MenuItem
onClick={createActionHandler({ type: 'test' })}
@@ -62,7 +62,7 @@ function ContextMenu(props) {
)}
</Can>
<Can I="create" a="Connection" passThrough>
<Can I="manage" a="Connection" passThrough>
{(allowed) => (
<MenuItem
component={Link}
@@ -79,7 +79,7 @@ function ContextMenu(props) {
)}
</Can>
<Can I="delete" a="Connection" passThrough>
<Can I="manage" a="Connection" passThrough>
{(allowed) => (
<MenuItem
onClick={createActionHandler({ type: 'delete' })}

View File

@@ -20,7 +20,7 @@ function AppConnections(props) {
if (!hasConnections) {
return (
<Can I="create" a="Connection" passThrough>
<Can I="manage" a="Connection" passThrough>
{(allowed) => (
<NoResultFound
text={formatMessage('app.noConnections')}

View File

@@ -37,7 +37,7 @@ function AppFlows(props) {
if (!hasFlows) {
return (
<Can I="create" a="Flow" passThrough>
<Can I="manage" a="Flow" passThrough>
{(allowed) => (
<NoResultFound
text={formatMessage('app.noFlows')}

View File

@@ -53,7 +53,7 @@ function DeleteRoleButton(props) {
return (
<>
<Can I="delete" a="Role" passThrough>
<Can I="manage" a="Role" passThrough>
{(allowed) => (
<IconButton
disabled={!allowed || disabled}

View File

@@ -126,7 +126,7 @@ export default function EditorLayout() {
)}
</Can>
<Can I="publish" a="Flow" passThrough>
<Can I="manage" a="Flow" passThrough>
{(allowed) => (
<Button
disabled={!allowed || !flow}

View File

@@ -130,7 +130,7 @@ function ContextMenu(props) {
)}
</Can>
<Can I="create" a="Flow" passThrough>
<Can I="manage" a="Flow" passThrough>
{(allowed) => (
<MenuItem disabled={!allowed} onClick={onFlowDuplicate}>
{formatMessage('flow.duplicate')}
@@ -139,7 +139,7 @@ function ContextMenu(props) {
</Can>
{isCurrentUserAdmin && (
<Can I="create" a="Flow" passThrough>
<Can I="manage" a="Flow" passThrough>
{(allowed) => (
<MenuItem disabled={!allowed} onClick={onCreateTemplate}>
{formatMessage('flow.createTemplateFromFlow')}
@@ -148,7 +148,7 @@ function ContextMenu(props) {
</Can>
)}
<Can I="update" a="Flow" passThrough>
<Can I="manage" a="Flow" passThrough>
{(allowed) => (
<MenuItem disabled={!allowed} onClick={onFlowFolderUpdate}>
{formatMessage('flow.moveTo')}
@@ -164,7 +164,7 @@ function ContextMenu(props) {
)}
</Can>
<Can I="delete" a="Flow" passThrough>
<Can I="manage" a="Flow" passThrough>
{(allowed) => (
<MenuItem disabled={!allowed} onClick={onFlowDelete}>
{formatMessage('flow.delete')}

View File

@@ -19,7 +19,7 @@ export default function FlowsButtons() {
const theme = useTheme();
const { data: config } = useAutomatischConfig();
const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md'));
const canCreateFlow = currentUserAbility.can('create', 'Flow');
const canCreateFlow = currentUserAbility.can('manage', 'Flow');
const enableTemplates = config?.data.enableTemplates === true;
const createFlowButtonData = {

View File

@@ -1,51 +0,0 @@
import React from 'react';
import { useFormContext } from 'react-hook-form';
import PropTypes from 'prop-types';
import ControlledCheckbox from 'components/ControlledCheckbox';
const ActionField = ({ action, subject, disabled, name, syncIsCreator }) => {
const { formState, resetField } = useFormContext();
const actionDefaultValue =
formState.defaultValues?.[name]?.[subject.key]?.[action.key].value;
const conditionFieldName = `${name}.${subject.key}.${action.key}.conditions.isCreator`;
const conditionFieldTouched =
formState.touchedFields?.[name]?.[subject.key]?.[action.key]?.conditions
?.isCreator === true;
const handleSyncIsCreator = (newValue) => {
if (
syncIsCreator &&
actionDefaultValue === false &&
!conditionFieldTouched
) {
resetField(conditionFieldName, { defaultValue: newValue });
}
};
return (
<ControlledCheckbox
disabled={disabled}
name={`${name}.${subject.key}.${action.key}.value`}
dataTest={`${action.key.toLowerCase()}-checkbox`}
onChange={(e, value) => {
handleSyncIsCreator(value);
}}
/>
);
};
ActionField.propTypes = {
action: PropTypes.shape({
key: PropTypes.string.isRequired,
subjects: PropTypes.arrayOf(PropTypes.string).isRequired,
}),
subject: PropTypes.shape({
key: PropTypes.string.isRequired,
}).isRequired,
disabled: PropTypes.bool,
name: PropTypes.string.isRequired,
syncIsCreator: PropTypes.bool,
};
export default ActionField;

View File

@@ -0,0 +1,63 @@
import React from 'react';
import { useFormContext } from 'react-hook-form';
import PropTypes from 'prop-types';
import ControlledCheckbox from 'components/ControlledCheckbox';
const AllEntitiesPermissions = ({
action,
subject,
disabled,
name,
syncIsCreator,
}) => {
const { getValues, formState, resetField } = useFormContext();
const fieldName = `${name}.${subject.key}.${action.key}.allEntities`;
const defaultValue =
formState.defaultValues?.[name]?.[subject.key]?.[action.key].allEntities;
const ownEntitiesFieldName = `${name}.${subject.key}.${action.key}.ownEntities`;
const ownEntitiesFieldTouched =
formState.touchedFields?.[name]?.[subject.key]?.[action.key]
?.ownEntities === true;
const currentValue = getValues(fieldName);
React.useEffect(() => {
if (currentValue === true) {
resetField(ownEntitiesFieldName, { defaultValue: true });
}
}, [ownEntitiesFieldName, currentValue]);
const handleSyncIsCreator = (newValue) => {
if (syncIsCreator && defaultValue === false && !ownEntitiesFieldTouched) {
resetField(ownEntitiesFieldName, { defaultValue: newValue });
}
if (newValue === true) {
resetField(ownEntitiesFieldName, { defaultValue: true });
}
};
return (
<ControlledCheckbox
disabled={disabled}
name={fieldName}
dataTest={`${action.key.toLowerCase()}-checkbox`}
/>
);
};
AllEntitiesPermissions.propTypes = {
action: PropTypes.shape({
key: PropTypes.string.isRequired,
subjects: PropTypes.arrayOf(PropTypes.string).isRequired,
}),
subject: PropTypes.shape({
key: PropTypes.string.isRequired,
}).isRequired,
disabled: PropTypes.bool,
name: PropTypes.string.isRequired,
syncIsCreator: PropTypes.bool,
};
export default AllEntitiesPermissions;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { useFormContext } from 'react-hook-form';
import PropTypes from 'prop-types';
import ControlledCheckbox from 'components/ControlledCheckbox';
const OwnEntitiesPermission = ({ action, subject, disabled, name }) => {
const { getValues, resetField } = useFormContext();
const fieldName = `${name}.${subject.key}.${action.key}.ownEntities`;
const allEntitiesFieldName = `${name}.${subject.key}.${action.key}.allEntities`;
const currentValue = getValues(fieldName);
React.useEffect(() => {
if (currentValue === false) {
resetField(allEntitiesFieldName, { defaultValue: false });
}
}, [allEntitiesFieldName, currentValue]);
return (
<ControlledCheckbox
name={fieldName}
disabled={disabled}
dataTest={`isCreator-${action.key.toLowerCase()}-checkbox`}
/>
);
};
OwnEntitiesPermission.propTypes = {
action: PropTypes.shape({
key: PropTypes.string.isRequired,
subjects: PropTypes.arrayOf(PropTypes.string).isRequired,
}),
subject: PropTypes.shape({
key: PropTypes.string.isRequired,
}).isRequired,
disabled: PropTypes.bool,
name: PropTypes.string.isRequired,
};
export default OwnEntitiesPermission;

View File

@@ -1,7 +1,5 @@
import {
IconButton,
Skeleton,
Stack,
Table,
TableBody,
TableCell,
@@ -10,7 +8,6 @@ import {
TableRow,
Typography,
} from '@mui/material';
import SettingsIcon from '@mui/icons-material/Settings';
import ControlledCheckbox from 'components/ControlledCheckbox';
@@ -21,7 +18,7 @@ const PermissionCatalogFieldLoader = () => {
<TableHead>
<TableRow>
<TableCell component="th" />
{[...Array(5)].map((row, index) => (
{[...Array(4)].map((row, index) => (
<TableCell key={index} component="th">
<Skeleton />
</TableCell>
@@ -30,27 +27,26 @@ const PermissionCatalogFieldLoader = () => {
</TableRow>
</TableHead>
<TableBody>
{[...Array(3)].map((row, index) => (
<TableRow key={index} sx={{ '&:last-child td': { border: 0 } }}>
{[...Array(3)].map((row, subjectIndex) => (
<TableRow
key={subjectIndex}
sx={{ '&:last-child td': { border: 0 } }}
>
<TableCell scope="row">
<Skeleton width={40} />
</TableCell>
{[...Array(5)].map((action, index) => (
<TableCell key={index} align="center">
<Typography variant="subtitle2">
<ControlledCheckbox name="value" disabled />
</Typography>
</TableCell>
))}
<TableCell>
<Stack direction="row" gap={1} justifyContent="right">
<IconButton color="info" size="small" disabled>
<SettingsIcon />
</IconButton>
</Stack>
</TableCell>
{[...Array(4)].map(
(action, actionIndex) =>
(subjectIndex !== 2 ||
(actionIndex !== 3 && actionIndex !== 2)) && (
<TableCell key={actionIndex} align="center">
<Typography variant="subtitle2">
<ControlledCheckbox name="value" disabled />
</Typography>
</TableCell>
),
)}
</TableRow>
))}
</TableBody>

View File

@@ -1,8 +1,4 @@
import PropTypes from 'prop-types';
import SettingsIcon from '@mui/icons-material/Settings';
import IconButton from '@mui/material/IconButton';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
@@ -10,12 +6,14 @@ import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import PropTypes from 'prop-types';
import * as React from 'react';
import usePermissionCatalog from 'hooks/usePermissionCatalog.ee';
import PermissionSettings from './PermissionSettings.ee';
import useFormatMessage from 'hooks/useFormatMessage';
import AllEntitiesPermissions from './AllEntitiesPermissions';
import ConditionField from './OwnEntitiesPermission';
import PermissionCatalogFieldLoader from './PermissionCatalogFieldLoader';
import ActionField from './ActionField';
const PermissionCatalogField = ({
name = 'permissions',
@@ -23,10 +21,10 @@ const PermissionCatalogField = ({
syncIsCreator = false,
loading = false,
}) => {
const formatMessage = useFormatMessage();
const { data, isLoading: isPermissionCatalogLoading } =
usePermissionCatalog();
const permissionCatalog = data?.data;
const [dialogName, setDialogName] = React.useState();
if (isPermissionCatalogLoading || loading)
return <PermissionCatalogFieldLoader />;
@@ -39,24 +37,44 @@ const PermissionCatalogField = ({
<TableCell component="th" />
{permissionCatalog?.actions.map((action) => (
<TableCell component="th" key={action.key}>
<Typography
component="div"
variant="subtitle1"
align="center"
sx={{
color: 'text.secondary',
fontWeight: 700,
}}
>
{action.label}
</Typography>
</TableCell>
))}
<React.Fragment key={action.key}>
<TableCell component="th" key={action.key}>
<Typography
component="div"
variant="subtitle2"
align="center"
sx={{
color: 'text.secondary',
fontWeight: 700,
}}
>
{action.label}{' '}
{formatMessage('permissionCatalogField.ownEntitiesLabel')}
</Typography>
</TableCell>
<TableCell component="th" />
<TableCell
component="th"
key={`${action.key}-isCreator-condition`}
>
<Typography
component="div"
variant="subtitle2"
align="center"
sx={{
color: 'text.secondary',
fontWeight: 700,
}}
>
{action.label}{' '}
{formatMessage('permissionCatalogField.allEntitiesLabel')}
</Typography>
</TableCell>
</React.Fragment>
))}
</TableRow>
</TableHead>
<TableBody>
{permissionCatalog?.subjects.map((subject) => (
<TableRow
@@ -71,44 +89,43 @@ const PermissionCatalogField = ({
</TableCell>
{permissionCatalog?.actions.map((action) => (
<TableCell key={`${subject.key}.${action.key}`} align="center">
<Typography variant="subtitle2" component="div">
{action.subjects.includes(subject.key) && (
<ActionField
action={action}
subject={subject}
disabled={disabled}
name={name}
syncIsCreator={syncIsCreator}
/>
)}
{!action.subjects.includes(subject.key) && '-'}
</Typography>
</TableCell>
))}
<TableCell>
<Stack direction="row" gap={1} justifyContent="right">
<IconButton
color="info"
size="small"
onClick={() => setDialogName(subject.key)}
disabled={disabled}
data-test="permission-settings-button"
<React.Fragment key={`${subject.key}.${action.key}`}>
<TableCell
key={`${subject.key}.${action.key}-isCreator-condition`}
align="center"
>
<SettingsIcon />
</IconButton>
<Typography variant="subtitle2" component="div">
{action.subjects.includes(subject.key) && (
<ConditionField
action={action}
subject={subject}
disabled={disabled}
name={name}
/>
)}
{!action.subjects.includes(subject.key) && '-'}
</Typography>
</TableCell>
<PermissionSettings
open={dialogName === subject.key}
onClose={() => setDialogName('')}
fieldPrefix={`${name}.${subject.key}`}
subject={subject.key}
actions={permissionCatalog?.actions}
conditions={permissionCatalog?.conditions}
/>
</Stack>
</TableCell>
<TableCell
key={`${subject.key}.${action.key}`}
align="center"
>
<Typography variant="subtitle2" component="div">
{action.subjects.includes(subject.key) && (
<AllEntitiesPermissions
action={action}
subject={subject}
disabled={disabled}
name={name}
syncIsCreator={syncIsCreator}
/>
)}
{!action.subjects.includes(subject.key) && '-'}
</Typography>
</TableCell>
</React.Fragment>
))}
</TableRow>
))}
</TableBody>
@@ -116,6 +133,7 @@ const PermissionCatalogField = ({
</TableContainer>
);
};
PermissionCatalogField.propTypes = {
name: PropTypes.string,
disabled: PropTypes.bool,

View File

@@ -6,10 +6,8 @@ export function getRoleWithComputedPermissions(role) {
[permission.subject]: {
...(computedPermissions[permission.subject] || {}),
[permission.action]: {
conditions: Object.fromEntries(
permission.conditions.map((condition) => [condition, true]),
),
value: true,
allEntities: permission.conditions.includes('isCreator') === false,
ownEntities: true,
},
},
}),
@@ -28,15 +26,19 @@ export function getPermissions(computedPermissions) {
(permissions, computedPermissionEntry) => {
const [subject, actionsWithConditions] = computedPermissionEntry;
for (const action in actionsWithConditions) {
const { value: permitted, conditions = {} } =
actionsWithConditions[action];
if (permitted) {
const { ownEntities, allEntities } = actionsWithConditions[action];
if (ownEntities && !allEntities) {
permissions.push({
action,
subject,
conditions: Object.entries(conditions)
.filter(([, enabled]) => enabled)
.map(([condition]) => condition),
conditions: ['isCreator'],
});
} else if (ownEntities && allEntities) {
permissions.push({
action,
subject,
conditions: [],
});
}
}
@@ -46,18 +48,9 @@ export function getPermissions(computedPermissions) {
);
}
export const getComputedPermissionsDefaultValues = (
data,
conditionsInitialValues,
) => {
export const getComputedPermissionsDefaultValues = (data) => {
if (!data) return {};
const conditions = {};
data.conditions.forEach((condition) => {
conditions[condition.key] =
conditionsInitialValues?.[condition.key] || false;
});
const result = {};
data.subjects.forEach((subject) => {
@@ -69,8 +62,8 @@ export const getComputedPermissionsDefaultValues = (
if (action.subjects.includes(subjectKey)) {
result[subjectKey][actionKey] = {
value: false,
conditions: { ...conditions },
ownEntities: false,
allEntities: false,
};
}
});

View File

@@ -403,5 +403,7 @@
"executionFilters.statusFilterSuccessfulOption": "Successful",
"executionFilters.statusFilterFailedOption": "Failed",
"executionFilters.startDateLabel": "Start Date",
"executionFilters.endDateLabel": "End Date"
"executionFilters.endDateLabel": "End Date",
"permissionCatalogField.ownEntitiesLabel": "(own entities)",
"permissionCatalogField.allEntitiesLabel": "(all entities)"
}

View File

@@ -81,7 +81,7 @@ export default function Application() {
'data-test': 'add-connection-button',
to: URLS.APP_ADD_CONNECTION(appKey, false),
disabled:
!currentUserAbility.can('create', 'Connection') ||
!currentUserAbility.can('manage', 'Connection') ||
appConfig?.data?.useOnlyPredefinedAuthClients === true ||
appConfig?.data?.disabled === true,
};
@@ -92,7 +92,7 @@ export default function Application() {
'data-test': 'add-connection-with-auth-client-button',
to: URLS.APP_ADD_CONNECTION(appKey, true),
disabled:
!currentUserAbility.can('create', 'Connection') ||
!currentUserAbility.can('manage', 'Connection') ||
appOAuthClients?.data?.length === 0 ||
appConfig?.data?.disabled === true,
};
@@ -139,7 +139,7 @@ export default function Application() {
<Route
path={`${URLS.FLOWS}/*`}
element={
<Can I="create" a="Flow" passThrough>
<Can I="manage" a="Flow" passThrough>
{(allowed) => (
<ConditionalIconButton
type="submit"
@@ -162,7 +162,7 @@ export default function Application() {
<Route
path={`${URLS.CONNECTIONS}/*`}
element={
<Can I="create" a="Connection" passThrough>
<Can I="manage" a="Connection" passThrough>
{(allowed) => (
<SplitButton
disabled={!allowed}
@@ -248,7 +248,7 @@ export default function Application() {
<Route
path="/connections/add"
element={
<Can I="create" a="Connection">
<Can I="manage" a="Connection">
<AddAppConnection
onClose={goToApplicationPage}
application={app}
@@ -260,7 +260,7 @@ export default function Application() {
<Route
path="/connections/:connectionId/reconnect"
element={
<Can I="create" a="Connection">
<Can I="manage" a="Connection">
<ReconnectConnection
application={app}
onClose={goToApplicationPage}

View File

@@ -53,7 +53,7 @@ export default function Applications() {
alignItems="center"
order={{ xs: 1, sm: 2 }}
>
<Can I="create" a="Connection" passThrough>
<Can I="manage" a="Connection" passThrough>
{(allowed) => (
<ConditionalIconButton
type="submit"
@@ -84,7 +84,7 @@ export default function Applications() {
)}
{!isLoading && !hasApps && (
<Can I="create" a="Connection" passThrough>
<Can I="manage" a="Connection" passThrough>
{(allowed) => (
<NoResultFound
text={formatMessage('apps.noConnections')}

View File

@@ -73,9 +73,6 @@ export default function CreateRole() {
description: '',
computedPermissions: getComputedPermissionsDefaultValues(
permissionCatalogData?.data,
{
isCreator: true,
},
),
}),
[permissionCatalogData],

View File

@@ -67,7 +67,7 @@ export default function CreateUser() {
const roles = rolesData?.data;
const queryClient = useQueryClient();
const currentUserAbility = useCurrentUserAbility();
const canUpdateRole = currentUserAbility.can('update', 'Role');
const canUpdateRole = currentUserAbility.can('manage', 'Role');
const handleUserCreation = async (userData) => {
try {
@@ -125,7 +125,7 @@ export default function CreateUser() {
helperText={errors?.email?.message}
/>
<Can I="update" a="Role">
<Can I="manage" a="Role">
<ControlledAutocomplete
name="roleId"
fullWidth

View File

@@ -78,6 +78,7 @@ export default function EditRole() {
try {
setPermissionError(null);
const newPermissions = getPermissions(roleData.computedPermissions);
await updateRole({
name: roleData.name,
description: roleData.description,

View File

@@ -73,7 +73,7 @@ export default function EditUser() {
const enqueueSnackbar = useEnqueueSnackbar();
const navigate = useNavigate();
const currentUserAbility = useCurrentUserAbility();
const canUpdateRole = currentUserAbility.can('update', 'Role');
const canUpdateRole = currentUserAbility.can('manage', 'Role');
const handleUserUpdate = async (userDataToUpdate) => {
try {
@@ -169,7 +169,7 @@ export default function EditUser() {
helperText={errors?.email?.message}
/>
<Can I="update" a="Role">
<Can I="manage" a="Role">
<ControlledAutocomplete
name="roleId"
fullWidth

View File

@@ -168,7 +168,7 @@ export default function Flows() {
{!isLoading && !navigateToLastPage && !hasFlows && (
<NoResultFound
text={formatMessage('flows.noFlows')}
{...(currentUserAbility.can('create', 'Flow') && {
{...(currentUserAbility.can('manage', 'Flow') && {
to: URLS.CREATE_FLOW,
})}
/>