[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

@ -1,6 +1,6 @@
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
export const SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS = {
export const OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS = {
apiKey: SettingPermissionType.API_KEYS_AND_WEBHOOKS,
webhook: SettingPermissionType.API_KEYS_AND_WEBHOOKS,
} as const;

View File

@ -15,7 +15,7 @@ import {
WorkspaceResolverBuilderMethodNames,
} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
import { SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/system-objects-permissions-requirements.constant';
import { OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS } from 'src/engine/api/graphql/graphql-query-runner/constants/objects-with-settings-permissions-requirements';
import { GraphqlQuerySelectedFieldsResult } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser';
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper';
@ -97,7 +97,7 @@ export abstract class GraphqlQueryBaseResolverService<
featureFlagsMap[FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED];
if (objectMetadataItemWithFieldMaps.isSystem === true) {
await this.validateSystemObjectPermissionsOrThrow(options);
await this.validateSettingsPermissionsOnObjectOrThrow(options);
} else {
if (!isPermissionsV2Enabled)
await this.validateObjectRecordPermissionsOrThrow({
@ -186,19 +186,19 @@ export abstract class GraphqlQueryBaseResolverService<
}
}
private async validateSystemObjectPermissionsOrThrow(
private async validateSettingsPermissionsOnObjectOrThrow(
options: WorkspaceQueryRunnerOptions,
) {
const { authContext, objectMetadataItemWithFieldMaps } = options;
if (
Object.keys(SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS).includes(
Object.keys(OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS).includes(
objectMetadataItemWithFieldMaps.nameSingular,
)
) {
const permissionRequired: SettingPermissionType =
// @ts-expect-error legacy noImplicitAny
SYSTEM_OBJECTS_PERMISSIONS_REQUIREMENTS[
OBJECTS_WITH_SETTINGS_PERMISSIONS_REQUIREMENTS[
objectMetadataItemWithFieldMaps.nameSingular
];

View File

@ -162,6 +162,10 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
{
req: {
workspaceId: executeParams?.[0].workspace.id,
userWorkspaceId: executeParams?.[0].userWorkspaceId,
apiKey: executeParams?.[0].apiKey,
workspaceMemberId: executeParams?.[0].workspaceMemberId,
user: executeParams?.[0].user,
},
},
contextId,

View File

@ -14,7 +14,8 @@ import { isDefined } from 'twenty-shared/utils';
import { WorkflowTriggerRestApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-rest-api-exception.filter';
import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard';
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
WorkflowVersionStatus,
WorkflowVersionWorkspaceEntity,
@ -28,10 +29,13 @@ import { WorkflowTriggerType } from 'src/modules/workflow/workflow-trigger/types
import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service';
@Controller('webhooks')
@UseFilters(WorkflowTriggerRestApiExceptionFilter)
@UseFilters(
WorkflowTriggerRestApiExceptionFilter,
PermissionsGraphqlApiExceptionFilter,
)
export class WorkflowTriggerController {
constructor(
private readonly twentyORMManager: TwentyORMManager,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService,
) {}
@ -68,8 +72,10 @@ export class WorkflowTriggerController {
workspaceId: string;
}) {
const workflowRepository =
await this.twentyORMManager.getRepository<WorkflowWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowWorkspaceEntity>(
workspaceId,
'workflow',
{ shouldBypassPermissionChecks: true },
);
const workflow = await workflowRepository.findOne({
@ -94,8 +100,10 @@ export class WorkflowTriggerController {
}
const workflowVersionRepository =
await this.twentyORMManager.getRepository<WorkflowVersionWorkspaceEntity>(
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowVersionWorkspaceEntity>(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersion = await workflowVersionRepository.findOne({
where: { id: workflow.lastPublishedVersionId },

View File

@ -7,14 +7,24 @@ import { ComputeStepOutputSchemaInput } from 'src/engine/core-modules/workflow/d
import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { OutputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/output-schema.type';
import { WorkflowSchemaWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service';
@Resolver()
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter)
@UseGuards(
WorkspaceAuthGuard,
UserAuthGuard,
SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS),
)
@UseFilters(
WorkflowTriggerGraphqlApiExceptionFilter,
PermissionsGraphqlApiExceptionFilter,
)
export class WorkflowBuilderResolver {
constructor(
private readonly workflowSchemaWorkspaceService: WorkflowSchemaWorkspaceService,

View File

@ -1,4 +1,4 @@
import { UseGuards } from '@nestjs/common';
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { CreateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow/dtos/create-workflow-version-step-input.dto';
@ -9,13 +9,21 @@ import { UpdateWorkflowVersionStepInput } from 'src/engine/core-modules/workflow
import { WorkflowActionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-step.dto';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-step/workflow-version-step.workspace-service';
import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service';
@Resolver()
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@UseGuards(
WorkspaceAuthGuard,
UserAuthGuard,
SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS),
)
@UseFilters(PermissionsGraphqlApiExceptionFilter)
export class WorkflowStepResolver {
constructor(
private readonly workflowVersionStepWorkspaceService: WorkflowVersionStepWorkspaceService,
@ -78,10 +86,12 @@ export class WorkflowStepResolver {
@Mutation(() => WorkflowActionDTO)
async updateWorkflowRunStep(
@AuthWorkspace() { id: workspaceId }: Workspace,
@Args('input')
{ workflowRunId, step }: UpdateWorkflowRunStepInput,
): Promise<WorkflowActionDTO> {
await this.workflowRunWorkspaceService.updateWorkflowRunStep({
workspaceId,
workflowRunId,
step,
});

View File

@ -1,20 +1,30 @@
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
import { User } from 'src/engine/core-modules/user/user.entity';
import { RunWorkflowVersionInput } from 'src/engine/core-modules/workflow/dtos/run-workflow-version-input.dto';
import { WorkflowRunDTO } from 'src/engine/core-modules/workflow/dtos/workflow-run.dto';
import { WorkflowTriggerGraphqlApiExceptionFilter } from 'src/engine/core-modules/workflow/filters/workflow-trigger-graphql-api-exception.filter';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspaceMemberId } from 'src/engine/decorators/auth/auth-workspace-member-id.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { WorkflowTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/workspace-services/workflow-trigger.workspace-service';
import { buildCreatedByFromFullNameMetadata } from 'src/engine/core-modules/actor/utils/build-created-by-from-full-name-metadata.util';
@Resolver()
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@UseFilters(WorkflowTriggerGraphqlApiExceptionFilter)
@UseGuards(
WorkspaceAuthGuard,
UserAuthGuard,
SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS),
)
@UseFilters(
WorkflowTriggerGraphqlApiExceptionFilter,
PermissionsGraphqlApiExceptionFilter,
)
export class WorkflowTriggerResolver {
constructor(
private readonly workflowTriggerWorkspaceService: WorkflowTriggerWorkspaceService,

View File

@ -1,16 +1,24 @@
import { UseGuards } from '@nestjs/common';
import { UseFilters, UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { CreateDraftFromWorkflowVersionInput } from 'src/engine/core-modules/workflow/dtos/create-draft-from-workflow-version-input';
import { WorkflowVersionDTO } from 'src/engine/core-modules/workflow/dtos/workflow-version.dto';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { WorkflowVersionWorkspaceService } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.workspace-service';
@Resolver()
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
@UseGuards(
WorkspaceAuthGuard,
UserAuthGuard,
SettingsPermissionsGuard(SettingPermissionType.WORKFLOWS),
)
@UseFilters(PermissionsGraphqlApiExceptionFilter)
export class WorkflowVersionResolver {
constructor(
private readonly workflowVersionWorkspaceService: WorkflowVersionWorkspaceService,

View File

@ -5,6 +5,7 @@ import { WorkflowBuilderResolver } from 'src/engine/core-modules/workflow/resolv
import { WorkflowStepResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-step.resolver';
import { WorkflowTriggerResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-trigger.resolver';
import { WorkflowVersionResolver } from 'src/engine/core-modules/workflow/resolvers/workflow-version.resolver';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowBuilderModule } from 'src/modules/workflow/workflow-builder/workflow-builder.module';
import { WorkflowVersionModule } from 'src/modules/workflow/workflow-builder/workflow-version/workflow-version.module';
@ -18,6 +19,7 @@ import { WorkflowTriggerModule } from 'src/modules/workflow/workflow-trigger/wor
WorkflowCommonModule,
WorkflowVersionModule,
WorkflowRunModule,
PermissionsModule,
],
controllers: [WorkflowTriggerController],
providers: [

View File

@ -6,4 +6,5 @@ export enum SettingPermissionType {
DATA_MODEL = 'DATA_MODEL',
ADMIN_PANEL = 'ADMIN_PANEL',
SECURITY = 'SECURITY',
WORKFLOWS = 'WORKFLOWS',
}

View File

@ -84,7 +84,7 @@ export class RemoteServerService<T extends RemoteServerType> {
const createdRemoteServer = entityManager.create(
RemoteServerEntity,
remoteServerToCreate,
);
) as RemoteServerEntity<RemoteServerType>;
const foreignDataWrapperQuery =
this.foreignDataWrapperServerQueryFactory.createForeignDataWrapperServer(

View File

@ -11,6 +11,7 @@ import { In, Repository } from 'typeorm';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UserWorkspaceRoleEntity } from 'src/engine/metadata-modules/role/user-workspace-role.entity';
import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type';
@ -18,6 +19,7 @@ import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-mod
import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception';
import { getFromCacheWithRecompute } from 'src/engine/utils/get-data-from-cache-with-recompute.util';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
type CacheResult<T, U> = {
version: T;
@ -244,7 +246,7 @@ export class WorkspacePermissionsCacheService {
workspaceId,
...(roleIds ? { id: In(roleIds) } : {}),
},
relations: ['objectPermissions'],
relations: ['objectPermissions', 'settingPermissions'],
});
const workspaceObjectMetadataCollection =
@ -256,7 +258,7 @@ export class WorkspacePermissionsCacheService {
const objectRecordsPermissions: ObjectRecordsPermissions = {};
for (const objectMetadata of workspaceObjectMetadataCollection) {
const { id: objectMetadataId, isSystem } = objectMetadata;
const { id: objectMetadataId, isSystem, standardId } = objectMetadata;
let canRead = role.canReadAllObjectRecords;
let canUpdate = role.canUpdateAllObjectRecords;
@ -264,32 +266,48 @@ export class WorkspacePermissionsCacheService {
let canDestroy = role.canDestroyAllObjectRecords;
if (isPermissionsV2Enabled) {
const objectRecordPermissionsOverride = role.objectPermissions.find(
(objectPermission) =>
objectPermission.objectMetadataId === objectMetadataId,
);
if (
standardId &&
[
STANDARD_OBJECT_IDS.workflow,
STANDARD_OBJECT_IDS.workflowRun,
STANDARD_OBJECT_IDS.workflowVersion,
].includes(standardId)
) {
const hasWorkflowsPermissions = this.hasWorkflowsPermissions(role);
const getPermissionValue = (
overrideValue: boolean | undefined,
defaultValue: boolean,
) => (isSystem ? true : (overrideValue ?? defaultValue));
canRead = hasWorkflowsPermissions;
canUpdate = hasWorkflowsPermissions;
canSoftDelete = hasWorkflowsPermissions;
canDestroy = hasWorkflowsPermissions;
} else {
const objectRecordPermissionsOverride = role.objectPermissions.find(
(objectPermission) =>
objectPermission.objectMetadataId === objectMetadataId,
);
canRead = getPermissionValue(
objectRecordPermissionsOverride?.canReadObjectRecords,
canRead,
);
canUpdate = getPermissionValue(
objectRecordPermissionsOverride?.canUpdateObjectRecords,
canUpdate,
);
canSoftDelete = getPermissionValue(
objectRecordPermissionsOverride?.canSoftDeleteObjectRecords,
canSoftDelete,
);
canDestroy = getPermissionValue(
objectRecordPermissionsOverride?.canDestroyObjectRecords,
canDestroy,
);
const getPermissionValue = (
overrideValue: boolean | undefined,
defaultValue: boolean,
) => (isSystem ? true : (overrideValue ?? defaultValue));
canRead = getPermissionValue(
objectRecordPermissionsOverride?.canReadObjectRecords,
canRead,
);
canUpdate = getPermissionValue(
objectRecordPermissionsOverride?.canUpdateObjectRecords,
canUpdate,
);
canSoftDelete = getPermissionValue(
objectRecordPermissionsOverride?.canSoftDeleteObjectRecords,
canSoftDelete,
);
canDestroy = getPermissionValue(
objectRecordPermissionsOverride?.canDestroyObjectRecords,
canDestroy,
);
}
}
objectRecordsPermissions[objectMetadataId] = {
@ -298,9 +316,9 @@ export class WorkspacePermissionsCacheService {
canSoftDelete,
canDestroy,
};
}
permissionsByRoleId[role.id] = objectRecordsPermissions;
permissionsByRoleId[role.id] = objectRecordsPermissions;
}
}
return permissionsByRoleId;
@ -313,7 +331,7 @@ export class WorkspacePermissionsCacheService {
where: {
workspaceId,
},
select: ['id', 'isSystem'],
select: ['id', 'isSystem', 'standardId'],
});
return workspaceObjectMetadata;
@ -336,4 +354,19 @@ export class WorkspacePermissionsCacheService {
return acc;
}, {} as UserWorkspaceRoleMap);
}
private hasWorkflowsPermissions(role: RoleEntity): boolean {
const hasWorkflowsPermissionFromRole = role.canUpdateAllSettings;
const hasWorkflowsPermissionsFromSettingPermissions = isDefined(
role.settingPermissions.find(
(settingPermission) =>
settingPermission.setting === SettingPermissionType.WORKFLOWS,
),
);
return (
hasWorkflowsPermissionFromRole ||
hasWorkflowsPermissionsFromSettingPermissions
);
}
}