Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Allow creating workflows in other projects than the personal one #8662

Merged
merged 8 commits into from
Feb 21, 2024
7 changes: 5 additions & 2 deletions packages/cli/src/databases/repositories/project.repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Service } from 'typedi';
import type { EntityManager } from '@n8n/typeorm';
import { DataSource, Repository } from '@n8n/typeorm';
import { Project } from '../entities/Project';

Expand All @@ -8,8 +9,10 @@ export class ProjectRepository extends Repository<Project> {
super(Project, dataSource.manager);
}

async getPersonalProjectForUser(userId: string) {
return await this.findOne({
async getPersonalProjectForUser(userId: string, entityManager?: EntityManager) {
const em = entityManager ?? this.manager;

return await em.findOne(Project, {
where: { type: 'personal', projectRelations: { userId, role: 'project:personalOwner' } },
});
}
Expand Down
55 changes: 44 additions & 11 deletions packages/cli/src/services/project.service.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
import type { Project } from '@/databases/entities/Project';
import { ProjectRelation, type ProjectRole } from '@/databases/entities/ProjectRelation';
import { Project } from '@/databases/entities/Project';
import { ProjectRelation } from '@/databases/entities/ProjectRelation';
import type { ProjectRole } from '@/databases/entities/ProjectRelation';
import type { User } from '@/databases/entities/User';
import { ProjectRepository } from '@/databases/repositories/project.repository';
import { ProjectRelationRepository } from '@/databases/repositories/projectRelation.repository';
import { Not, type EntityManager } from '@n8n/typeorm';
import { Service } from 'typedi';
import { type Scope } from '@n8n/permissions';
import { In } from '@n8n/typeorm';
import { RoleService } from './role.service';

@Service()
export class ProjectService {
constructor(
private projectsRepository: ProjectRepository,
private projectRelationRepository: ProjectRelationRepository,
private readonly projectRepository: ProjectRepository,
private readonly projectRelationRepository: ProjectRelationRepository,
private readonly roleService: RoleService,
) {}

async getAccessibleProjects(user: User): Promise<Project[]> {
// This user is probably an admin, show them everything
if (user.hasGlobalScope('project:read')) {
return await this.projectsRepository.find();
return await this.projectRepository.find();
}
return await this.projectsRepository.getAccessibleProjects(user.id);
return await this.projectRepository.getAccessibleProjects(user.id);
}

async getPersonalProjectOwners(projectIds: string[]): Promise<ProjectRelation[]> {
Expand All @@ -39,16 +44,16 @@ export class ProjectService {
} else if (pr) {
name = pr.user.email;
}
return this.projectsRepository.create({
return this.projectRepository.create({
...p,
name,
});
}) as Array<Project & { name: string }>;
}

async createTeamProject(name: string, adminUser: User): Promise<Project> {
const project = await this.projectsRepository.save(
this.projectsRepository.create({
const project = await this.projectRepository.save(
this.projectRepository.create({
name,
type: 'team',
}),
Expand All @@ -67,7 +72,7 @@ export class ProjectService {
}

async getPersonalProject(user: User): Promise<Project | null> {
return await this.projectsRepository.getPersonalProjectForUser(user.id);
return await this.projectRepository.getPersonalProjectForUser(user.id);
}

async getProjectRelationsForUser(user: User): Promise<ProjectRelation[]> {
Expand All @@ -81,7 +86,7 @@ export class ProjectService {
projectId: string,
relations: Array<{ userId: string; role: ProjectRole }>,
) {
const project = await this.projectsRepository.findOneOrFail({
const project = await this.projectRepository.findOneOrFail({
where: { id: projectId, type: Not('personal') },
});
await this.projectRelationRepository.manager.transaction(async (em) => {
Expand Down Expand Up @@ -110,4 +115,32 @@ export class ProjectService {
),
);
}

async getProjectWithScope(
user: User,
projectId: string,
scope: Scope,
entityManager?: EntityManager,
) {
const em = entityManager ?? this.projectRepository.manager;
const projectRoles = this.roleService.rolesWithScope('project', [scope]);

return await em.findOne(Project, {
where: {
id: projectId,
projectRelations: {
role: In(projectRoles),
userId: user.id,
},
},
});
}

async addUser(projectId: string, userId: string, role: ProjectRole) {
return await this.projectRelationRepository.save({
projectId,
userId,
role,
});
}
}
3 changes: 2 additions & 1 deletion packages/cli/src/workflows/workflow.request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { INode, IConnections, IWorkflowSettings, IRunData, IPinData } from

export declare namespace WorkflowRequest {
type CreateUpdatePayload = Partial<{
id: string; // delete if sent
id: string; // deleted if sent
name: string;
nodes: INode[];
connections: IConnections;
Expand All @@ -13,6 +13,7 @@ export declare namespace WorkflowRequest {
tags: string[];
hash: string;
meta: Record<string, unknown>;
projectId: string;
}>;

type ManualRunPayload = {
Expand Down
32 changes: 27 additions & 5 deletions packages/cli/src/workflows/workflows.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
Put,
RestController,
} from '@/decorators';
import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow';
import type { SharedWorkflow, WorkflowSharingRole } from '@db/entities/SharedWorkflow';
import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { TagRepository } from '@db/repositories/tag.repository';
Expand Down Expand Up @@ -48,6 +48,8 @@ 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';

@Authorized()
@RestController('/workflows')
Expand All @@ -72,6 +74,7 @@ export class WorkflowsController {
private readonly mailer: UserManagementMailer,
private readonly credentialsService: CredentialsService,
private readonly projectRepository: ProjectRepository,
private readonly projectService: ProjectService,
) {}

@Post('/')
Expand Down Expand Up @@ -121,14 +124,33 @@ export class WorkflowsController {
await Db.transaction(async (transactionManager) => {
savedWorkflow = await transactionManager.save<WorkflowEntity>(newWorkflow);

const project = await this.projectRepository.getPersonalProjectForUserOrFail(req.user.id);
const { projectId } = req.body;
const project =
projectId === undefined
? await this.projectRepository.getPersonalProjectForUser(req.user.id, transactionManager)
: await this.projectService.getProjectWithScope(
req.user,
projectId,
'workflow:create',
transactionManager,
);

if (typeof projectId === 'string' && project === null) {
despairblue marked this conversation as resolved.
Show resolved Hide resolved
throw new BadRequestError(
"You don't have the permissions to save the workflow in this project.",
);
}

const newSharedWorkflow = new SharedWorkflow();
// Safe guard in case the personal project does not exist for whatever reason.
if (project === null) {
throw new ApplicationError('No personal project found');
}

Object.assign(newSharedWorkflow, {
const newSharedWorkflow = this.sharedWorkflowRepository.create({
role: 'workflow:owner',
// TODO: remove when https://linear.app/n8n/issue/PAY-1353/make-sure-that-sharedworkflowuserid-is-not-used-anymore-to-check lands
user: req.user,
project,
projectId: project.id,
workflow: savedWorkflow,
});

Expand Down
Loading
Loading