[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:
+4
-1
@@ -14,11 +14,14 @@ export class WorkflowVersionDeleteOnePreQueryHook
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
_authContext: AuthContext,
|
||||
authContext: AuthContext,
|
||||
_objectName: string,
|
||||
payload: DeleteOneResolverArgs,
|
||||
): Promise<DeleteOneResolverArgs> {
|
||||
const { workspace } = authContext;
|
||||
|
||||
await this.workflowVersionValidationWorkspaceService.validateWorkflowVersionForDeleteOne(
|
||||
workspace.id,
|
||||
payload,
|
||||
);
|
||||
|
||||
|
||||
+7
-2
@@ -15,12 +15,17 @@ export class WorkflowVersionUpdateOnePreQueryHook
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
_authContext: AuthContext,
|
||||
authContext: AuthContext,
|
||||
_objectName: string,
|
||||
payload: UpdateOneResolverArgs<WorkflowVersionWorkspaceEntity>,
|
||||
): Promise<UpdateOneResolverArgs<WorkflowVersionWorkspaceEntity>> {
|
||||
const { workspace } = authContext;
|
||||
|
||||
await this.workflowVersionValidationWorkspaceService.validateWorkflowVersionForUpdateOne(
|
||||
payload,
|
||||
{
|
||||
workspaceId: workspace.id,
|
||||
payload,
|
||||
},
|
||||
);
|
||||
|
||||
return payload;
|
||||
|
||||
+22
-10
@@ -1,10 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
import { ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import { getObjectMetadataMapItemByNameSingular } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-singular.util';
|
||||
import { WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
|
||||
import {
|
||||
WorkflowCommonException,
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
WorkflowTriggerException,
|
||||
WorkflowTriggerExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-trigger/exceptions/workflow-trigger.exception';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
|
||||
export type ObjectMetadataInfo = {
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
|
||||
@@ -28,14 +28,18 @@ export type ObjectMetadataInfo = {
|
||||
@Injectable()
|
||||
export class WorkflowCommonWorkspaceService {
|
||||
constructor(
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||
) {}
|
||||
|
||||
async getWorkflowVersionOrFail(
|
||||
workflowVersionId: string,
|
||||
): Promise<WorkflowVersionWorkspaceEntity> {
|
||||
async getWorkflowVersionOrFail({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
workflowVersionId: string;
|
||||
}): Promise<WorkflowVersionWorkspaceEntity> {
|
||||
if (!workflowVersionId) {
|
||||
throw new WorkflowTriggerException(
|
||||
'Workflow version ID is required',
|
||||
@@ -44,8 +48,10 @@ export class WorkflowCommonWorkspaceService {
|
||||
}
|
||||
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
|
||||
);
|
||||
|
||||
const workflowVersion = await workflowVersionRepository.findOne({
|
||||
@@ -124,18 +130,24 @@ export class WorkflowCommonWorkspaceService {
|
||||
operation: 'restore' | 'delete' | 'destroy';
|
||||
}): Promise<void> {
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
|
||||
);
|
||||
|
||||
const workflowRunRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowRunWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
|
||||
);
|
||||
|
||||
const workflowAutomatedTriggerRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowAutomatedTriggerWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowAutomatedTriggerWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowAutomatedTrigger',
|
||||
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
|
||||
);
|
||||
|
||||
for (const workflowId of workflowIds) {
|
||||
|
||||
+28
-14
@@ -8,7 +8,7 @@ import {
|
||||
UpdateOneResolverArgs,
|
||||
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import {
|
||||
WorkflowQueryValidationException,
|
||||
WorkflowQueryValidationExceptionCode,
|
||||
@@ -24,10 +24,11 @@ import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/work
|
||||
export class WorkflowVersionValidationWorkspaceService {
|
||||
constructor(
|
||||
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
) {}
|
||||
|
||||
async validateWorkflowVersionForCreateOne(
|
||||
workspaceId: string,
|
||||
payload: CreateOneResolverArgs<WorkflowVersionWorkspaceEntity>,
|
||||
) {
|
||||
if (
|
||||
@@ -41,8 +42,10 @@ export class WorkflowVersionValidationWorkspaceService {
|
||||
}
|
||||
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
|
||||
);
|
||||
|
||||
const workflowAlreadyHasDraftVersion =
|
||||
@@ -63,13 +66,18 @@ export class WorkflowVersionValidationWorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
async validateWorkflowVersionForUpdateOne(
|
||||
payload: UpdateOneResolverArgs<WorkflowVersionWorkspaceEntity>,
|
||||
) {
|
||||
async validateWorkflowVersionForUpdateOne({
|
||||
workspaceId,
|
||||
payload,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
payload: UpdateOneResolverArgs<WorkflowVersionWorkspaceEntity>;
|
||||
}) {
|
||||
const workflowVersion =
|
||||
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail(
|
||||
payload.id,
|
||||
);
|
||||
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
|
||||
workspaceId,
|
||||
workflowVersionId: payload.id,
|
||||
});
|
||||
|
||||
// If the only field updated is the name, we can update the workflow version
|
||||
// Otherwise, we need to assert that the workflow version is a draft
|
||||
@@ -93,17 +101,23 @@ export class WorkflowVersionValidationWorkspaceService {
|
||||
}
|
||||
}
|
||||
|
||||
async validateWorkflowVersionForDeleteOne(payload: DeleteOneResolverArgs) {
|
||||
async validateWorkflowVersionForDeleteOne(
|
||||
workspaceId: string,
|
||||
payload: DeleteOneResolverArgs,
|
||||
) {
|
||||
const workflowVersion =
|
||||
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail(
|
||||
payload.id,
|
||||
);
|
||||
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
|
||||
workspaceId,
|
||||
workflowVersionId: payload.id,
|
||||
});
|
||||
|
||||
assertWorkflowVersionIsDraft(workflowVersion);
|
||||
|
||||
const workflowVersionRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowVersion',
|
||||
{ shouldBypassPermissionChecks: true }, // settings permissions are checked at resolver-level
|
||||
);
|
||||
|
||||
const otherWorkflowVersionsExist = await workflowVersionRepository.exists({
|
||||
|
||||
Reference in New Issue
Block a user