From ab0f776df12609fa51bcdd0959736ca6c08dd66c Mon Sep 17 00:00:00 2001 From: Omar Ajoue Date: Tue, 11 Oct 2022 16:40:39 +0200 Subject: [PATCH] feat: return sharees when returning a workflow (#4312) (no-changelog) --- .../src/workflows/workflows.controller.ee.ts | 36 +++- .../src/workflows/workflows.services.ee.ts | 24 +++ .../cli/src/workflows/workflows.services.ts | 5 + packages/cli/src/workflows/workflows.types.ts | 7 + .../cli/test/integration/shared/testDb.ts | 17 ++ .../workflows.controller.ee.test.ts | 200 ++++++++++++------ 6 files changed, 227 insertions(+), 62 deletions(-) create mode 100644 packages/cli/src/workflows/workflows.types.ts diff --git a/packages/cli/src/workflows/workflows.controller.ee.ts b/packages/cli/src/workflows/workflows.controller.ee.ts index 6ed3e6a91d..dc9000b3d2 100644 --- a/packages/cli/src/workflows/workflows.controller.ee.ts +++ b/packages/cli/src/workflows/workflows.controller.ee.ts @@ -1,5 +1,5 @@ import express from 'express'; -import { Db } from '..'; +import { Db, ResponseHelper } from '..'; import config from '../../config'; import type { WorkflowRequest } from '../requests'; import { isSharingEnabled, rightDiff } from '../UserManagement/UserManagementHelper'; @@ -58,3 +58,37 @@ EEWorkflowController.put('/:workflowId/share', async (req: WorkflowRequest.Share return res.status(200).send(); }); + +EEWorkflowController.get( + '/:id', + (req: WorkflowRequest.Get, res, next) => (req.params.id === 'new' ? next('router') : next()), // skip ee router and use free one for naming + ResponseHelper.send(async (req: WorkflowRequest.Get) => { + const { id: workflowId } = req.params; + + if (Number.isNaN(Number(workflowId))) { + throw new ResponseHelper.ResponseError(`Workflow ID must be a number.`, undefined, 400); + } + + const workflow = await EEWorkflows.get( + { id: parseInt(workflowId, 10) }, + { relations: ['shared', 'shared.user', 'shared.role'] }, + ); + + if (!workflow) { + throw new ResponseHelper.ResponseError( + `Workflow with ID "${workflowId}" could not be found.`, + undefined, + 404, + ); + } + + const userSharing = workflow.shared?.find((shared) => shared.user.id === req.user.id); + + if (!userSharing && req.user.globalRole.name !== 'owner') { + throw new ResponseHelper.ResponseError(`Forbidden.`, undefined, 403); + } + // @TODO: also return the credentials used by the workflow + + return EEWorkflows.addOwnerAndSharings(workflow); + }), +); diff --git a/packages/cli/src/workflows/workflows.services.ee.ts b/packages/cli/src/workflows/workflows.services.ee.ts index f55de55e52..89ab7e168f 100644 --- a/packages/cli/src/workflows/workflows.services.ee.ts +++ b/packages/cli/src/workflows/workflows.services.ee.ts @@ -6,6 +6,7 @@ import { WorkflowEntity } from '../databases/entities/WorkflowEntity'; import { RoleService } from '../role/role.service'; import { UserService } from '../user/user.service'; import { WorkflowsService } from './workflows.services'; +import { WorkflowWithSharings } from './workflows.types'; export class EEWorkflowsService extends WorkflowsService { static async isOwned( @@ -70,4 +71,27 @@ export class EEWorkflowsService extends WorkflowsService { return transaction.save(newSharedWorkflows); } + + static addOwnerAndSharings( + workflow: WorkflowEntity & WorkflowWithSharings, + ): WorkflowEntity & WorkflowWithSharings { + workflow.ownedBy = null; + workflow.sharedWith = []; + + workflow.shared?.forEach(({ user, role }) => { + const { id, email, firstName, lastName } = user; + + if (role.name === 'owner') { + workflow.ownedBy = { id, email, firstName, lastName }; + return; + } + + workflow.sharedWith?.push({ id, email, firstName, lastName }); + }); + + // @ts-ignore + delete workflow.shared; + + return workflow; + } } diff --git a/packages/cli/src/workflows/workflows.services.ts b/packages/cli/src/workflows/workflows.services.ts index daf5fdc6cb..20d89ca075 100644 --- a/packages/cli/src/workflows/workflows.services.ts +++ b/packages/cli/src/workflows/workflows.services.ts @@ -2,6 +2,7 @@ import { FindOneOptions, ObjectLiteral } from 'typeorm'; import { Db } from '..'; import { SharedWorkflow } from '../databases/entities/SharedWorkflow'; import { User } from '../databases/entities/User'; +import { WorkflowEntity } from '../databases/entities/WorkflowEntity'; export class WorkflowsService { static async getSharing( @@ -29,4 +30,8 @@ export class WorkflowsService { return Db.collections.SharedWorkflow.findOne(options); } + + static async get(workflow: Partial, options?: { relations: string[] }) { + return Db.collections.Workflow.findOne(workflow, options); + } } diff --git a/packages/cli/src/workflows/workflows.types.ts b/packages/cli/src/workflows/workflows.types.ts new file mode 100644 index 0000000000..cb9f51c248 --- /dev/null +++ b/packages/cli/src/workflows/workflows.types.ts @@ -0,0 +1,7 @@ +import type { IUser } from 'n8n-workflow'; +import { WorkflowEntity } from '../databases/entities/WorkflowEntity'; + +export interface WorkflowWithSharings extends WorkflowEntity { + ownedBy?: IUser | null; + sharedWith?: IUser[]; +} diff --git a/packages/cli/test/integration/shared/testDb.ts b/packages/cli/test/integration/shared/testDb.ts index 787687783c..bbe813c94c 100644 --- a/packages/cli/test/integration/shared/testDb.ts +++ b/packages/cli/test/integration/shared/testDb.ts @@ -458,6 +458,13 @@ export function getWorkflowOwnerRole() { }); } +export function getWorkflowEditorRole() { + return Db.collections.Role.findOneOrFail({ + name: 'editor', + scope: 'workflow', + }); +} + export function getCredentialOwnerRole() { return Db.collections.Role.findOneOrFail({ name: 'owner', @@ -607,6 +614,16 @@ export async function createWorkflow(attributes: Partial = {}, u return workflow; } +export async function shareWorkflowWithUsers(workflow: WorkflowEntity, users: User[]) { + const role = await getWorkflowEditorRole(); + const sharedWorkflows = users.map((user) => ({ + user, + workflow, + role, + })); + return Db.collections.SharedWorkflow.save(sharedWorkflows); +} + /** * Store a workflow in the DB (with a trigger) and optionally assign it to a user. * @param user user to assign the workflow to diff --git a/packages/cli/test/integration/workflows.controller.ee.test.ts b/packages/cli/test/integration/workflows.controller.ee.test.ts index 4794517c55..658f24a266 100644 --- a/packages/cli/test/integration/workflows.controller.ee.test.ts +++ b/packages/cli/test/integration/workflows.controller.ee.test.ts @@ -49,73 +49,151 @@ afterAll(async () => { await testDb.terminate(testDbName); }); -test('PUT /workflows/:id/share should save sharing with new users', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const workflow = await createWorkflow({}, owner); +describe('PUT /workflows/:id', () => { + test('PUT /workflows/:id/share should save sharing with new users', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const member = await testDb.createUser({ globalRole: globalMemberRole }); + const workflow = await createWorkflow({}, owner); - const response = await authAgent(owner) - .put(`/workflows/${workflow.id}/share`) - .send({ shareWithIds: [member.id] }); + const response = await authAgent(owner) + .put(`/workflows/${workflow.id}/share`) + .send({ shareWithIds: [member.id] }); - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(200); - const sharedWorkflows = await testDb.getWorkflowSharing(workflow); - expect(sharedWorkflows).toHaveLength(2); + const sharedWorkflows = await testDb.getWorkflowSharing(workflow); + expect(sharedWorkflows).toHaveLength(2); + }); + + test('PUT /workflows/:id/share should not fail when sharing with invalid user-id', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const workflow = await createWorkflow({}, owner); + + const response = await authAgent(owner) + .put(`/workflows/${workflow.id}/share`) + .send({ shareWithIds: [uuid()] }); + + expect(response.statusCode).toBe(200); + + const sharedWorkflows = await testDb.getWorkflowSharing(workflow); + expect(sharedWorkflows).toHaveLength(1); + }); + + test('PUT /workflows/:id/share should allow sharing with multiple users', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const member = await testDb.createUser({ globalRole: globalMemberRole }); + const anotherMember = await testDb.createUser({ globalRole: globalMemberRole }); + const workflow = await createWorkflow({}, owner); + + const response = await authAgent(owner) + .put(`/workflows/${workflow.id}/share`) + .send({ shareWithIds: [member.id, anotherMember.id] }); + + expect(response.statusCode).toBe(200); + + const sharedWorkflows = await testDb.getWorkflowSharing(workflow); + expect(sharedWorkflows).toHaveLength(3); + }); + + test('PUT /workflows/:id/share should override sharing', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const member = await testDb.createUser({ globalRole: globalMemberRole }); + const anotherMember = await testDb.createUser({ globalRole: globalMemberRole }); + const workflow = await createWorkflow({}, owner); + + const authOwnerAgent = authAgent(owner); + + const response = await authOwnerAgent + .put(`/workflows/${workflow.id}/share`) + .send({ shareWithIds: [member.id, anotherMember.id] }); + + expect(response.statusCode).toBe(200); + + const sharedWorkflows = await testDb.getWorkflowSharing(workflow); + expect(sharedWorkflows).toHaveLength(3); + + const secondResponse = await authOwnerAgent + .put(`/workflows/${workflow.id}/share`) + .send({ shareWithIds: [member.id] }); + expect(secondResponse.statusCode).toBe(200); + + const secondSharedWorkflows = await testDb.getWorkflowSharing(workflow); + expect(secondSharedWorkflows).toHaveLength(2); + }); }); -test('PUT /workflows/:id/share should not fail when sharing with invalid user-id', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const workflow = await createWorkflow({}, owner); +describe('GET /workflows/:id', () => { + test('GET should fail with invalid id', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); - const response = await authAgent(owner) - .put(`/workflows/${workflow.id}/share`) - .send({ shareWithIds: [uuid()] }); + const response = await authOwnerAgent.get('/workflows/potatoes'); - expect(response.statusCode).toBe(200); + expect(response.statusCode).toBe(400); + }); - const sharedWorkflows = await testDb.getWorkflowSharing(workflow); - expect(sharedWorkflows).toHaveLength(1); -}); - -test('PUT /workflows/:id/share should allow sharing with multiple users', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const anotherMember = await testDb.createUser({ globalRole: globalMemberRole }); - const workflow = await createWorkflow({}, owner); - - const response = await authAgent(owner) - .put(`/workflows/${workflow.id}/share`) - .send({ shareWithIds: [member.id, anotherMember.id] }); - - expect(response.statusCode).toBe(200); - - const sharedWorkflows = await testDb.getWorkflowSharing(workflow); - expect(sharedWorkflows).toHaveLength(3); -}); - -test('PUT /workflows/:id/share should override sharing', async () => { - const owner = await testDb.createUser({ globalRole: globalOwnerRole }); - const member = await testDb.createUser({ globalRole: globalMemberRole }); - const anotherMember = await testDb.createUser({ globalRole: globalMemberRole }); - const workflow = await createWorkflow({}, owner); - - const authOwnerAgent = authAgent(owner); - - const response = await authOwnerAgent - .put(`/workflows/${workflow.id}/share`) - .send({ shareWithIds: [member.id, anotherMember.id] }); - - expect(response.statusCode).toBe(200); - - const sharedWorkflows = await testDb.getWorkflowSharing(workflow); - expect(sharedWorkflows).toHaveLength(3); - - const secondResponse = await authOwnerAgent - .put(`/workflows/${workflow.id}/share`) - .send({ shareWithIds: [member.id] }); - expect(secondResponse.statusCode).toBe(200); - - const secondSharedWorkflows = await testDb.getWorkflowSharing(workflow); - expect(secondSharedWorkflows).toHaveLength(2); + test('GET should return a workflow with owner', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const workflow = await createWorkflow({}, owner); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + + const response = await authOwnerAgent.get(`/workflows/${workflow.id}`); + + expect(response.statusCode).toBe(200); + expect(response.body.data.ownedBy).toMatchObject({ + id: owner.id, + email: owner.email, + firstName: owner.firstName, + lastName: owner.lastName, + }); + + expect(response.body.data.sharedWith).toHaveLength(0); + }); + + test('GET should return shared workflow with user data', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const member = await testDb.createUser({ globalRole: globalMemberRole }); + const workflow = await createWorkflow({}, owner); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + await testDb.shareWorkflowWithUsers(workflow, [member]); + + const response = await authOwnerAgent.get(`/workflows/${workflow.id}`); + + expect(response.statusCode).toBe(200); + expect(response.body.data.ownedBy).toMatchObject({ + id: owner.id, + email: owner.email, + firstName: owner.firstName, + lastName: owner.lastName, + }); + + expect(response.body.data.sharedWith).toHaveLength(1); + expect(response.body.data.sharedWith[0]).toMatchObject({ + id: member.id, + email: member.email, + firstName: member.firstName, + lastName: member.lastName, + }); + }); + + test('GET should return all sharees', async () => { + const owner = await testDb.createUser({ globalRole: globalOwnerRole }); + const member1 = await testDb.createUser({ globalRole: globalMemberRole }); + const member2 = await testDb.createUser({ globalRole: globalMemberRole }); + const workflow = await createWorkflow({}, owner); + const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner }); + await testDb.shareWorkflowWithUsers(workflow, [member1, member2]); + + const response = await authOwnerAgent.get(`/workflows/${workflow.id}`); + + expect(response.statusCode).toBe(200); + expect(response.body.data.ownedBy).toMatchObject({ + id: owner.id, + email: owner.email, + firstName: owner.firstName, + lastName: owner.lastName, + }); + + expect(response.body.data.sharedWith).toHaveLength(2); + }); });