[permissions] permissions and workflows (#12436)

In this PR

- Determine object record permissions on workflows objects (workflow,
workflowVersion, workflowRun) base on settings permissions @Weiko
- Add Workflow permission guards on workflow resolvers @thomtrp . **Any
method within a resolver that has the SettingsPermission Guard is only
callable by a apiKey or a user that has the permission** (so not by
external parties).
- Add checks bypass in workflow services since 1) for actions gated by
settings permissions, the gate should be done at resolver level, so it
will have been done before the call to the service 2) some service
methods may be called by workflowTriggerController which is callable by
external parties without permissions (ex:
workflowCommonWorkspaceService.getWorkflowVersionOrFail). This is
something we may want to change in the future (still to discuss), by
removing the guard at resolver-level and relying on
shouldBypassPermissionChecks at getRepository and made in a way that we
only bypass for external parties.
- Add checks bypass for actions performed by workflows since they should
not be restricted in our current vision
- Add tests
This commit is contained in:
Marie
2025-06-11 18:47:29 +02:00
committed by GitHub
parent e33f2fadd8
commit 04dd0e50bb
34 changed files with 864 additions and 215 deletions

View File

@ -0,0 +1,15 @@
export const WORKFLOW_GQL_FIELDS = `
id
name
lastPublishedVersionId
statuses
position
createdBy {
source
workspaceMemberId
name
}
createdAt
updatedAt
deletedAt
`;

View File

@ -1,6 +1,7 @@
import { print } from 'graphql';
import request from 'supertest';
import { deleteOneRoleOperationFactory } from 'test/integration/graphql/utils/delete-one-role-operation-factory.util';
import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
import { updateWorkspaceMemberRole } from 'test/integration/graphql/utils/update-workspace-member-role.util';
@ -84,7 +85,7 @@ describe('Granular settings permissions', () => {
mutation UpsertSettingPermissions {
upsertSettingPermissions(upsertSettingPermissionsInput: {
roleId: "${customRoleId}"
settingPermissionKeys: [${SettingPermissionType.DATA_MODEL}, ${SettingPermissionType.WORKSPACE}]
settingPermissionKeys: [${SettingPermissionType.DATA_MODEL}, ${SettingPermissionType.WORKSPACE}, ${SettingPermissionType.WORKFLOWS}]
}) {
id
setting
@ -243,6 +244,48 @@ describe('Granular settings permissions', () => {
});
});
describe('Workflows Permissions', () => {
it('should allow access to workflows operations when user has WORKFLOWS setting permission', async () => {
// Test creating a workflow (requires WORKFLOWS permission)
const createWorkflowQuery = {
query: `
mutation CreateWorkflow {
createWorkflow(data: {
name: "Test Workflow"
}) {
id
name
}
}
`,
};
const response = await client
.post('/graphql')
.set('Authorization', `Bearer ${MEMBER_ACCESS_TOKEN}`)
.send(createWorkflowQuery);
expect(response.status).toBe(200);
expect(response.body.errors).toBeUndefined();
expect(response.body.data.createWorkflow).toBeDefined();
expect(response.body.data.createWorkflow.name).toBe('Test Workflow');
// Clean up - delete the created workflow
const graphqlOperation = destroyOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: `
id
`,
recordId: response.body.data.createWorkflow.id,
});
await client
.post('/graphql')
.set('Authorization', `Bearer ${ADMIN_ACCESS_TOKEN}`)
.send(graphqlOperation);
});
});
describe('Denied Permissions', () => {
it('should deny access to roles operations when user does not have ROLES setting permission', async () => {
// Test creating a role (requires ROLES permission, which our custom role doesn't have)
@ -354,7 +397,7 @@ describe('Granular settings permissions', () => {
expect(customRole).toBeDefined();
expect(customRole.canUpdateAllSettings).toBe(false);
expect(customRole.settingPermissions).toHaveLength(2);
expect(customRole.settingPermissions).toHaveLength(3);
expect(
customRole.settingPermissions.map((p: any) => p.setting),
).toContain(SettingPermissionType.DATA_MODEL);

View File

@ -0,0 +1,370 @@
import { randomUUID } from 'node:crypto';
import { WORKFLOW_GQL_FIELDS } from 'test/integration/constants/workflow-gql-fields.constants';
import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util';
import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util';
import { makeGraphqlAPIRequestWithApiKey } from 'test/integration/graphql/utils/make-graphql-api-request-with-api-key.util';
import { makeGraphqlAPIRequestWithGuestRole } from 'test/integration/graphql/utils/make-graphql-api-request-with-guest-role.util';
import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util';
import { updateFeatureFlagFactory } from 'test/integration/graphql/utils/update-feature-flag-factory.util';
import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util';
import { ErrorCode } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util';
describe('workflowsPermissions', () => {
describe('createOne workflow', () => {
describe('permissions V2 disabled', () => {
it('should throw a permission error when user does not have permission (guest role)', async () => {
const workflowId = randomUUID();
const graphqlOperation = createOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
data: {
id: workflowId,
name: 'Test Workflow',
},
});
const response =
await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
expect(response.body.data).toStrictEqual({ createWorkflow: null });
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(
ErrorCode.FORBIDDEN,
);
});
it('should create a workflow when user has permission (admin role)', async () => {
const workflowId = randomUUID();
const graphqlOperation = createOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
data: {
id: workflowId,
name: 'Test Workflow Admin',
},
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.createWorkflow).toBeDefined();
expect(response.body.data.createWorkflow.id).toBe(workflowId);
expect(response.body.data.createWorkflow.name).toBe(
'Test Workflow Admin',
);
// Clean up - delete the created workflow
const destroyWorkflowOperation = destroyOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: `
id
`,
recordId: response.body.data.createWorkflow.id,
});
await makeGraphqlAPIRequest(destroyWorkflowOperation);
});
});
describe('permissions V2 enabled', () => {
beforeAll(async () => {
const enablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IS_PERMISSIONS_V2_ENABLED',
true,
);
await makeGraphqlAPIRequest(enablePermissionsQuery);
});
afterAll(async () => {
const disablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IS_PERMISSIONS_V2_ENABLED',
false,
);
await makeGraphqlAPIRequest(disablePermissionsQuery);
});
it('should throw a permission error when user does not have permission (guest role)', async () => {
const workflowId = randomUUID();
const graphqlOperation = createOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
data: {
id: workflowId,
name: 'Test Workflow V2',
},
});
const response =
await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
expect(response.body.data).toStrictEqual({ createWorkflow: null });
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(
ErrorCode.FORBIDDEN,
);
});
it('should create a workflow when user has permission (admin role)', async () => {
const workflowId = randomUUID();
const graphqlOperation = createOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
data: {
id: workflowId,
name: 'Test Workflow Admin',
},
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.createWorkflow).toBeDefined();
expect(response.body.data.createWorkflow.id).toBe(workflowId);
expect(response.body.data.createWorkflow.name).toBe(
'Test Workflow Admin',
);
// Clean up - delete the created workflow
const destroyWorkflowOperation = destroyOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: `
id
`,
recordId: response.body.data.createWorkflow.id,
});
await makeGraphqlAPIRequest(destroyWorkflowOperation);
});
it('should create a workflow when executed by api key', async () => {
const workflowId = randomUUID();
const graphqlOperation = createOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
data: {
id: workflowId,
name: 'Test Workflow API Key',
},
});
const response =
await makeGraphqlAPIRequestWithApiKey(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.createWorkflow).toBeDefined();
expect(response.body.data.createWorkflow.id).toBe(workflowId);
expect(response.body.data.createWorkflow.name).toBe(
'Test Workflow API Key',
);
// Clean up - delete the created workflow
const destroyWorkflowOperation = destroyOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: `
id
`,
recordId: response.body.data.createWorkflow.id,
});
await makeGraphqlAPIRequest(destroyWorkflowOperation);
});
});
});
describe('updateOne workflow', () => {
describe('permissions V2 disabled', () => {
const workflowId = randomUUID();
beforeAll(async () => {
const createWorkflowOperation = createOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
data: {
id: workflowId,
name: 'Original Workflow Name',
},
});
await makeGraphqlAPIRequest(createWorkflowOperation);
});
afterAll(async () => {
const destroyWorkflowOperation = destroyOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: `
id
`,
recordId: workflowId,
});
await makeGraphqlAPIRequest(destroyWorkflowOperation);
});
it('should throw a permission error when user does not have permission (guest role)', async () => {
const graphqlOperation = updateOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
recordId: workflowId,
data: {
name: 'Updated Workflow Name Guest',
},
});
const response =
await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
expect(response.body.data).toStrictEqual({ updateWorkflow: null });
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(
ErrorCode.FORBIDDEN,
);
});
it('should update a workflow when user has permission (admin role)', async () => {
const graphqlOperation = updateOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
recordId: workflowId,
data: {
name: 'Updated Workflow Name Admin',
},
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.updateWorkflow).toBeDefined();
expect(response.body.data.updateWorkflow.id).toBe(workflowId);
expect(response.body.data.updateWorkflow.name).toBe(
'Updated Workflow Name Admin',
);
});
});
describe('permissions V2 enabled', () => {
const workflowId = randomUUID();
beforeAll(async () => {
const createWorkflowOperation = createOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
data: {
id: workflowId,
name: 'Original Workflow V2',
},
});
await makeGraphqlAPIRequest(createWorkflowOperation);
const enablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IS_PERMISSIONS_V2_ENABLED',
true,
);
await makeGraphqlAPIRequest(enablePermissionsQuery);
});
afterAll(async () => {
const destroyWorkflowOperation = destroyOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: `
id
`,
recordId: workflowId,
});
await makeGraphqlAPIRequest(destroyWorkflowOperation);
const disablePermissionsQuery = updateFeatureFlagFactory(
SEED_APPLE_WORKSPACE_ID,
'IS_PERMISSIONS_V2_ENABLED',
false,
);
await makeGraphqlAPIRequest(disablePermissionsQuery);
});
it('should throw a permission error when user does not have permission (guest role)', async () => {
const graphqlOperation = updateOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
recordId: workflowId,
data: {
name: 'Updated Workflow V2 Guest',
},
});
const response =
await makeGraphqlAPIRequestWithGuestRole(graphqlOperation);
expect(response.body.data).toStrictEqual({ updateWorkflow: null });
expect(response.body.errors).toBeDefined();
expect(response.body.errors[0].message).toBe(
PermissionsExceptionMessage.PERMISSION_DENIED,
);
expect(response.body.errors[0].extensions.code).toBe(
ErrorCode.FORBIDDEN,
);
});
it('should update a workflow when user has permission (admin role)', async () => {
const graphqlOperation = updateOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
recordId: workflowId,
data: {
name: 'Updated Workflow V2 Admin',
},
});
const response = await makeGraphqlAPIRequest(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.updateWorkflow).toBeDefined();
expect(response.body.data.updateWorkflow.id).toBe(workflowId);
expect(response.body.data.updateWorkflow.name).toBe(
'Updated Workflow V2 Admin',
);
});
it('should update a workflow when executed by api key', async () => {
const graphqlOperation = updateOneOperationFactory({
objectMetadataSingularName: 'workflow',
gqlFields: WORKFLOW_GQL_FIELDS,
recordId: workflowId,
data: {
name: 'Updated Workflow API Key',
},
});
const response =
await makeGraphqlAPIRequestWithApiKey(graphqlOperation);
expect(response.body.data).toBeDefined();
expect(response.body.data.updateWorkflow).toBeDefined();
expect(response.body.data.updateWorkflow.id).toBe(workflowId);
expect(response.body.data.updateWorkflow.name).toBe(
'Updated Workflow API Key',
);
});
});
});
});