mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
refactor(core): Move typeorm operators from various sources into repositories (no-changelog) (#8174)
Follow-up to: #8165
This commit is contained in:
@@ -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
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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> = {};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }>>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user