[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:
+25
-7
@@ -37,21 +37,25 @@ export class RunWorkflowJob {
|
||||
workflowRunId,
|
||||
payload,
|
||||
lastExecutedStepId,
|
||||
workspaceId,
|
||||
}: RunWorkflowJobData): Promise<void> {
|
||||
try {
|
||||
if (lastExecutedStepId) {
|
||||
await this.resumeWorkflowExecution({
|
||||
workspaceId,
|
||||
workflowRunId,
|
||||
lastExecutedStepId,
|
||||
});
|
||||
} else {
|
||||
await this.startWorkflowExecution({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
payload: payload ?? {},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
||||
workspaceId,
|
||||
workflowRunId,
|
||||
status: WorkflowRunStatus.FAILED,
|
||||
error: error.message,
|
||||
@@ -61,9 +65,11 @@ export class RunWorkflowJob {
|
||||
|
||||
private async startWorkflowExecution({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
payload,
|
||||
}: {
|
||||
workflowRunId: string;
|
||||
workspaceId: string;
|
||||
payload: object;
|
||||
}): Promise<void> {
|
||||
const context = {
|
||||
@@ -71,14 +77,16 @@ export class RunWorkflowJob {
|
||||
};
|
||||
|
||||
const workflowRun =
|
||||
await this.workflowRunWorkspaceService.getWorkflowRunOrFail(
|
||||
await this.workflowRunWorkspaceService.getWorkflowRunOrFail({
|
||||
workflowRunId,
|
||||
);
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const workflowVersion =
|
||||
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail(
|
||||
workflowRun.workflowVersionId,
|
||||
);
|
||||
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
|
||||
workspaceId,
|
||||
workflowVersionId: workflowRun.workflowVersionId,
|
||||
});
|
||||
|
||||
if (!workflowVersion.trigger || !workflowVersion.steps) {
|
||||
throw new WorkflowRunException(
|
||||
@@ -89,6 +97,7 @@ export class RunWorkflowJob {
|
||||
|
||||
await this.workflowRunWorkspaceService.startWorkflowRun({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
context,
|
||||
output: {
|
||||
flow: {
|
||||
@@ -110,20 +119,24 @@ export class RunWorkflowJob {
|
||||
currentStepId: workflowVersion.steps[0].id,
|
||||
steps: workflowVersion.steps,
|
||||
context,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
private async resumeWorkflowExecution({
|
||||
workflowRunId,
|
||||
lastExecutedStepId,
|
||||
workspaceId,
|
||||
}: {
|
||||
workflowRunId: string;
|
||||
lastExecutedStepId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
const workflowRun =
|
||||
await this.workflowRunWorkspaceService.getWorkflowRunOrFail(
|
||||
await this.workflowRunWorkspaceService.getWorkflowRunOrFail({
|
||||
workflowRunId,
|
||||
);
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (workflowRun.status !== WorkflowRunStatus.RUNNING) {
|
||||
throw new WorkflowRunException(
|
||||
@@ -148,6 +161,7 @@ export class RunWorkflowJob {
|
||||
if (!nextStepId) {
|
||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
status: WorkflowRunStatus.COMPLETED,
|
||||
});
|
||||
|
||||
@@ -159,6 +173,7 @@ export class RunWorkflowJob {
|
||||
currentStepId: nextStepId,
|
||||
steps: workflowRun.output?.flow?.steps ?? [],
|
||||
context: workflowRun.context ?? {},
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,12 +182,14 @@ export class RunWorkflowJob {
|
||||
currentStepId,
|
||||
steps,
|
||||
context,
|
||||
workspaceId,
|
||||
}: {
|
||||
workflowRunId: string;
|
||||
currentStepId: string;
|
||||
steps: WorkflowAction[];
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context: Record<string, any>;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const { error, pendingEvent } =
|
||||
await this.workflowExecutorWorkspaceService.execute({
|
||||
@@ -188,6 +205,7 @@ export class RunWorkflowJob {
|
||||
|
||||
await this.workflowRunWorkspaceService.endWorkflowRun({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
status: error ? WorkflowRunStatus.FAILED : WorkflowRunStatus.COMPLETED,
|
||||
error,
|
||||
});
|
||||
|
||||
+54
-26
@@ -9,7 +9,7 @@ import { RecordPositionService } from 'src/engine/core-modules/record-position/s
|
||||
import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter';
|
||||
import {
|
||||
StepOutput,
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
WorkflowRunStatus,
|
||||
WorkflowRunWorkspaceEntity,
|
||||
} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity';
|
||||
import { WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import { WorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
|
||||
import {
|
||||
@@ -28,7 +27,7 @@ import {
|
||||
@Injectable()
|
||||
export class WorkflowRunWorkspaceService {
|
||||
constructor(
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
private readonly workspaceEventEmitter: WorkspaceEventEmitter,
|
||||
@@ -44,19 +43,34 @@ export class WorkflowRunWorkspaceService {
|
||||
workflowVersionId: string;
|
||||
createdBy: ActorMetadata;
|
||||
}) {
|
||||
const workspaceId =
|
||||
this.scopedWorkspaceContextFactory.create()?.workspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new WorkflowRunException(
|
||||
'Workspace id is invalid',
|
||||
WorkflowRunExceptionCode.WORKFLOW_RUN_INVALID,
|
||||
);
|
||||
}
|
||||
|
||||
const workflowRunRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowRunWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowVersion =
|
||||
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail(
|
||||
await this.workflowCommonWorkspaceService.getWorkflowVersionOrFail({
|
||||
workspaceId,
|
||||
workflowVersionId,
|
||||
);
|
||||
});
|
||||
|
||||
const workflowRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflow',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflow = await workflowRepository.findOne({
|
||||
@@ -78,16 +92,6 @@ export class WorkflowRunWorkspaceService {
|
||||
},
|
||||
});
|
||||
|
||||
const workspaceId =
|
||||
this.scopedWorkspaceContextFactory.create()?.workspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new WorkflowRunException(
|
||||
'Workspace id is invalid',
|
||||
WorkflowRunExceptionCode.WORKFLOW_RUN_INVALID,
|
||||
);
|
||||
}
|
||||
|
||||
const position = await this.recordPositionService.buildRecordPosition({
|
||||
value: 'first',
|
||||
objectMetadata: {
|
||||
@@ -111,17 +115,21 @@ export class WorkflowRunWorkspaceService {
|
||||
|
||||
async startWorkflowRun({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
context,
|
||||
output,
|
||||
}: {
|
||||
workflowRunId: string;
|
||||
workspaceId: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context: Record<string, any>;
|
||||
output: WorkflowRunOutput;
|
||||
}) {
|
||||
const workflowRunRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowRunWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowRunToUpdate = await workflowRunRepository.findOneBy({
|
||||
@@ -159,16 +167,20 @@ export class WorkflowRunWorkspaceService {
|
||||
|
||||
async endWorkflowRun({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
status,
|
||||
error,
|
||||
}: {
|
||||
workflowRunId: string;
|
||||
workspaceId: string;
|
||||
status: WorkflowRunStatus;
|
||||
error?: string;
|
||||
}) {
|
||||
const workflowRunRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowRunWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowRunToUpdate = await workflowRunRepository.findOneBy({
|
||||
@@ -202,16 +214,20 @@ export class WorkflowRunWorkspaceService {
|
||||
async saveWorkflowRunState({
|
||||
workflowRunId,
|
||||
stepOutput,
|
||||
workspaceId,
|
||||
context,
|
||||
}: {
|
||||
workflowRunId: string;
|
||||
stepOutput: StepOutput;
|
||||
workspaceId: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context: Record<string, any>;
|
||||
}) {
|
||||
const workflowRunRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowRunWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowRunToUpdate = await workflowRunRepository.findOneBy({
|
||||
@@ -250,13 +266,17 @@ export class WorkflowRunWorkspaceService {
|
||||
async updateWorkflowRunStep({
|
||||
workflowRunId,
|
||||
step,
|
||||
workspaceId,
|
||||
}: {
|
||||
workflowRunId: string;
|
||||
step: WorkflowAction;
|
||||
workspaceId: string;
|
||||
}) {
|
||||
const workflowRunRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowRunWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowRunToUpdate = await workflowRunRepository.findOneBy({
|
||||
@@ -302,12 +322,18 @@ export class WorkflowRunWorkspaceService {
|
||||
});
|
||||
}
|
||||
|
||||
async getWorkflowRunOrFail(
|
||||
workflowRunId: string,
|
||||
): Promise<WorkflowRunWorkspaceEntity> {
|
||||
async getWorkflowRunOrFail({
|
||||
workflowRunId,
|
||||
workspaceId,
|
||||
}: {
|
||||
workflowRunId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<WorkflowRunWorkspaceEntity> {
|
||||
const workflowRunRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowRunWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowRunWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowRun = await workflowRunRepository.findOne({
|
||||
@@ -353,8 +379,10 @@ export class WorkflowRunWorkspaceService {
|
||||
}
|
||||
|
||||
const workflowRunRepository =
|
||||
await this.twentyORMManager.getRepository<WorkflowRunWorkspaceEntity>(
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
'workflowRun',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflowRunAfter = await workflowRunRepository.findOneBy({
|
||||
|
||||
Reference in New Issue
Block a user