refactor(core): Move typeorm operators from various sources into repositories (no-changelog) (#8174)

Follow-up to: #8165
This commit is contained in:
Iván Ovejero
2023-12-28 13:14:10 +01:00
committed by GitHub
parent 405e26757e
commit e418d42450
17 changed files with 185 additions and 209 deletions

View File

@@ -31,7 +31,6 @@ import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import type { WorkflowExecute } from 'n8n-core'; import type { WorkflowExecute } from 'n8n-core';
import type PCancelable from 'p-cancelable'; import type PCancelable from 'p-cancelable';
import type { FindOperator } from 'typeorm';
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';
@@ -606,8 +605,6 @@ export interface IWorkflowStatisticsDataLoaded {
dataLoaded: boolean; dataLoaded: boolean;
} }
export type WhereClause = Record<string, { [key: string]: string | FindOperator<unknown> }>;
// ---------------------------------- // ----------------------------------
// community nodes // community nodes
// ---------------------------------- // ----------------------------------

View File

@@ -1,9 +1,4 @@
import { In } from 'typeorm';
import { Container } from 'typedi'; import { Container } from 'typedi';
import type { Scope } from '@n8n/permissions';
import type { WhereClause } from '@/Interfaces';
import type { User } from '@db/entities/User';
import { License } from '@/License'; import { License } from '@/License';
export function isSharingEnabled(): boolean { export function isSharingEnabled(): boolean {
@@ -29,32 +24,3 @@ export function rightDiff<T1, T2>(
return acc; return acc;
}, []); }, []);
} }
/**
* Build a `where` clause for a TypeORM entity search,
* checking for member access if the user is not an owner.
*/
export function whereClause({
user,
entityType,
globalScope,
entityId = '',
roles = [],
}: {
user: User;
entityType: 'workflow' | 'credentials';
globalScope: Scope;
entityId?: string;
roles?: string[];
}): WhereClause {
const where: WhereClause = entityId ? { [entityType]: { id: entityId } } : {};
if (!user.hasGlobalScope(globalScope)) {
where.user = { id: user.id };
if (roles?.length) {
where.role = { name: In(roles) };
}
}
return where;
}

View File

@@ -418,7 +418,7 @@ export async function replaceInvalidCredentials(workflow: WorkflowEntity): Promi
/** /**
* Get the IDs of the workflows that have been shared with the user. * Get the IDs of the workflows that have been shared with the user.
* Returns all IDs if user has the 'workflow:read' scope (see `whereClause`) * Returns all IDs if user has the 'workflow:read' scope.
*/ */
export async function getSharedWorkflowIds(user: User, roles?: RoleNames[]): Promise<string[]> { export async function getSharedWorkflowIds(user: User, roles?: RoleNames[]): Promise<string[]> {
const where: FindOptionsWhere<SharedWorkflow> = {}; const where: FindOptionsWhere<SharedWorkflow> = {};

View File

@@ -1,5 +1,4 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Not } from 'typeorm';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { User } from '@db/entities/User'; import { User } from '@db/entities/User';
import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { CredentialsRepository } from '@db/repositories/credentials.repository';
@@ -25,20 +24,16 @@ export class Reset extends BaseCommand {
async run(): Promise<void> { async run(): Promise<void> {
const owner = await this.getInstanceOwner(); const owner = await this.getInstanceOwner();
const ownerWorkflowRole = await Container.get(RoleService).findWorkflowOwnerRole(); const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole();
const ownerCredentialRole = await Container.get(RoleService).findCredentialOwnerRole(); const credentialOwnerRole = await Container.get(RoleService).findCredentialOwnerRole();
await Container.get(SharedWorkflowRepository).update( await Container.get(SharedWorkflowRepository).makeOwnerOfAllWorkflows(owner, workflowOwnerRole);
{ userId: Not(owner.id), roleId: ownerWorkflowRole.id }, await Container.get(SharedCredentialsRepository).makeOwnerOfAllCredentials(
{ user: owner }, owner,
credentialOwnerRole,
); );
await Container.get(SharedCredentialsRepository).update( await Container.get(UserRepository).deleteAllExcept(owner);
{ userId: Not(owner.id), roleId: ownerCredentialRole.id },
{ user: owner },
);
await Container.get(UserRepository).delete({ id: Not(owner.id) });
await Container.get(UserRepository).save(Object.assign(owner, defaultUserProps)); await Container.get(UserRepository).save(Object.assign(owner, defaultUserProps));
const danglingCredentials: CredentialsEntity[] = await Container.get(CredentialsRepository) const danglingCredentials: CredentialsEntity[] = await Container.get(CredentialsRepository)
@@ -50,7 +45,7 @@ export class Reset extends BaseCommand {
Container.get(SharedCredentialsRepository).create({ Container.get(SharedCredentialsRepository).create({
credentials, credentials,
user: owner, user: owner,
role: ownerCredentialRole, role: credentialOwnerRole,
}), }),
); );
await Container.get(SharedCredentialsRepository).save(newSharedCredentials); await Container.get(SharedCredentialsRepository).save(newSharedCredentials);

View File

@@ -2,8 +2,6 @@ import { Get, RestController } from '@/decorators';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { In } from 'typeorm';
import { WebhookEntity } from '@/databases/entities/WebhookEntity';
@RestController('/debug') @RestController('/debug')
export class DebugController { export class DebugController {
@@ -17,16 +15,11 @@ export class DebugController {
async getMultiMainSetupDetails() { async getMultiMainSetupDetails() {
const leaderKey = await this.multiMainSetup.fetchLeaderKey(); const leaderKey = await this.multiMainSetup.fetchLeaderKey();
const triggersAndPollers = await this.workflowRepository.find({ const triggersAndPollers = await this.workflowRepository.findIn(
select: ['id', 'name'], this.activeWorkflowRunner.allActiveInMemory(),
where: { id: In(this.activeWorkflowRunner.allActiveInMemory()) }, );
});
const webhooks = (await this.workflowRepository const webhooks = await this.workflowRepository.findWebhookBasedActiveWorkflows();
.createQueryBuilder('workflow')
.select('DISTINCT workflow.id, workflow.name')
.innerJoin(WebhookEntity, 'webhook_entity', 'workflow.id = webhook_entity.workflowId')
.execute()) as Array<{ id: string; name: string }>;
const activationErrors = await this.activeWorkflowRunner.getAllWorkflowActivationErrors(); const activationErrors = await this.activeWorkflowRunner.getAllWorkflowActivationErrors();

View File

@@ -5,7 +5,6 @@ import { StatisticsNames } from '@db/entities/WorkflowStatistics';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository'; import { WorkflowStatisticsRepository } from '@db/repositories/workflowStatistics.repository';
import { ExecutionRequest } from '@/requests'; import { ExecutionRequest } from '@/requests';
import { whereClause } from '@/UserManagement/UserManagementHelper';
import type { IWorkflowStatisticsDataLoaded } from '@/Interfaces'; import type { IWorkflowStatisticsDataLoaded } from '@/Interfaces';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
@@ -33,17 +32,10 @@ export class WorkflowStatisticsController {
async hasWorkflowAccess(req: ExecutionRequest.Get, res: Response, next: NextFunction) { async hasWorkflowAccess(req: ExecutionRequest.Get, res: Response, next: NextFunction) {
const { user } = req; const { user } = req;
const workflowId = req.params.id; const workflowId = req.params.id;
const allowed = await this.sharedWorkflowRepository.exist({
relations: ['workflow'],
where: whereClause({
user,
globalScope: 'workflow:read',
entityType: 'workflow',
entityId: workflowId,
}),
});
if (allowed) { const hasAccess = await this.sharedWorkflowRepository.hasAccess(workflowId, user);
if (hasAccess) {
next(); next();
} else { } else {
this.logger.verbose('User attempted to read a workflow without permissions', { this.logger.verbose('User attempted to read a workflow without permissions', {

View File

@@ -8,8 +8,7 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { CREDENTIAL_EMPTY_VALUE, deepCopy, NodeHelpers } from 'n8n-workflow'; import { CREDENTIAL_EMPTY_VALUE, deepCopy, NodeHelpers } from 'n8n-workflow';
import { Container } from 'typedi'; import { Container } from 'typedi';
import type { FindManyOptions, FindOptionsWhere } from 'typeorm'; import type { FindOptionsWhere } from 'typeorm';
import { In, Like } from 'typeorm';
import type { Scope } from '@n8n/permissions'; import type { Scope } from '@n8n/permissions';
@@ -42,97 +41,35 @@ export class CredentialsService {
}); });
} }
private static toFindManyOptions(listQueryOptions?: ListQuery.Options) {
const findManyOptions: FindManyOptions<CredentialsEntity> = {};
type Select = Array<keyof CredentialsEntity>;
const defaultRelations = ['shared', 'shared.role', 'shared.user'];
const defaultSelect: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'];
if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations };
const { filter, select, take, skip } = listQueryOptions;
if (typeof filter?.name === 'string' && filter?.name !== '') {
filter.name = Like(`%${filter.name}%`);
}
if (typeof filter?.type === 'string' && filter?.type !== '') {
filter.type = Like(`%${filter.type}%`);
}
if (filter) findManyOptions.where = filter;
if (select) findManyOptions.select = select;
if (take) findManyOptions.take = take;
if (skip) findManyOptions.skip = skip;
if (take && select && !select?.id) {
findManyOptions.select = { ...findManyOptions.select, id: true }; // pagination requires id
}
if (!findManyOptions.select) {
findManyOptions.select = defaultSelect;
findManyOptions.relations = defaultRelations;
}
return findManyOptions;
}
static async getMany( static async getMany(
user: User, user: User,
options: { listQueryOptions?: ListQuery.Options; onlyOwn?: boolean } = {}, options: { listQueryOptions?: ListQuery.Options; onlyOwn?: boolean } = {},
) { ) {
const findManyOptions = this.toFindManyOptions(options.listQueryOptions);
const returnAll = user.hasGlobalScope('credential:list') && !options.onlyOwn; const returnAll = user.hasGlobalScope('credential:list') && !options.onlyOwn;
const isDefaultSelect = !options.listQueryOptions?.select; const isDefaultSelect = !options.listQueryOptions?.select;
if (returnAll) { if (returnAll) {
const credentials = await Container.get(CredentialsRepository).find(findManyOptions); const credentials = await Container.get(CredentialsRepository).findMany(
options.listQueryOptions,
);
return isDefaultSelect return isDefaultSelect
? credentials.map((c) => Container.get(OwnershipService).addOwnedByAndSharedWith(c)) ? credentials.map((c) => Container.get(OwnershipService).addOwnedByAndSharedWith(c))
: credentials; : credentials;
} }
const ids = await this.getAccessibleCredentials(user.id); const ids = await Container.get(SharedCredentialsRepository).getAccessibleCredentials(user.id);
const credentials = await Container.get(CredentialsRepository).find({ const credentials = await Container.get(CredentialsRepository).findMany(
...findManyOptions, options.listQueryOptions,
where: { ...findManyOptions.where, id: In(ids) }, // only accessible credentials ids, // only accessible credentials
}); );
return isDefaultSelect return isDefaultSelect
? credentials.map((c) => Container.get(OwnershipService).addOwnedByAndSharedWith(c)) ? credentials.map((c) => Container.get(OwnershipService).addOwnedByAndSharedWith(c))
: credentials; : credentials;
} }
/**
* Get the IDs of all credentials owned by or shared with a user.
*/
private static async getAccessibleCredentials(userId: string) {
const sharings = await Container.get(SharedCredentialsRepository).find({
relations: ['role'],
where: {
userId,
role: { name: In(['owner', 'user']), scope: 'credential' },
},
});
return sharings.map((s) => s.credentialsId);
}
static async getManyByIds(ids: string[], { withSharings } = { withSharings: false }) {
const options: FindManyOptions<CredentialsEntity> = { where: { id: In(ids) } };
if (withSharings) {
options.relations = ['shared', 'shared.user', 'shared.role'];
}
return Container.get(CredentialsRepository).find(options);
}
/** /**
* Retrieve the sharing that matches a user and a credential. * Retrieve the sharing that matches a user and a credential.
*/ */

View File

@@ -1,16 +1,9 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { import { DataSource, In, Not, Repository, Like } from 'typeorm';
DataSource, import type { FindManyOptions, DeleteResult, EntityManager, FindOptionsWhere } from 'typeorm';
In,
Not,
Repository,
type DeleteResult,
type EntityManager,
type FindOptionsWhere,
Like,
} from 'typeorm';
import { CredentialsEntity } from '../entities/CredentialsEntity'; import { CredentialsEntity } from '../entities/CredentialsEntity';
import { SharedCredentials } from '../entities/SharedCredentials'; import { SharedCredentials } from '../entities/SharedCredentials';
import type { ListQuery } from '@/requests';
@Service() @Service()
export class CredentialsRepository extends Repository<CredentialsEntity> { export class CredentialsRepository extends Repository<CredentialsEntity> {
@@ -36,4 +29,61 @@ export class CredentialsRepository extends Repository<CredentialsEntity> {
where: { name: Like(`${credentialName}%`) }, where: { name: Like(`${credentialName}%`) },
}); });
} }
async findMany(listQueryOptions?: ListQuery.Options, credentialIds?: string[]) {
const findManyOptions = this.toFindManyOptions(listQueryOptions);
if (credentialIds) {
findManyOptions.where = { ...findManyOptions.where, id: In(credentialIds) };
}
return this.find(findManyOptions);
}
private toFindManyOptions(listQueryOptions?: ListQuery.Options) {
const findManyOptions: FindManyOptions<CredentialsEntity> = {};
type Select = Array<keyof CredentialsEntity>;
const defaultRelations = ['shared', 'shared.role', 'shared.user'];
const defaultSelect: Select = ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'];
if (!listQueryOptions) return { select: defaultSelect, relations: defaultRelations };
const { filter, select, take, skip } = listQueryOptions;
if (typeof filter?.name === 'string' && filter?.name !== '') {
filter.name = Like(`%${filter.name}%`);
}
if (typeof filter?.type === 'string' && filter?.type !== '') {
filter.type = Like(`%${filter.type}%`);
}
if (filter) findManyOptions.where = filter;
if (select) findManyOptions.select = select;
if (take) findManyOptions.take = take;
if (skip) findManyOptions.skip = skip;
if (take && select && !select?.id) {
findManyOptions.select = { ...findManyOptions.select, id: true }; // pagination requires id
}
if (!findManyOptions.select) {
findManyOptions.select = defaultSelect;
findManyOptions.relations = defaultRelations;
}
return findManyOptions;
}
async getManyByIds(ids: string[], { withSharings } = { withSharings: false }) {
const findManyOptions: FindManyOptions<CredentialsEntity> = { where: { id: In(ids) } };
if (withSharings) {
findManyOptions.relations = ['shared', 'shared.user', 'shared.role'];
}
return this.find(findManyOptions);
}
} }

View File

@@ -187,17 +187,17 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
where?: FindOptionsWhere<ExecutionEntity>; where?: FindOptionsWhere<ExecutionEntity>;
}, },
): Promise<IExecutionFlattedDb | IExecutionResponse | IExecutionBase | undefined> { ): Promise<IExecutionFlattedDb | IExecutionResponse | IExecutionBase | undefined> {
const whereClause: FindOneOptions<ExecutionEntity> = { const findOptions: FindOneOptions<ExecutionEntity> = {
where: { where: {
id, id,
...options?.where, ...options?.where,
}, },
}; };
if (options?.includeData) { if (options?.includeData) {
whereClause.relations = ['executionData']; findOptions.relations = ['executionData'];
} }
const execution = await this.findOne(whereClause); const execution = await this.findOne(findOptions);
if (!execution) { if (!execution) {
return undefined; return undefined;

View File

@@ -1,7 +1,8 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { DataSource, In, Repository } from 'typeorm'; import { DataSource, In, Not, Repository } from 'typeorm';
import { SharedCredentials } from '../entities/SharedCredentials'; import { SharedCredentials } from '../entities/SharedCredentials';
import type { User } from '../entities/User'; import type { User } from '../entities/User';
import type { Role } from '../entities/Role';
@Service() @Service()
export class SharedCredentialsRepository extends Repository<SharedCredentials> { export class SharedCredentialsRepository extends Repository<SharedCredentials> {
@@ -30,4 +31,23 @@ export class SharedCredentialsRepository extends Repository<SharedCredentials> {
}, },
}); });
} }
async makeOwnerOfAllCredentials(user: User, role: Role) {
return this.update({ userId: Not(user.id), roleId: role.id }, { user });
}
/**
* Get the IDs of all credentials owned by or shared with a user.
*/
async getAccessibleCredentials(userId: string) {
const sharings = await this.find({
relations: ['role'],
where: {
userId,
role: { name: In(['owner', 'user']), scope: 'credential' },
},
});
return sharings.map((s) => s.credentialsId);
}
} }

View File

@@ -1,7 +1,9 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { DataSource, type FindOptionsWhere, Repository, In } from 'typeorm'; import { DataSource, type FindOptionsWhere, Repository, In, Not } from 'typeorm';
import { SharedWorkflow } from '../entities/SharedWorkflow'; import { SharedWorkflow } from '../entities/SharedWorkflow';
import { type User } from '../entities/User'; import { type User } from '../entities/User';
import type { Scope } from '@n8n/permissions';
import type { Role } from '../entities/Role';
@Service() @Service()
export class SharedWorkflowRepository extends Repository<SharedWorkflow> { export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
@@ -41,4 +43,33 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
}, },
}); });
} }
async findSharing(
workflowId: string,
user: User,
scope: Scope,
{ roles, extraRelations }: { roles?: string[]; extraRelations?: string[] } = {},
) {
const where: FindOptionsWhere<SharedWorkflow> = {
workflow: { id: workflowId },
};
if (!user.hasGlobalScope(scope)) {
where.user = { id: user.id };
}
if (roles) {
where.role = { name: In(roles) };
}
const relations = ['workflow', 'role'];
if (extraRelations) relations.push(...extraRelations);
return this.findOne({ relations, where });
}
async makeOwnerOfAllWorkflows(user: User, role: Role) {
return this.update({ userId: Not(user.id), roleId: role.id }, { user });
}
} }

View File

@@ -1,5 +1,5 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import { DataSource, In, Repository } from 'typeorm'; import { DataSource, In, Not, Repository } from 'typeorm';
import { User } from '../entities/User'; import { User } from '../entities/User';
@Service() @Service()
@@ -14,4 +14,8 @@ export class UserRepository extends Repository<User> {
relations: ['globalRole'], relations: ['globalRole'],
}); });
} }
async deleteAllExcept(user: User) {
await this.delete({ id: Not(user.id) });
}
} }

View File

@@ -17,6 +17,7 @@ import { isStringArray } from '@/utils';
import config from '@/config'; import config from '@/config';
import { WorkflowEntity } from '../entities/WorkflowEntity'; import { WorkflowEntity } from '../entities/WorkflowEntity';
import { SharedWorkflow } from '../entities/SharedWorkflow'; import { SharedWorkflow } from '../entities/SharedWorkflow';
import { WebhookEntity } from '../entities/WebhookEntity';
@Service() @Service()
export class WorkflowRepository extends Repository<WorkflowEntity> { export class WorkflowRepository extends Repository<WorkflowEntity> {
@@ -183,4 +184,18 @@ export class WorkflowRepository extends Repository<WorkflowEntity> {
where: { name: Like(`${workflowName}%`) }, where: { name: Like(`${workflowName}%`) },
}); });
} }
async findIn(workflowIds: string[]) {
return this.find({
select: ['id', 'name'],
where: { id: In(workflowIds) },
});
}
async findWebhookBasedActiveWorkflows() {
return this.createQueryBuilder('workflow')
.select('DISTINCT workflow.id, workflow.name')
.innerJoin(WebhookEntity, 'webhook_entity', 'workflow.id = webhook_entity.workflowId')
.execute() as Promise<Array<{ id: string; name: string }>>;
}
} }

View File

@@ -18,6 +18,7 @@ import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.reposi
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
@Service() @Service()
export class EnterpriseWorkflowService { export class EnterpriseWorkflowService {
@@ -27,6 +28,7 @@ export class EnterpriseWorkflowService {
private readonly roleService: RoleService, private readonly roleService: RoleService,
private readonly sharedWorkflowRepository: SharedWorkflowRepository, private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly workflowRepository: WorkflowRepository, private readonly workflowRepository: WorkflowRepository,
private readonly credentialsRepository: CredentialsRepository,
) {} ) {}
async isOwned( async isOwned(
@@ -111,7 +113,7 @@ export class EnterpriseWorkflowService {
credentialIdsUsedByWorkflow.add(credential.id); credentialIdsUsedByWorkflow.add(credential.id);
}); });
}); });
const workflowCredentials = await CredentialsService.getManyByIds( const workflowCredentials = await this.credentialsRepository.getManyByIds(
Array.from(credentialIdsUsedByWorkflow), Array.from(credentialIdsUsedByWorkflow),
{ withSharings: true }, { withSharings: true },
); );

View File

@@ -20,7 +20,6 @@ import { NodeTypes } from '@/NodeTypes';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { TestWebhooks } from '@/TestWebhooks'; import { TestWebhooks } from '@/TestWebhooks';
import { whereClause } from '@/UserManagement/UserManagementHelper';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { WorkflowRepository } from '@db/repositories/workflow.repository'; import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
@@ -137,16 +136,12 @@ export class WorkflowService {
forceSave?: boolean, forceSave?: boolean,
roles?: string[], roles?: string[],
): Promise<WorkflowEntity> { ): Promise<WorkflowEntity> {
const shared = await this.sharedWorkflowRepository.findOne({ const shared = await this.sharedWorkflowRepository.findSharing(
relations: ['workflow', 'role'], workflowId,
where: whereClause({ user,
user, 'workflow:update',
globalScope: 'workflow:update', { roles },
entityType: 'workflow', );
entityId: workflowId,
roles,
}),
});
if (!shared) { if (!shared) {
this.logger.verbose('User attempted to update a workflow without permissions', { this.logger.verbose('User attempted to update a workflow without permissions', {
@@ -403,16 +398,12 @@ export class WorkflowService {
async delete(user: User, workflowId: string): Promise<WorkflowEntity | undefined> { async delete(user: User, workflowId: string): Promise<WorkflowEntity | undefined> {
await this.externalHooks.run('workflow.delete', [workflowId]); await this.externalHooks.run('workflow.delete', [workflowId]);
const sharedWorkflow = await this.sharedWorkflowRepository.findOne({ const sharedWorkflow = await this.sharedWorkflowRepository.findSharing(
relations: ['workflow', 'role'], workflowId,
where: whereClause({ user,
user, 'workflow:delete',
globalScope: 'workflow:delete', { roles: ['owner'] },
entityType: 'workflow', );
entityId: workflowId,
roles: ['owner'],
}),
});
if (!sharedWorkflow) { if (!sharedWorkflow) {
return; return;

View File

@@ -16,7 +16,6 @@ import type { ListQuery, WorkflowRequest } from '@/requests';
import { isBelowOnboardingThreshold } from '@/WorkflowHelpers'; import { isBelowOnboardingThreshold } from '@/WorkflowHelpers';
import { EEWorkflowController } from './workflows.controller.ee'; import { EEWorkflowController } from './workflows.controller.ee';
import { WorkflowService } from './workflow.service'; import { WorkflowService } from './workflow.service';
import { whereClause } from '@/UserManagement/UserManagementHelper';
import { Container } from 'typedi'; import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
@@ -196,22 +195,14 @@ workflowsController.get(
ResponseHelper.send(async (req: WorkflowRequest.Get) => { ResponseHelper.send(async (req: WorkflowRequest.Get) => {
const { id: workflowId } = req.params; const { id: workflowId } = req.params;
let relations = ['workflow', 'workflow.tags', 'role']; const extraRelations = config.getEnv('workflowTagsDisabled') ? [] : ['workflow.tags'];
if (config.getEnv('workflowTagsDisabled')) { const shared = await Container.get(SharedWorkflowRepository).findSharing(
relations = relations.filter((relation) => relation !== 'workflow.tags'); workflowId,
} req.user,
'workflow:read',
const shared = await Container.get(SharedWorkflowRepository).findOne({ { extraRelations },
relations, );
where: whereClause({
user: req.user,
entityType: 'workflow',
globalScope: 'workflow:read',
entityId: workflowId,
roles: ['owner'],
}),
});
if (!shared) { if (!shared) {
Container.get(Logger).verbose('User attempted to access a workflow without permissions', { Container.get(Logger).verbose('User attempted to access a workflow without permissions', {

View File

@@ -31,19 +31,11 @@ describe('DebugController', () => {
const instanceId = 'main-71JdWtq306epIFki'; const instanceId = 'main-71JdWtq306epIFki';
const leaderKey = 'some-leader-key'; const leaderKey = 'some-leader-key';
const createQueryBuilder = { workflowRepository.findIn.mockResolvedValue(triggersAndPollers);
select: () => createQueryBuilder, workflowRepository.findWebhookBasedActiveWorkflows.mockResolvedValue(webhooks);
innerJoin: () => createQueryBuilder,
execute: () => webhooks,
};
workflowRepository.find.mockResolvedValue(triggersAndPollers);
activeWorkflowRunner.allActiveInMemory.mockReturnValue([workflowId]); activeWorkflowRunner.allActiveInMemory.mockReturnValue([workflowId]);
activeWorkflowRunner.getAllWorkflowActivationErrors.mockResolvedValue(activationErrors); activeWorkflowRunner.getAllWorkflowActivationErrors.mockResolvedValue(activationErrors);
jest
.spyOn(workflowRepository, 'createQueryBuilder')
.mockImplementation(() => createQueryBuilder);
jest.spyOn(MultiMainSetup.prototype, 'instanceId', 'get').mockReturnValue(instanceId); jest.spyOn(MultiMainSetup.prototype, 'instanceId', 'get').mockReturnValue(instanceId);
jest.spyOn(MultiMainSetup.prototype, 'fetchLeaderKey').mockResolvedValue(leaderKey); jest.spyOn(MultiMainSetup.prototype, 'fetchLeaderKey').mockResolvedValue(leaderKey);
jest.spyOn(MultiMainSetup.prototype, 'isLeader', 'get').mockReturnValue(true); jest.spyOn(MultiMainSetup.prototype, 'isLeader', 'get').mockReturnValue(true);