feat: RBAC (#8922)
Signed-off-by: Oleg Ivaniv <me@olegivaniv.com> Co-authored-by: Val <68596159+valya@users.noreply.github.com> Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: Valya Bullions <valya@n8n.io> Co-authored-by: Danny Martini <danny@n8n.io> Co-authored-by: Danny Martini <despair.blue@gmail.com> Co-authored-by: Iván Ovejero <ivov.src@gmail.com> Co-authored-by: Omar Ajoue <krynble@gmail.com> Co-authored-by: oleg <me@olegivaniv.com> Co-authored-by: Michael Kret <michael.k@radency.com> Co-authored-by: Michael Kret <88898367+michael-radency@users.noreply.github.com> Co-authored-by: Elias Meire <elias@meire.dev> Co-authored-by: Giulio Andreini <andreini@netseven.it> Co-authored-by: Giulio Andreini <g.andreini@gmail.com> Co-authored-by: Ayato Hayashi <go12limchangyong@gmail.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { IWorkflowDb } from '@/Interfaces';
|
||||
import type { AuthenticatedRequest } from '@/requests';
|
||||
import type { AuthenticatedRequest, ListQuery } from '@/requests';
|
||||
import type {
|
||||
INode,
|
||||
IConnections,
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
|
||||
export declare namespace WorkflowRequest {
|
||||
type CreateUpdatePayload = Partial<{
|
||||
id: string; // delete if sent
|
||||
id: string; // deleted if sent
|
||||
name: string;
|
||||
nodes: INode[];
|
||||
connections: IConnections;
|
||||
@@ -20,6 +20,7 @@ export declare namespace WorkflowRequest {
|
||||
tags: string[];
|
||||
hash: string;
|
||||
meta: Record<string, unknown>;
|
||||
projectId: string;
|
||||
}>;
|
||||
|
||||
type ManualRunPayload = {
|
||||
@@ -32,12 +33,16 @@ export declare namespace WorkflowRequest {
|
||||
|
||||
type Create = AuthenticatedRequest<{}, {}, CreateUpdatePayload>;
|
||||
|
||||
type Get = AuthenticatedRequest<{ id: string }>;
|
||||
type Get = AuthenticatedRequest<{ workflowId: string }>;
|
||||
|
||||
type GetMany = AuthenticatedRequest<{}, {}, {}, ListQuery.Params & { includeScopes?: string }> & {
|
||||
listQueryOptions: ListQuery.Options;
|
||||
};
|
||||
|
||||
type Delete = Get;
|
||||
|
||||
type Update = AuthenticatedRequest<
|
||||
{ id: string },
|
||||
{ workflowId: string },
|
||||
{},
|
||||
CreateUpdatePayload,
|
||||
{ forceSave?: string }
|
||||
@@ -45,7 +50,7 @@ export declare namespace WorkflowRequest {
|
||||
|
||||
type NewName = AuthenticatedRequest<{}, {}, {}, { name?: string }>;
|
||||
|
||||
type ManualRun = AuthenticatedRequest<{}, {}, ManualRunPayload>;
|
||||
type ManualRun = AuthenticatedRequest<{ workflowId: string }, {}, ManualRunPayload>;
|
||||
|
||||
type Share = AuthenticatedRequest<{ workflowId: string }, {}, { shareWithIds: string[] }>;
|
||||
|
||||
|
||||
@@ -15,7 +15,11 @@ import { Logger } from '@/Logger';
|
||||
import type {
|
||||
CredentialUsedByWorkflow,
|
||||
WorkflowWithSharingsAndCredentials,
|
||||
WorkflowWithSharingsMetaDataAndCredentials,
|
||||
} from './workflows.types';
|
||||
import { OwnershipService } from '@/services/ownership.service';
|
||||
import { In, type EntityManager } from '@n8n/typeorm';
|
||||
import { Project } from '@/databases/entities/Project';
|
||||
|
||||
@Service()
|
||||
export class EnterpriseWorkflowService {
|
||||
@@ -25,49 +29,48 @@ export class EnterpriseWorkflowService {
|
||||
private readonly workflowRepository: WorkflowRepository,
|
||||
private readonly credentialsRepository: CredentialsRepository,
|
||||
private readonly credentialsService: CredentialsService,
|
||||
private readonly ownershipService: OwnershipService,
|
||||
) {}
|
||||
|
||||
async isOwned(
|
||||
user: User,
|
||||
workflowId: string,
|
||||
): Promise<{ ownsWorkflow: boolean; workflow?: WorkflowEntity }> {
|
||||
const sharing = await this.sharedWorkflowRepository.getSharing(
|
||||
user,
|
||||
workflowId,
|
||||
{ allowGlobalScope: false },
|
||||
['workflow'],
|
||||
);
|
||||
async shareWithProjects(
|
||||
workflow: WorkflowEntity,
|
||||
shareWithIds: string[],
|
||||
entityManager: EntityManager,
|
||||
) {
|
||||
const em = entityManager ?? this.sharedWorkflowRepository.manager;
|
||||
|
||||
if (!sharing || sharing.role !== 'workflow:owner') return { ownsWorkflow: false };
|
||||
|
||||
const { workflow } = sharing;
|
||||
|
||||
return { ownsWorkflow: true, workflow };
|
||||
}
|
||||
|
||||
addOwnerAndSharings(workflow: WorkflowWithSharingsAndCredentials): void {
|
||||
workflow.ownedBy = null;
|
||||
workflow.sharedWith = [];
|
||||
if (!workflow.usedCredentials) {
|
||||
workflow.usedCredentials = [];
|
||||
}
|
||||
|
||||
workflow.shared?.forEach(({ user, role }) => {
|
||||
const { id, email, firstName, lastName } = user;
|
||||
|
||||
if (role === 'workflow:owner') {
|
||||
workflow.ownedBy = { id, email, firstName, lastName };
|
||||
return;
|
||||
}
|
||||
|
||||
workflow.sharedWith?.push({ id, email, firstName, lastName });
|
||||
const projects = await em.find(Project, {
|
||||
where: { id: In(shareWithIds), type: 'personal' },
|
||||
});
|
||||
|
||||
delete workflow.shared;
|
||||
const newSharedWorkflows = projects
|
||||
// We filter by role === 'project:personalOwner' above and there should
|
||||
// always only be one owner.
|
||||
.map((project) =>
|
||||
this.sharedWorkflowRepository.create({
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:editor',
|
||||
projectId: project.id,
|
||||
}),
|
||||
);
|
||||
|
||||
return await em.save(newSharedWorkflows);
|
||||
}
|
||||
|
||||
addOwnerAndSharings(
|
||||
workflow: WorkflowWithSharingsAndCredentials,
|
||||
): WorkflowWithSharingsMetaDataAndCredentials {
|
||||
const workflowWithMetaData = this.ownershipService.addOwnedByAndSharedWith(workflow);
|
||||
|
||||
return {
|
||||
...workflow,
|
||||
...workflowWithMetaData,
|
||||
usedCredentials: workflow.usedCredentials ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async addCredentialsToWorkflow(
|
||||
workflow: WorkflowWithSharingsAndCredentials,
|
||||
workflow: WorkflowWithSharingsMetaDataAndCredentials,
|
||||
currentUser: User,
|
||||
): Promise<void> {
|
||||
workflow.usedCredentials = [];
|
||||
@@ -100,14 +103,7 @@ export class EnterpriseWorkflowService {
|
||||
sharedWith: [],
|
||||
ownedBy: null,
|
||||
};
|
||||
credential.shared?.forEach(({ user, role }) => {
|
||||
const { id, email, firstName, lastName } = user;
|
||||
if (role === 'credential:owner') {
|
||||
workflowCredential.ownedBy = { id, email, firstName, lastName };
|
||||
} else {
|
||||
workflowCredential.sharedWith?.push({ id, email, firstName, lastName });
|
||||
}
|
||||
});
|
||||
credential = this.ownershipService.addOwnedByAndSharedWith(credential);
|
||||
workflow.usedCredentials?.push(workflowCredential);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ import { BinaryDataService } from 'n8n-core';
|
||||
import config from '@/config';
|
||||
import type { User } from '@db/entities/User';
|
||||
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import type { WorkflowSharingRole } from '@db/entities/SharedWorkflow';
|
||||
import { ExecutionRepository } from '@db/repositories/execution.repository';
|
||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||
import { WorkflowTagMappingRepository } from '@db/repositories/workflowTagMapping.repository';
|
||||
import { WorkflowRepository } from '@db/repositories/workflow.repository';
|
||||
@@ -26,12 +24,19 @@ import { Logger } from '@/Logger';
|
||||
import { OrchestrationService } from '@/services/orchestration.service';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { RoleService } from '@/services/role.service';
|
||||
import { WorkflowSharingService } from './workflowSharing.service';
|
||||
import { ProjectService } from '@/services/project.service';
|
||||
import { ExecutionRepository } from '@/databases/repositories/execution.repository';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type { EntityManager } from '@n8n/typeorm';
|
||||
import { In } from '@n8n/typeorm';
|
||||
import { SharedWorkflow } from '@/databases/entities/SharedWorkflow';
|
||||
|
||||
@Service()
|
||||
export class WorkflowService {
|
||||
constructor(
|
||||
private readonly logger: Logger,
|
||||
private readonly executionRepository: ExecutionRepository,
|
||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||
private readonly workflowRepository: WorkflowRepository,
|
||||
private readonly workflowTagMappingRepository: WorkflowTagMappingRepository,
|
||||
@@ -42,36 +47,52 @@ export class WorkflowService {
|
||||
private readonly orchestrationService: OrchestrationService,
|
||||
private readonly externalHooks: ExternalHooks,
|
||||
private readonly activeWorkflowManager: ActiveWorkflowManager,
|
||||
private readonly roleService: RoleService,
|
||||
private readonly workflowSharingService: WorkflowSharingService,
|
||||
private readonly projectService: ProjectService,
|
||||
private readonly executionRepository: ExecutionRepository,
|
||||
) {}
|
||||
|
||||
async getMany(sharedWorkflowIds: string[], options?: ListQuery.Options) {
|
||||
const { workflows, count } = await this.workflowRepository.getMany(sharedWorkflowIds, options);
|
||||
async getMany(user: User, options?: ListQuery.Options, includeScopes?: boolean) {
|
||||
const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(user, {
|
||||
scopes: ['workflow:read'],
|
||||
});
|
||||
|
||||
return hasSharing(workflows)
|
||||
? {
|
||||
workflows: workflows.map((w) => this.ownershipService.addOwnedByAndSharedWith(w)),
|
||||
count,
|
||||
}
|
||||
: { workflows, count };
|
||||
// eslint-disable-next-line prefer-const
|
||||
let { workflows, count } = await this.workflowRepository.getMany(sharedWorkflowIds, options);
|
||||
|
||||
if (hasSharing(workflows)) {
|
||||
workflows = workflows.map((w) => this.ownershipService.addOwnedByAndSharedWith(w));
|
||||
}
|
||||
|
||||
if (includeScopes) {
|
||||
const projectRelations = await this.projectService.getProjectRelationsForUser(user);
|
||||
workflows = workflows.map((w) => this.roleService.addScopes(w, user, projectRelations));
|
||||
}
|
||||
|
||||
workflows.forEach((w) => {
|
||||
// @ts-expect-error: This is to emulate the old behaviour of removing the shared
|
||||
// field as part of `addOwnedByAndSharedWith`. We need this field in `addScopes`
|
||||
// though. So to avoid leaking the information we just delete it.
|
||||
delete w.shared;
|
||||
});
|
||||
|
||||
return { workflows, count };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
async update(
|
||||
user: User,
|
||||
workflow: WorkflowEntity,
|
||||
workflowUpdateData: WorkflowEntity,
|
||||
workflowId: string,
|
||||
tagIds?: string[],
|
||||
forceSave?: boolean,
|
||||
roles?: WorkflowSharingRole[],
|
||||
): Promise<WorkflowEntity> {
|
||||
const shared = await this.sharedWorkflowRepository.findSharing(
|
||||
workflowId,
|
||||
user,
|
||||
const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [
|
||||
'workflow:update',
|
||||
{ roles },
|
||||
);
|
||||
]);
|
||||
|
||||
if (!shared) {
|
||||
if (!workflow) {
|
||||
this.logger.verbose('User attempted to update a workflow without permissions', {
|
||||
workflowId,
|
||||
userId: user.id,
|
||||
@@ -83,8 +104,8 @@ export class WorkflowService {
|
||||
|
||||
if (
|
||||
!forceSave &&
|
||||
workflow.versionId !== '' &&
|
||||
workflow.versionId !== shared.workflow.versionId
|
||||
workflowUpdateData.versionId !== '' &&
|
||||
workflowUpdateData.versionId !== workflow.versionId
|
||||
) {
|
||||
throw new BadRequestError(
|
||||
'Your most recent changes may be lost, because someone else just updated this workflow. Open this workflow in a new tab to see those new updates.',
|
||||
@@ -92,25 +113,25 @@ export class WorkflowService {
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(omit(workflow, ['id', 'versionId', 'active'])).length > 0) {
|
||||
if (Object.keys(omit(workflowUpdateData, ['id', 'versionId', 'active'])).length > 0) {
|
||||
// Update the workflow's version when changing properties such as
|
||||
// `name`, `pinData`, `nodes`, `connections`, `settings` or `tags`
|
||||
workflow.versionId = uuid();
|
||||
workflowUpdateData.versionId = uuid();
|
||||
this.logger.verbose(
|
||||
`Updating versionId for workflow ${workflowId} for user ${user.id} after saving`,
|
||||
{
|
||||
previousVersionId: shared.workflow.versionId,
|
||||
newVersionId: workflow.versionId,
|
||||
previousVersionId: workflow.versionId,
|
||||
newVersionId: workflowUpdateData.versionId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// check credentials for old format
|
||||
await WorkflowHelpers.replaceInvalidCredentials(workflow);
|
||||
await WorkflowHelpers.replaceInvalidCredentials(workflowUpdateData);
|
||||
|
||||
WorkflowHelpers.addNodeIds(workflow);
|
||||
WorkflowHelpers.addNodeIds(workflowUpdateData);
|
||||
|
||||
await this.externalHooks.run('workflow.update', [workflow]);
|
||||
await this.externalHooks.run('workflow.update', [workflowUpdateData]);
|
||||
|
||||
/**
|
||||
* If the workflow being updated is stored as `active`, remove it from
|
||||
@@ -119,11 +140,11 @@ export class WorkflowService {
|
||||
* If a trigger or poller in the workflow was updated, the new value
|
||||
* will take effect only on removing and re-adding.
|
||||
*/
|
||||
if (shared.workflow.active) {
|
||||
if (workflow.active) {
|
||||
await this.activeWorkflowManager.remove(workflowId);
|
||||
}
|
||||
|
||||
const workflowSettings = workflow.settings ?? {};
|
||||
const workflowSettings = workflowUpdateData.settings ?? {};
|
||||
|
||||
const keysAllowingDefault = [
|
||||
'timezone',
|
||||
@@ -144,14 +165,14 @@ export class WorkflowService {
|
||||
delete workflowSettings.executionTimeout;
|
||||
}
|
||||
|
||||
if (workflow.name) {
|
||||
workflow.updatedAt = new Date(); // required due to atomic update
|
||||
await validateEntity(workflow);
|
||||
if (workflowUpdateData.name) {
|
||||
workflowUpdateData.updatedAt = new Date(); // required due to atomic update
|
||||
await validateEntity(workflowUpdateData);
|
||||
}
|
||||
|
||||
await this.workflowRepository.update(
|
||||
workflowId,
|
||||
pick(workflow, [
|
||||
pick(workflowUpdateData, [
|
||||
'name',
|
||||
'active',
|
||||
'nodes',
|
||||
@@ -168,8 +189,8 @@ export class WorkflowService {
|
||||
await this.workflowTagMappingRepository.overwriteTaggings(workflowId, tagIds);
|
||||
}
|
||||
|
||||
if (workflow.versionId !== shared.workflow.versionId) {
|
||||
await this.workflowHistoryService.saveVersion(user, workflow, workflowId);
|
||||
if (workflowUpdateData.versionId !== workflow.versionId) {
|
||||
await this.workflowHistoryService.saveVersion(user, workflowUpdateData, workflowId);
|
||||
}
|
||||
|
||||
const relations = config.getEnv('workflowTagsDisabled') ? [] : ['tags'];
|
||||
@@ -200,16 +221,13 @@ export class WorkflowService {
|
||||
// When the workflow is supposed to be active add it again
|
||||
try {
|
||||
await this.externalHooks.run('workflow.activate', [updatedWorkflow]);
|
||||
await this.activeWorkflowManager.add(
|
||||
workflowId,
|
||||
shared.workflow.active ? 'update' : 'activate',
|
||||
);
|
||||
await this.activeWorkflowManager.add(workflowId, workflow.active ? 'update' : 'activate');
|
||||
} catch (error) {
|
||||
// If workflow could not be activated set it again to inactive
|
||||
// and revert the versionId change so UI remains consistent
|
||||
await this.workflowRepository.update(workflowId, {
|
||||
active: false,
|
||||
versionId: shared.workflow.versionId,
|
||||
versionId: workflow.versionId,
|
||||
});
|
||||
|
||||
// Also set it in the returned data
|
||||
@@ -232,18 +250,15 @@ export class WorkflowService {
|
||||
async delete(user: User, workflowId: string): Promise<WorkflowEntity | undefined> {
|
||||
await this.externalHooks.run('workflow.delete', [workflowId]);
|
||||
|
||||
const sharedWorkflow = await this.sharedWorkflowRepository.findSharing(
|
||||
workflowId,
|
||||
user,
|
||||
const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [
|
||||
'workflow:delete',
|
||||
{ roles: ['workflow:owner'] },
|
||||
);
|
||||
]);
|
||||
|
||||
if (!sharedWorkflow) {
|
||||
if (!workflow) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sharedWorkflow.workflow.active) {
|
||||
if (workflow.active) {
|
||||
// deactivate before deleting
|
||||
await this.activeWorkflowManager.remove(workflowId);
|
||||
}
|
||||
@@ -261,6 +276,71 @@ export class WorkflowService {
|
||||
void Container.get(InternalHooks).onWorkflowDeleted(user, workflowId, false);
|
||||
await this.externalHooks.run('workflow.afterDelete', [workflowId]);
|
||||
|
||||
return sharedWorkflow.workflow;
|
||||
return workflow;
|
||||
}
|
||||
|
||||
async getWorkflowScopes(user: User, workflowId: string): Promise<Scope[]> {
|
||||
const userProjectRelations = await this.projectService.getProjectRelationsForUser(user);
|
||||
const shared = await this.sharedWorkflowRepository.find({
|
||||
where: {
|
||||
projectId: In([...new Set(userProjectRelations.map((pr) => pr.projectId))]),
|
||||
workflowId,
|
||||
},
|
||||
});
|
||||
return this.roleService.combineResourceScopes('workflow', user, shared, userProjectRelations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers all workflows owned by a project to another one.
|
||||
* This has only been tested for personal projects. It may need to be amended
|
||||
* for team projects.
|
||||
**/
|
||||
async transferAll(fromProjectId: string, toProjectId: string, trx?: EntityManager) {
|
||||
trx = trx ?? this.workflowRepository.manager;
|
||||
|
||||
// Get all shared workflows for both projects.
|
||||
const allSharedWorkflows = await trx.findBy(SharedWorkflow, {
|
||||
projectId: In([fromProjectId, toProjectId]),
|
||||
});
|
||||
const sharedWorkflowsOfFromProject = allSharedWorkflows.filter(
|
||||
(sw) => sw.projectId === fromProjectId,
|
||||
);
|
||||
|
||||
// For all workflows that the from-project owns transfer the ownership to
|
||||
// the to-project.
|
||||
// This will override whatever relationship the to-project already has to
|
||||
// the resources at the moment.
|
||||
|
||||
const ownedWorkflowIds = sharedWorkflowsOfFromProject
|
||||
.filter((sw) => sw.role === 'workflow:owner')
|
||||
.map((sw) => sw.workflowId);
|
||||
|
||||
await this.sharedWorkflowRepository.makeOwner(ownedWorkflowIds, toProjectId, trx);
|
||||
|
||||
// Delete the relationship to the from-project.
|
||||
await this.sharedWorkflowRepository.deleteByIds(ownedWorkflowIds, fromProjectId, trx);
|
||||
|
||||
// Transfer relationships that are not `workflow:owner`.
|
||||
// This will NOT override whatever relationship the from-project already
|
||||
// has to the resource at the moment.
|
||||
const sharedWorkflowIdsOfTransferee = allSharedWorkflows
|
||||
.filter((sw) => sw.projectId === toProjectId)
|
||||
.map((sw) => sw.workflowId);
|
||||
|
||||
// All resources that are shared with the from-project, but not with the
|
||||
// to-project.
|
||||
const sharedWorkflowsToTransfer = sharedWorkflowsOfFromProject.filter(
|
||||
(sw) =>
|
||||
sw.role !== 'workflow:owner' && !sharedWorkflowIdsOfTransferee.includes(sw.workflowId),
|
||||
);
|
||||
|
||||
await trx.insert(
|
||||
SharedWorkflow,
|
||||
sharedWorkflowsToTransfer.map((sw) => ({
|
||||
workflowId: sw.workflowId,
|
||||
projectId: toProjectId,
|
||||
role: sw.role,
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'
|
||||
import { TestWebhooks } from '@/TestWebhooks';
|
||||
import { Logger } from '@/Logger';
|
||||
import { PermissionChecker } from '@/UserManagement/PermissionChecker';
|
||||
import type { Project } from '@/databases/entities/Project';
|
||||
|
||||
@Service()
|
||||
export class WorkflowExecutionService {
|
||||
@@ -161,7 +162,7 @@ export class WorkflowExecutionService {
|
||||
async executeErrorWorkflow(
|
||||
workflowId: string,
|
||||
workflowErrorData: IWorkflowErrorData,
|
||||
runningUser: User,
|
||||
runningProject: Project,
|
||||
): Promise<void> {
|
||||
// Wrap everything in try/catch to make sure that no errors bubble up and all get caught here
|
||||
try {
|
||||
@@ -284,7 +285,7 @@ export class WorkflowExecutionService {
|
||||
executionMode,
|
||||
executionData: runExecutionData,
|
||||
workflowData,
|
||||
userId: runningUser.id,
|
||||
projectId: runningProject.id,
|
||||
};
|
||||
|
||||
await this.workflowRunner.run(runData);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||
import type { User } from '@db/entities/User';
|
||||
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import type { WorkflowHistory } from '@db/entities/WorkflowHistory';
|
||||
@@ -18,28 +17,23 @@ export class WorkflowHistoryService {
|
||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||
) {}
|
||||
|
||||
private async getSharedWorkflow(user: User, workflowId: string): Promise<SharedWorkflow | null> {
|
||||
return await this.sharedWorkflowRepository.findOne({
|
||||
where: {
|
||||
...(!user.hasGlobalScope('workflow:read') && { userId: user.id }),
|
||||
workflowId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getList(
|
||||
user: User,
|
||||
workflowId: string,
|
||||
take: number,
|
||||
skip: number,
|
||||
): Promise<Array<Omit<WorkflowHistory, 'nodes' | 'connections'>>> {
|
||||
const sharedWorkflow = await this.getSharedWorkflow(user, workflowId);
|
||||
if (!sharedWorkflow) {
|
||||
const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [
|
||||
'workflow:read',
|
||||
]);
|
||||
|
||||
if (!workflow) {
|
||||
throw new SharedWorkflowNotFoundError('');
|
||||
}
|
||||
|
||||
return await this.workflowHistoryRepository.find({
|
||||
where: {
|
||||
workflowId: sharedWorkflow.workflowId,
|
||||
workflowId: workflow.id,
|
||||
},
|
||||
take,
|
||||
skip,
|
||||
@@ -49,13 +43,17 @@ export class WorkflowHistoryService {
|
||||
}
|
||||
|
||||
async getVersion(user: User, workflowId: string, versionId: string): Promise<WorkflowHistory> {
|
||||
const sharedWorkflow = await this.getSharedWorkflow(user, workflowId);
|
||||
if (!sharedWorkflow) {
|
||||
const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, user, [
|
||||
'workflow:read',
|
||||
]);
|
||||
|
||||
if (!workflow) {
|
||||
throw new SharedWorkflowNotFoundError('');
|
||||
}
|
||||
|
||||
const hist = await this.workflowHistoryRepository.findOne({
|
||||
where: {
|
||||
workflowId: sharedWorkflow.workflowId,
|
||||
workflowId: workflow.id,
|
||||
versionId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,30 +1,61 @@
|
||||
import { Service } from 'typedi';
|
||||
import { In, type FindOptionsWhere } from '@n8n/typeorm';
|
||||
import { In } from '@n8n/typeorm';
|
||||
|
||||
import type { SharedWorkflow, WorkflowSharingRole } from '@db/entities/SharedWorkflow';
|
||||
import type { User } from '@db/entities/User';
|
||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||
import { RoleService } from '@/services/role.service';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type { ProjectRole } from '@/databases/entities/ProjectRelation';
|
||||
import type { WorkflowSharingRole } from '@/databases/entities/SharedWorkflow';
|
||||
|
||||
@Service()
|
||||
export class WorkflowSharingService {
|
||||
constructor(private readonly sharedWorkflowRepository: SharedWorkflowRepository) {}
|
||||
constructor(
|
||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||
private readonly roleService: RoleService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the IDs of the workflows that have been shared with the user.
|
||||
* Returns all IDs if user has the 'workflow:read' scope.
|
||||
* Get the IDs of the workflows that have been shared with the user based on
|
||||
* scope or roles.
|
||||
* If `scopes` is passed the roles are inferred. Alternatively `projectRoles`
|
||||
* and `workflowRoles` can be passed specifically.
|
||||
*
|
||||
* Returns all IDs if user has the 'workflow:read' global scope.
|
||||
*/
|
||||
async getSharedWorkflowIds(user: User, roles?: WorkflowSharingRole[]): Promise<string[]> {
|
||||
const where: FindOptionsWhere<SharedWorkflow> = {};
|
||||
if (!user.hasGlobalScope('workflow:read')) {
|
||||
where.userId = user.id;
|
||||
}
|
||||
if (roles?.length) {
|
||||
where.role = In(roles);
|
||||
async getSharedWorkflowIds(
|
||||
user: User,
|
||||
options:
|
||||
| { scopes: Scope[] }
|
||||
| { projectRoles: ProjectRole[]; workflowRoles: WorkflowSharingRole[] },
|
||||
): Promise<string[]> {
|
||||
if (user.hasGlobalScope('workflow:read')) {
|
||||
const sharedWorkflows = await this.sharedWorkflowRepository.find({ select: ['workflowId'] });
|
||||
return sharedWorkflows.map(({ workflowId }) => workflowId);
|
||||
}
|
||||
|
||||
const projectRoles =
|
||||
'scopes' in options
|
||||
? this.roleService.rolesWithScope('project', options.scopes)
|
||||
: options.projectRoles;
|
||||
const workflowRoles =
|
||||
'scopes' in options
|
||||
? this.roleService.rolesWithScope('workflow', options.scopes)
|
||||
: options.workflowRoles;
|
||||
|
||||
const sharedWorkflows = await this.sharedWorkflowRepository.find({
|
||||
where,
|
||||
where: {
|
||||
role: In(workflowRoles),
|
||||
project: {
|
||||
projectRelations: {
|
||||
userId: user.id,
|
||||
role: In(projectRoles),
|
||||
},
|
||||
},
|
||||
},
|
||||
select: ['workflowId'],
|
||||
});
|
||||
|
||||
return sharedWorkflows.map(({ workflowId }) => workflowId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,16 +7,14 @@ import * as ResponseHelper from '@/ResponseHelper';
|
||||
import * as WorkflowHelpers from '@/WorkflowHelpers';
|
||||
import type { IWorkflowResponse } from '@/Interfaces';
|
||||
import config from '@/config';
|
||||
import { Delete, Get, Patch, Post, Put, RestController } from '@/decorators';
|
||||
import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow';
|
||||
import { Delete, Get, Patch, Post, ProjectScope, Put, RestController } from '@/decorators';
|
||||
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||
import { TagRepository } from '@db/repositories/tag.repository';
|
||||
import { WorkflowRepository } from '@db/repositories/workflow.repository';
|
||||
import { UserRepository } from '@db/repositories/user.repository';
|
||||
import { validateEntity } from '@/GenericHelpers';
|
||||
import { ExternalHooks } from '@/ExternalHooks';
|
||||
import { ListQuery } from '@/requests';
|
||||
import { WorkflowService } from './workflow.service';
|
||||
import { License } from '@/License';
|
||||
import { InternalHooks } from '@/InternalHooks';
|
||||
@@ -28,15 +26,20 @@ import { Logger } from '@/Logger';
|
||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
|
||||
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
|
||||
import { NamingService } from '@/services/naming.service';
|
||||
import { UserOnboardingService } from '@/services/userOnboarding.service';
|
||||
import { CredentialsService } from '../credentials/credentials.service';
|
||||
import { WorkflowRequest } from './workflow.request';
|
||||
import { EnterpriseWorkflowService } from './workflow.service.ee';
|
||||
import { WorkflowExecutionService } from './workflowExecution.service';
|
||||
import { WorkflowSharingService } from './workflowSharing.service';
|
||||
import { UserManagementMailer } from '@/UserManagement/email';
|
||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||
import { ProjectService } from '@/services/project.service';
|
||||
import { ApplicationError } from 'n8n-workflow';
|
||||
import { In, type FindOptionsRelations } from '@n8n/typeorm';
|
||||
import type { Project } from '@/databases/entities/Project';
|
||||
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
|
||||
|
||||
@RestController('/workflows')
|
||||
export class WorkflowsController {
|
||||
@@ -53,17 +56,21 @@ export class WorkflowsController {
|
||||
private readonly workflowRepository: WorkflowRepository,
|
||||
private readonly workflowService: WorkflowService,
|
||||
private readonly workflowExecutionService: WorkflowExecutionService,
|
||||
private readonly workflowSharingService: WorkflowSharingService,
|
||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||
private readonly userRepository: UserRepository,
|
||||
private readonly license: License,
|
||||
private readonly mailer: UserManagementMailer,
|
||||
private readonly credentialsService: CredentialsService,
|
||||
private readonly projectRepository: ProjectRepository,
|
||||
private readonly projectService: ProjectService,
|
||||
private readonly projectRelationRepository: ProjectRelationRepository,
|
||||
) {}
|
||||
|
||||
@Post('/')
|
||||
async create(req: WorkflowRequest.Create) {
|
||||
delete req.body.id; // delete if sent
|
||||
// @ts-expect-error: We shouldn't accept this because it can
|
||||
// mess with relations of other workflows
|
||||
delete req.body.shared;
|
||||
|
||||
const newWorkflow = new WorkflowEntity();
|
||||
|
||||
@@ -87,7 +94,7 @@ export class WorkflowsController {
|
||||
|
||||
if (this.license.isSharingEnabled()) {
|
||||
// This is a new workflow, so we simply check if the user has access to
|
||||
// all used workflows
|
||||
// all used credentials
|
||||
|
||||
const allCredentials = await this.credentialsService.getMany(req.user);
|
||||
|
||||
@@ -103,20 +110,46 @@ export class WorkflowsController {
|
||||
}
|
||||
}
|
||||
|
||||
let savedWorkflow: undefined | WorkflowEntity;
|
||||
let project: Project | null;
|
||||
const savedWorkflow = await Db.transaction(async (transactionManager) => {
|
||||
const workflow = await transactionManager.save<WorkflowEntity>(newWorkflow);
|
||||
|
||||
await Db.transaction(async (transactionManager) => {
|
||||
savedWorkflow = await transactionManager.save<WorkflowEntity>(newWorkflow);
|
||||
const { projectId } = req.body;
|
||||
project =
|
||||
projectId === undefined
|
||||
? await this.projectRepository.getPersonalProjectForUser(req.user.id, transactionManager)
|
||||
: await this.projectService.getProjectWithScope(
|
||||
req.user,
|
||||
projectId,
|
||||
['workflow:create'],
|
||||
transactionManager,
|
||||
);
|
||||
|
||||
const newSharedWorkflow = new SharedWorkflow();
|
||||
if (typeof projectId === 'string' && project === null) {
|
||||
throw new BadRequestError(
|
||||
"You don't have the permissions to save the workflow in this project.",
|
||||
);
|
||||
}
|
||||
|
||||
Object.assign(newSharedWorkflow, {
|
||||
// Safe guard in case the personal project does not exist for whatever reason.
|
||||
if (project === null) {
|
||||
throw new ApplicationError('No personal project found');
|
||||
}
|
||||
|
||||
const newSharedWorkflow = this.sharedWorkflowRepository.create({
|
||||
role: 'workflow:owner',
|
||||
user: req.user,
|
||||
workflow: savedWorkflow,
|
||||
projectId: project.id,
|
||||
workflow,
|
||||
});
|
||||
|
||||
await transactionManager.save<SharedWorkflow>(newSharedWorkflow);
|
||||
|
||||
return await this.sharedWorkflowRepository.findWorkflowForUser(
|
||||
workflow.id,
|
||||
req.user,
|
||||
['workflow:read'],
|
||||
{ em: transactionManager, includeTags: true },
|
||||
);
|
||||
});
|
||||
|
||||
if (!savedWorkflow) {
|
||||
@@ -132,26 +165,28 @@ export class WorkflowsController {
|
||||
});
|
||||
}
|
||||
|
||||
await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
||||
void this.internalHooks.onWorkflowCreated(req.user, newWorkflow, false);
|
||||
const savedWorkflowWithMetaData =
|
||||
this.enterpriseWorkflowService.addOwnerAndSharings(savedWorkflow);
|
||||
|
||||
return savedWorkflow;
|
||||
// @ts-expect-error: This is added as part of addOwnerAndSharings but
|
||||
// shouldn't be returned to the frontend
|
||||
delete savedWorkflowWithMetaData.shared;
|
||||
|
||||
await this.externalHooks.run('workflow.afterCreate', [savedWorkflow]);
|
||||
void this.internalHooks.onWorkflowCreated(req.user, newWorkflow, project!, false);
|
||||
|
||||
const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id);
|
||||
|
||||
return { ...savedWorkflowWithMetaData, scopes };
|
||||
}
|
||||
|
||||
@Get('/', { middlewares: listQueryMiddleware })
|
||||
async getAll(req: ListQuery.Request, res: express.Response) {
|
||||
async getAll(req: WorkflowRequest.GetMany, res: express.Response) {
|
||||
try {
|
||||
const roles: WorkflowSharingRole[] = this.license.isSharingEnabled()
|
||||
? []
|
||||
: ['workflow:owner'];
|
||||
const sharedWorkflowIds = await this.workflowSharingService.getSharedWorkflowIds(
|
||||
req.user,
|
||||
roles,
|
||||
);
|
||||
|
||||
const { workflows: data, count } = await this.workflowService.getMany(
|
||||
sharedWorkflowIds,
|
||||
req.user,
|
||||
req.listQueryOptions,
|
||||
!!req.query.includeScopes,
|
||||
);
|
||||
|
||||
res.json({ count, data });
|
||||
@@ -210,48 +245,60 @@ export class WorkflowsController {
|
||||
return workflowData;
|
||||
}
|
||||
|
||||
@Get('/:id')
|
||||
@Get('/:workflowId')
|
||||
@ProjectScope('workflow:read')
|
||||
async getWorkflow(req: WorkflowRequest.Get) {
|
||||
const { id: workflowId } = req.params;
|
||||
const { workflowId } = req.params;
|
||||
|
||||
if (this.license.isSharingEnabled()) {
|
||||
const relations = ['shared', 'shared.user'];
|
||||
const relations: FindOptionsRelations<WorkflowEntity> = {
|
||||
shared: {
|
||||
project: {
|
||||
projectRelations: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!config.getEnv('workflowTagsDisabled')) {
|
||||
relations.push('tags');
|
||||
relations.tags = true;
|
||||
}
|
||||
|
||||
const workflow = await this.workflowRepository.get({ id: workflowId }, { relations });
|
||||
const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(
|
||||
workflowId,
|
||||
req.user,
|
||||
['workflow:read'],
|
||||
{ includeTags: !config.getEnv('workflowTagsDisabled') },
|
||||
);
|
||||
|
||||
if (!workflow) {
|
||||
throw new NotFoundError(`Workflow with ID "${workflowId}" does not exist`);
|
||||
}
|
||||
|
||||
const userSharing = workflow.shared?.find((shared) => shared.user.id === req.user.id);
|
||||
if (!userSharing && !req.user.hasGlobalScope('workflow:read')) {
|
||||
throw new UnauthorizedError(
|
||||
'You do not have permission to access this workflow. Ask the owner to share it with you',
|
||||
);
|
||||
}
|
||||
|
||||
const enterpriseWorkflowService = this.enterpriseWorkflowService;
|
||||
|
||||
enterpriseWorkflowService.addOwnerAndSharings(workflow);
|
||||
await enterpriseWorkflowService.addCredentialsToWorkflow(workflow, req.user);
|
||||
return workflow;
|
||||
const workflowWithMetaData = enterpriseWorkflowService.addOwnerAndSharings(workflow);
|
||||
|
||||
await enterpriseWorkflowService.addCredentialsToWorkflow(workflowWithMetaData, req.user);
|
||||
|
||||
// @ts-expect-error: This is added as part of addOwnerAndSharings but
|
||||
// shouldn't be returned to the frontend
|
||||
delete workflowWithMetaData.shared;
|
||||
|
||||
const scopes = await this.workflowService.getWorkflowScopes(req.user, workflowId);
|
||||
|
||||
return { ...workflowWithMetaData, scopes };
|
||||
}
|
||||
|
||||
// sharing disabled
|
||||
|
||||
const extraRelations = config.getEnv('workflowTagsDisabled') ? [] : ['workflow.tags'];
|
||||
|
||||
const shared = await this.sharedWorkflowRepository.findSharing(
|
||||
const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(
|
||||
workflowId,
|
||||
req.user,
|
||||
'workflow:read',
|
||||
{ extraRelations },
|
||||
['workflow:read'],
|
||||
{ includeTags: !config.getEnv('workflowTagsDisabled') },
|
||||
);
|
||||
|
||||
if (!shared) {
|
||||
if (!workflow) {
|
||||
this.logger.verbose('User attempted to access a workflow without permissions', {
|
||||
workflowId,
|
||||
userId: req.user.id,
|
||||
@@ -261,12 +308,15 @@ export class WorkflowsController {
|
||||
);
|
||||
}
|
||||
|
||||
return shared.workflow;
|
||||
const scopes = await this.workflowService.getWorkflowScopes(req.user, workflowId);
|
||||
|
||||
return { ...workflow, scopes };
|
||||
}
|
||||
|
||||
@Patch('/:id')
|
||||
@Patch('/:workflowId')
|
||||
@ProjectScope('workflow:update')
|
||||
async update(req: WorkflowRequest.Update) {
|
||||
const { id: workflowId } = req.params;
|
||||
const { workflowId } = req.params;
|
||||
const forceSave = req.query.forceSave === 'true';
|
||||
|
||||
let updateData = new WorkflowEntity();
|
||||
@@ -288,15 +338,17 @@ export class WorkflowsController {
|
||||
workflowId,
|
||||
tags,
|
||||
isSharingEnabled ? forceSave : true,
|
||||
isSharingEnabled ? undefined : ['workflow:owner'],
|
||||
);
|
||||
|
||||
return updatedWorkflow;
|
||||
const scopes = await this.workflowService.getWorkflowScopes(req.user, workflowId);
|
||||
|
||||
return { ...updatedWorkflow, scopes };
|
||||
}
|
||||
|
||||
@Delete('/:id')
|
||||
@Delete('/:workflowId')
|
||||
@ProjectScope('workflow:delete')
|
||||
async delete(req: WorkflowRequest.Delete) {
|
||||
const { id: workflowId } = req.params;
|
||||
const { workflowId } = req.params;
|
||||
|
||||
const workflow = await this.workflowService.delete(req.user, workflowId);
|
||||
if (!workflow) {
|
||||
@@ -312,19 +364,30 @@ export class WorkflowsController {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Post('/run')
|
||||
@Post('/:workflowId/run')
|
||||
@ProjectScope('workflow:execute')
|
||||
async runManually(req: WorkflowRequest.ManualRun) {
|
||||
if (!req.body.workflowData.id) {
|
||||
throw new ApplicationError('You cannot execute a workflow without an ID', {
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
if (req.params.workflowId !== req.body.workflowData.id) {
|
||||
throw new ApplicationError('Workflow ID in body does not match workflow ID in URL', {
|
||||
level: 'warning',
|
||||
});
|
||||
}
|
||||
|
||||
if (this.license.isSharingEnabled()) {
|
||||
const workflow = this.workflowRepository.create(req.body.workflowData);
|
||||
|
||||
if (req.body.workflowData.id !== undefined) {
|
||||
const safeWorkflow = await this.enterpriseWorkflowService.preventTampering(
|
||||
workflow,
|
||||
workflow.id,
|
||||
req.user,
|
||||
);
|
||||
req.body.workflowData.nodes = safeWorkflow.nodes;
|
||||
}
|
||||
const safeWorkflow = await this.enterpriseWorkflowService.preventTampering(
|
||||
workflow,
|
||||
workflow.id,
|
||||
req.user,
|
||||
);
|
||||
req.body.workflowData.nodes = safeWorkflow.nodes;
|
||||
}
|
||||
|
||||
return await this.workflowExecutionService.executeManually(
|
||||
@@ -335,6 +398,7 @@ export class WorkflowsController {
|
||||
}
|
||||
|
||||
@Put('/:workflowId/share')
|
||||
@ProjectScope('workflow:share')
|
||||
async share(req: WorkflowRequest.Share) {
|
||||
if (!this.license.isSharingEnabled()) throw new NotFoundError('Route not found');
|
||||
|
||||
@@ -348,59 +412,51 @@ export class WorkflowsController {
|
||||
throw new BadRequestError('Bad request');
|
||||
}
|
||||
|
||||
const isOwnedRes = await this.enterpriseWorkflowService.isOwned(req.user, workflowId);
|
||||
const { ownsWorkflow } = isOwnedRes;
|
||||
let { workflow } = isOwnedRes;
|
||||
const workflow = await this.sharedWorkflowRepository.findWorkflowForUser(workflowId, req.user, [
|
||||
'workflow:share',
|
||||
]);
|
||||
|
||||
if (!ownsWorkflow || !workflow) {
|
||||
workflow = undefined;
|
||||
// Allow owners/admins to share
|
||||
if (req.user.hasGlobalScope('workflow:share')) {
|
||||
const sharedRes = await this.sharedWorkflowRepository.getSharing(req.user, workflowId, {
|
||||
allowGlobalScope: true,
|
||||
globalScope: 'workflow:share',
|
||||
});
|
||||
workflow = sharedRes?.workflow;
|
||||
}
|
||||
if (!workflow) {
|
||||
throw new UnauthorizedError('Forbidden');
|
||||
}
|
||||
if (!workflow) {
|
||||
throw new ForbiddenError();
|
||||
}
|
||||
|
||||
const ownerIds = (
|
||||
await this.workflowRepository.getSharings(
|
||||
Db.getConnection().createEntityManager(),
|
||||
workflowId,
|
||||
['shared'],
|
||||
)
|
||||
)
|
||||
.filter((e) => e.role === 'workflow:owner')
|
||||
.map((e) => e.userId);
|
||||
|
||||
let newShareeIds: string[] = [];
|
||||
await Db.transaction(async (trx) => {
|
||||
// remove all sharings that are not supposed to exist anymore
|
||||
await this.workflowRepository.pruneSharings(trx, workflowId, [...ownerIds, ...shareWithIds]);
|
||||
const currentPersonalProjectIDs = workflow.shared
|
||||
.filter((sw) => sw.role === 'workflow:editor')
|
||||
.map((sw) => sw.projectId);
|
||||
const newPersonalProjectIDs = shareWithIds;
|
||||
|
||||
const sharings = await this.workflowRepository.getSharings(trx, workflowId);
|
||||
|
||||
// extract the new sharings that need to be added
|
||||
newShareeIds = utils.rightDiff(
|
||||
[sharings, (sharing) => sharing.userId],
|
||||
[shareWithIds, (shareeId) => shareeId],
|
||||
const toShare = utils.rightDiff(
|
||||
[currentPersonalProjectIDs, (id) => id],
|
||||
[newPersonalProjectIDs, (id) => id],
|
||||
);
|
||||
|
||||
if (newShareeIds.length) {
|
||||
const users = await this.userRepository.getByIds(trx, newShareeIds);
|
||||
await this.sharedWorkflowRepository.share(trx, workflow, users);
|
||||
}
|
||||
const toUnshare = utils.rightDiff(
|
||||
[newPersonalProjectIDs, (id) => id],
|
||||
[currentPersonalProjectIDs, (id) => id],
|
||||
);
|
||||
|
||||
await trx.delete(SharedWorkflow, {
|
||||
workflowId,
|
||||
projectId: In(toUnshare),
|
||||
});
|
||||
|
||||
await this.enterpriseWorkflowService.shareWithProjects(workflow, toShare, trx);
|
||||
|
||||
newShareeIds = toShare;
|
||||
});
|
||||
|
||||
void this.internalHooks.onWorkflowSharingUpdate(workflowId, req.user.id, shareWithIds);
|
||||
|
||||
const projectsRelations = await this.projectRelationRepository.findBy({
|
||||
projectId: In(newShareeIds),
|
||||
role: 'project:personalOwner',
|
||||
});
|
||||
|
||||
await this.mailer.notifyWorkflowShared({
|
||||
sharer: req.user,
|
||||
newShareeIds,
|
||||
newShareeIds: projectsRelations.map((pr) => pr.userId),
|
||||
workflow,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import type { IUser } from 'n8n-workflow';
|
||||
import type { SharedWorkflow } from '@db/entities/SharedWorkflow';
|
||||
import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
|
||||
import type { SlimProject } from '@/requests';
|
||||
|
||||
export interface WorkflowWithSharingsAndCredentials extends Omit<WorkflowEntity, 'shared'> {
|
||||
ownedBy?: IUser | null;
|
||||
sharedWith?: IUser[];
|
||||
homeProject?: SlimProject;
|
||||
sharedWithProjects?: SlimProject[];
|
||||
usedCredentials?: CredentialUsedByWorkflow[];
|
||||
shared?: SharedWorkflow[];
|
||||
}
|
||||
|
||||
export interface WorkflowWithSharingsMetaDataAndCredentials extends Omit<WorkflowEntity, 'shared'> {
|
||||
homeProject?: SlimProject | null;
|
||||
sharedWithProjects: SlimProject[];
|
||||
usedCredentials?: CredentialUsedByWorkflow[];
|
||||
}
|
||||
|
||||
export interface CredentialUsedByWorkflow {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
Reference in New Issue
Block a user