fix(core): Don't create multiple owners when importing credentials or workflows (#9112)

This commit is contained in:
Danny Martini 2024-04-12 17:25:59 +02:00 committed by GitHub
parent 5433004d77
commit 3eb5be5f5a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 826 additions and 184 deletions

View file

@ -64,67 +64,25 @@ export class ImportCredentialsCommand extends BaseCommand {
} }
} }
let totalImported = 0;
const cipher = Container.get(Cipher);
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner();
if (flags.separate) { const credentials = await this.readCredentials(flags.input, flags.separate);
let { input: inputPath } = flags;
if (process.platform === 'win32') {
inputPath = inputPath.replace(/\\/g, '/');
}
const files = await glob('*.json', {
cwd: inputPath,
absolute: true,
});
totalImported = files.length;
await Db.getConnection().transaction(async (transactionManager) => { await Db.getConnection().transaction(async (transactionManager) => {
this.transactionManager = transactionManager; this.transactionManager = transactionManager;
for (const file of files) {
const credential = jsonParse<ICredentialsEncrypted>(
fs.readFileSync(file, { encoding: 'utf8' }),
);
if (typeof credential.data === 'object') {
// plain data / decrypted input. Should be encrypted first.
credential.data = cipher.encrypt(credential.data);
}
await this.storeCredential(credential, user);
}
});
this.reportSuccess(totalImported); const result = await this.checkRelations(credentials, flags.userId);
return;
if (!result.success) {
throw new ApplicationError(result.message);
} }
const credentials = jsonParse<ICredentialsEncrypted[]>(
fs.readFileSync(flags.input, { encoding: 'utf8' }),
);
totalImported = credentials.length;
if (!Array.isArray(credentials)) {
throw new ApplicationError(
'File does not seem to contain credentials. Make sure the credentials are contained in an array.',
);
}
await Db.getConnection().transaction(async (transactionManager) => {
this.transactionManager = transactionManager;
for (const credential of credentials) { for (const credential of credentials) {
if (typeof credential.data === 'object') {
// plain data / decrypted input. Should be encrypted first.
credential.data = cipher.encrypt(credential.data);
}
await this.storeCredential(credential, user); await this.storeCredential(credential, user);
} }
}); });
this.reportSuccess(totalImported); this.reportSuccess(credentials.length);
} }
async catch(error: Error) { async catch(error: Error) {
@ -142,6 +100,13 @@ export class ImportCredentialsCommand extends BaseCommand {
private async storeCredential(credential: Partial<CredentialsEntity>, user: User) { private async storeCredential(credential: Partial<CredentialsEntity>, user: User) {
const result = await this.transactionManager.upsert(CredentialsEntity, credential, ['id']); const result = await this.transactionManager.upsert(CredentialsEntity, credential, ['id']);
const sharingExists = await this.transactionManager.existsBy(SharedCredentials, {
credentialsId: credential.id,
role: 'credential:owner',
});
if (!sharingExists) {
await this.transactionManager.upsert( await this.transactionManager.upsert(
SharedCredentials, SharedCredentials,
{ {
@ -152,6 +117,7 @@ export class ImportCredentialsCommand extends BaseCommand {
['credentialsId', 'userId'], ['credentialsId', 'userId'],
); );
} }
}
private async getOwner() { private async getOwner() {
const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' }); const owner = await Container.get(UserRepository).findOneBy({ role: 'global:owner' });
@ -162,6 +128,84 @@ export class ImportCredentialsCommand extends BaseCommand {
return owner; return owner;
} }
private async checkRelations(credentials: ICredentialsEncrypted[], userId?: string) {
if (!userId) {
return {
success: true as const,
message: undefined,
};
}
for (const credential of credentials) {
if (credential.id === undefined) {
continue;
}
if (!(await this.credentialExists(credential.id))) {
continue;
}
const ownerId = await this.getCredentialOwner(credential.id);
if (!ownerId) {
continue;
}
if (ownerId !== userId) {
return {
success: false as const,
message: `The credential with id "${credential.id}" is already owned by the user with the id "${ownerId}". It can't be re-owned by the user with the id "${userId}"`,
};
}
}
return {
success: true as const,
message: undefined,
};
}
private async readCredentials(path: string, separate: boolean): Promise<ICredentialsEncrypted[]> {
const cipher = Container.get(Cipher);
if (process.platform === 'win32') {
path = path.replace(/\\/g, '/');
}
let credentials: ICredentialsEncrypted[];
if (separate) {
const files = await glob('*.json', {
cwd: path,
absolute: true,
});
credentials = files.map((file) =>
jsonParse<ICredentialsEncrypted>(fs.readFileSync(file, { encoding: 'utf8' })),
);
} else {
const credentialsUnchecked = jsonParse<ICredentialsEncrypted[]>(
fs.readFileSync(path, { encoding: 'utf8' }),
);
if (!Array.isArray(credentialsUnchecked)) {
throw new ApplicationError(
'File does not seem to contain credentials. Make sure the credentials are contained in an array.',
);
}
credentials = credentialsUnchecked;
}
return credentials.map((credential) => {
if (typeof credential.data === 'object') {
// plain data / decrypted input. Should be encrypted first.
credential.data = cipher.encrypt(credential.data);
}
return credential;
});
}
private async getAssignee(userId: string) { private async getAssignee(userId: string) {
const user = await Container.get(UserRepository).findOneBy({ id: userId }); const user = await Container.get(UserRepository).findOneBy({ id: userId });
@ -171,4 +215,17 @@ export class ImportCredentialsCommand extends BaseCommand {
return user; return user;
} }
private async getCredentialOwner(credentialsId: string) {
const sharedCredential = await this.transactionManager.findOneBy(SharedCredentials, {
credentialsId,
role: 'credential:owner',
});
return sharedCredential?.userId;
}
private async credentialExists(credentialId: string) {
return await this.transactionManager.existsBy(CredentialsEntity, { id: credentialId });
}
} }

View file

@ -13,6 +13,7 @@ import { WorkflowRepository } from '@db/repositories/workflow.repository';
import type { IWorkflowToImport } from '@/Interfaces'; import type { IWorkflowToImport } from '@/Interfaces';
import { ImportService } from '@/services/import.service'; import { ImportService } from '@/services/import.service';
import { BaseCommand } from '../BaseCommand'; import { BaseCommand } from '../BaseCommand';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] { function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] {
if (!Array.isArray(workflows)) { if (!Array.isArray(workflows)) {
@ -78,53 +79,52 @@ export class ImportWorkflowsCommand extends BaseCommand {
} }
} }
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner(); const owner = await this.getOwner();
let totalImported = 0; const workflows = await this.readWorkflows(flags.input, flags.separate);
if (flags.separate) { const result = await this.checkRelations(workflows, flags.userId);
let { input: inputPath } = flags; if (!result.success) {
throw new ApplicationError(result.message);
if (process.platform === 'win32') {
inputPath = inputPath.replace(/\\/g, '/');
} }
const files = await glob('*.json', { this.logger.info(`Importing ${workflows.length} workflows...`);
cwd: inputPath,
absolute: true,
});
totalImported = files.length; await Container.get(ImportService).importWorkflows(workflows, flags.userId ?? owner.id);
this.logger.info(`Importing ${totalImported} workflows...`);
for (const file of files) { this.reportSuccess(workflows.length);
const workflow = jsonParse<IWorkflowToImport>(fs.readFileSync(file, { encoding: 'utf8' }));
if (!workflow.id) {
workflow.id = generateNanoId();
} }
const _workflow = Container.get(WorkflowRepository).create(workflow); private async checkRelations(workflows: WorkflowEntity[], userId: string | undefined) {
if (!userId) {
await Container.get(ImportService).importWorkflows([_workflow], user.id); return {
success: true as const,
message: undefined,
};
} }
this.reportSuccess(totalImported); for (const workflow of workflows) {
process.exit(); if (!(await this.workflowExists(workflow))) {
continue;
} }
const workflows = jsonParse<IWorkflowToImport[]>( const ownerId = await this.getWorkflowOwner(workflow);
fs.readFileSync(flags.input, { encoding: 'utf8' }), if (!ownerId) {
); continue;
}
const _workflows = workflows.map((w) => Container.get(WorkflowRepository).create(w)); if (ownerId !== userId) {
return {
success: false as const,
message: `The credential with id "${workflow.id}" is already owned by the user with the id "${ownerId}". It can't be re-owned by the user with the id "${userId}"`,
};
}
}
assertHasWorkflowsToImport(workflows); return {
success: true as const,
totalImported = workflows.length; message: undefined,
};
await Container.get(ImportService).importWorkflows(_workflows, user.id);
this.reportSuccess(totalImported);
} }
async catch(error: Error) { async catch(error: Error) {
@ -145,13 +145,48 @@ export class ImportWorkflowsCommand extends BaseCommand {
return owner; return owner;
} }
private async getAssignee(userId: string) { private async getWorkflowOwner(workflow: WorkflowEntity) {
const user = await Container.get(UserRepository).findOneBy({ id: userId }); const sharing = await Container.get(SharedWorkflowRepository).findOneBy({
workflowId: workflow.id,
role: 'workflow:owner',
});
if (!user) { return sharing?.userId;
throw new ApplicationError('Failed to find user', { extra: { userId } });
} }
return user; private async workflowExists(workflow: WorkflowEntity) {
return await Container.get(WorkflowRepository).existsBy({ id: workflow.id });
}
private async readWorkflows(path: string, separate: boolean): Promise<WorkflowEntity[]> {
if (process.platform === 'win32') {
path = path.replace(/\\/g, '/');
}
if (separate) {
const files = await glob('*.json', {
cwd: path,
absolute: true,
});
const workflowInstances = files.map((file) => {
const workflow = jsonParse<IWorkflowToImport>(fs.readFileSync(file, { encoding: 'utf8' }));
if (!workflow.id) {
workflow.id = generateNanoId();
}
const workflowInstance = Container.get(WorkflowRepository).create(workflow);
return workflowInstance;
});
return workflowInstances;
} else {
const workflows = jsonParse<IWorkflowToImport[]>(fs.readFileSync(path, { encoding: 'utf8' }));
const workflowInstances = workflows.map((w) => Container.get(WorkflowRepository).create(w));
assertHasWorkflowsToImport(workflows);
return workflowInstances;
}
} }
} }

View file

@ -53,14 +53,18 @@ export class ImportService {
this.logger.info(`Deactivating workflow "${workflow.name}". Remember to activate later.`); this.logger.info(`Deactivating workflow "${workflow.name}". Remember to activate later.`);
} }
const upsertResult = await tx.upsert(WorkflowEntity, workflow, ['id']); const exists = workflow.id ? await tx.existsBy(WorkflowEntity, { id: workflow.id }) : false;
const upsertResult = await tx.upsert(WorkflowEntity, workflow, ['id']);
const workflowId = upsertResult.identifiers.at(0)?.id as string; const workflowId = upsertResult.identifiers.at(0)?.id as string;
// Create relationship if the workflow was inserted instead of updated.
if (!exists) {
await tx.upsert(SharedWorkflow, { workflowId, userId, role: 'workflow:owner' }, [ await tx.upsert(SharedWorkflow, { workflowId, userId, role: 'workflow:owner' }, [
'workflowId', 'workflowId',
'userId', 'userId',
]); ]);
}
if (!workflow.tags?.length) continue; if (!workflow.tags?.length) continue;

View file

@ -6,10 +6,17 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../../shared/mocking'; import { mockInstance } from '../../shared/mocking';
import * as testDb from '../shared/testDb'; import * as testDb from '../shared/testDb';
import { getAllCredentials } from '../shared/db/credentials'; import { getAllCredentials, getAllSharedCredentials } from '../shared/db/credentials';
import { createMember, createOwner } from '../shared/db/users';
const oclifConfig = new Config({ root: __dirname }); const oclifConfig = new Config({ root: __dirname });
async function importCredential(argv: string[]) {
const importer = new ImportCredentialsCommand(argv, oclifConfig);
await importer.init();
await importer.run();
}
beforeAll(async () => { beforeAll(async () => {
mockInstance(InternalHooks); mockInstance(InternalHooks);
mockInstance(LoadNodesAndCredentials); mockInstance(LoadNodesAndCredentials);
@ -17,7 +24,7 @@ beforeAll(async () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate(['Credentials']); await testDb.truncate(['Credentials', 'SharedCredentials', 'User']);
}); });
afterAll(async () => { afterAll(async () => {
@ -25,25 +32,202 @@ afterAll(async () => {
}); });
test('import:credentials should import a credential', async () => { test('import:credentials should import a credential', async () => {
const before = await getAllCredentials(); //
expect(before.length).toBe(0); // ARRANGE
const importer = new ImportCredentialsCommand( //
['--input=./test/integration/commands/importCredentials/credentials.json'], const owner = await createOwner();
oclifConfig,
); //
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { // ACT
throw new Error('process.exit'); //
await importCredential([
'--input=./test/integration/commands/importCredentials/credentials.json',
]);
//
// ASSERT
//
const after = {
credentials: await getAllCredentials(),
sharings: await getAllSharedCredentials(),
};
expect(after).toMatchObject({
credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })],
sharings: [
expect.objectContaining({ credentialsId: '123', userId: owner.id, role: 'credential:owner' }),
],
});
});
test('import:credentials should import a credential from separated files', async () => {
//
// ARRANGE
//
const owner = await createOwner();
//
// ACT
//
// import credential the first time, assigning it to the owner
await importCredential([
'--separate',
'--input=./test/integration/commands/importCredentials/separate',
]);
//
// ASSERT
//
const after = {
credentials: await getAllCredentials(),
sharings: await getAllSharedCredentials(),
};
expect(after).toMatchObject({
credentials: [
expect.objectContaining({
id: '123',
name: 'cred-aws-test',
}),
],
sharings: [
expect.objectContaining({
credentialsId: '123',
userId: owner.id,
role: 'credential:owner',
}),
],
});
});
test('`import:credentials --userId ...` should fail if the credential exists already and is owned by somebody else', async () => {
//
// ARRANGE
//
const owner = await createOwner();
const member = await createMember();
// import credential the first time, assigning it to the owner
await importCredential([
'--input=./test/integration/commands/importCredentials/credentials.json',
`--userId=${owner.id}`,
]);
// making sure the import worked
const before = {
credentials: await getAllCredentials(),
sharings: await getAllSharedCredentials(),
};
expect(before).toMatchObject({
credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })],
sharings: [
expect.objectContaining({
credentialsId: '123',
userId: owner.id,
role: 'credential:owner',
}),
],
}); });
await importer.init(); //
try { // ACT
await importer.run(); //
} catch (error) {
expect(error.message).toBe('process.exit'); // Import again while updating the name we try to assign the
} // credential to another user.
const after = await getAllCredentials(); await expect(
expect(after.length).toBe(1); importCredential([
expect(after[0].name).toBe('cred-aws-test'); '--input=./test/integration/commands/importCredentials/credentials-updated.json',
expect(after[0].id).toBe('123'); `--userId=${member.id}`,
mockExit.mockRestore(); ]),
).rejects.toThrowError(
`The credential with id "123" is already owned by the user with the id "${owner.id}". It can't be re-owned by the user with the id "${member.id}"`,
);
//
// ASSERT
//
const after = {
credentials: await getAllCredentials(),
sharings: await getAllSharedCredentials(),
};
expect(after).toMatchObject({
credentials: [
expect.objectContaining({
id: '123',
// only the name was updated
name: 'cred-aws-test',
}),
],
sharings: [
expect.objectContaining({
credentialsId: '123',
userId: owner.id,
role: 'credential:owner',
}),
],
});
});
test("only update credential, don't create or update owner if `--userId` is not passed", async () => {
//
// ARRANGE
//
await createOwner();
const member = await createMember();
// import credential the first time, assigning it to a member
await importCredential([
'--input=./test/integration/commands/importCredentials/credentials.json',
`--userId=${member.id}`,
]);
// making sure the import worked
const before = {
credentials: await getAllCredentials(),
sharings: await getAllSharedCredentials(),
};
expect(before).toMatchObject({
credentials: [expect.objectContaining({ id: '123', name: 'cred-aws-test' })],
sharings: [
expect.objectContaining({
credentialsId: '123',
userId: member.id,
role: 'credential:owner',
}),
],
});
//
// ACT
//
// Import again only updating the name and omitting `--userId`
await importCredential([
'--input=./test/integration/commands/importCredentials/credentials-updated.json',
]);
//
// ASSERT
//
const after = {
credentials: await getAllCredentials(),
sharings: await getAllSharedCredentials(),
};
expect(after).toMatchObject({
credentials: [
expect.objectContaining({
id: '123',
// only the name was updated
name: 'cred-aws-prod',
}),
],
sharings: [
expect.objectContaining({
credentialsId: '123',
userId: member.id,
role: 'credential:owner',
}),
],
});
}); });

View file

@ -6,10 +6,17 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../../shared/mocking'; import { mockInstance } from '../../shared/mocking';
import * as testDb from '../shared/testDb'; import * as testDb from '../shared/testDb';
import { getAllWorkflows } from '../shared/db/workflows'; import { getAllSharedWorkflows, getAllWorkflows } from '../shared/db/workflows';
import { createMember, createOwner } from '../shared/db/users';
const oclifConfig = new Config({ root: __dirname }); const oclifConfig = new Config({ root: __dirname });
async function importWorkflow(argv: string[]) {
const importer = new ImportWorkflowsCommand(argv, oclifConfig);
await importer.init();
await importer.run();
}
beforeAll(async () => { beforeAll(async () => {
mockInstance(InternalHooks); mockInstance(InternalHooks);
mockInstance(LoadNodesAndCredentials); mockInstance(LoadNodesAndCredentials);
@ -17,7 +24,7 @@ beforeAll(async () => {
}); });
beforeEach(async () => { beforeEach(async () => {
await testDb.truncate(['Workflow']); await testDb.truncate(['Workflow', 'SharedWorkflow', 'User']);
}); });
afterAll(async () => { afterAll(async () => {
@ -25,53 +32,186 @@ afterAll(async () => {
}); });
test('import:workflow should import active workflow and deactivate it', async () => { test('import:workflow should import active workflow and deactivate it', async () => {
const before = await getAllWorkflows(); //
expect(before.length).toBe(0); // ARRANGE
const importer = new ImportWorkflowsCommand( //
['--separate', '--input=./test/integration/commands/importWorkflows/separate'], const owner = await createOwner();
oclifConfig,
);
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit');
});
await importer.init(); //
try { // ACT
await importer.run(); //
} catch (error) { await importWorkflow([
expect(error.message).toBe('process.exit'); '--separate',
} '--input=./test/integration/commands/importWorkflows/separate',
const after = await getAllWorkflows(); ]);
expect(after.length).toBe(2);
expect(after[0].name).toBe('active-workflow'); //
expect(after[0].active).toBe(false); // ASSERT
expect(after[1].name).toBe('inactive-workflow'); //
expect(after[1].active).toBe(false); const after = {
mockExit.mockRestore(); workflows: await getAllWorkflows(),
sharings: await getAllSharedWorkflows(),
};
expect(after).toMatchObject({
workflows: [
expect.objectContaining({ name: 'active-workflow', active: false }),
expect.objectContaining({ name: 'inactive-workflow', active: false }),
],
sharings: [
expect.objectContaining({ workflowId: '998', userId: owner.id, role: 'workflow:owner' }),
expect.objectContaining({ workflowId: '999', userId: owner.id, role: 'workflow:owner' }),
],
});
}); });
test('import:workflow should import active workflow from combined file and deactivate it', async () => { test('import:workflow should import active workflow from combined file and deactivate it', async () => {
const before = await getAllWorkflows(); //
expect(before.length).toBe(0); // ARRANGE
const importer = new ImportWorkflowsCommand( //
['--input=./test/integration/commands/importWorkflows/combined/combined.json'], const owner = await createOwner();
oclifConfig,
); //
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { // ACT
throw new Error('process.exit'); //
await importWorkflow([
'--input=./test/integration/commands/importWorkflows/combined/combined.json',
]);
//
// ASSERT
//
const after = {
workflows: await getAllWorkflows(),
sharings: await getAllSharedWorkflows(),
};
expect(after).toMatchObject({
workflows: [
expect.objectContaining({ name: 'active-workflow', active: false }),
expect.objectContaining({ name: 'inactive-workflow', active: false }),
],
sharings: [
expect.objectContaining({ workflowId: '998', userId: owner.id, role: 'workflow:owner' }),
expect.objectContaining({ workflowId: '999', userId: owner.id, role: 'workflow:owner' }),
],
});
});
test('`import:workflow --userId ...` should fail if the workflow exists already and is owned by somebody else', async () => {
//
// ARRANGE
//
const owner = await createOwner();
const member = await createMember();
// Import workflow the first time, assigning it to a member.
await importWorkflow([
'--input=./test/integration/commands/importWorkflows/combined-with-update/original.json',
`--userId=${owner.id}`,
]);
const before = {
workflows: await getAllWorkflows(),
sharings: await getAllSharedWorkflows(),
};
// Make sure the workflow and sharing have been created.
expect(before).toMatchObject({
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })],
sharings: [
expect.objectContaining({
workflowId: '998',
userId: owner.id,
role: 'workflow:owner',
}),
],
}); });
await importer.init(); //
try { // ACT
await importer.run(); //
} catch (error) { // Import the same workflow again, with another name but the same ID, and try
expect(error.message).toBe('process.exit'); // to assign it to the member.
} await expect(
const after = await getAllWorkflows(); importWorkflow([
expect(after.length).toBe(2); '--input=./test/integration/commands/importWorkflows/combined-with-update/updated.json',
expect(after[0].name).toBe('active-workflow'); `--userId=${member.id}`,
expect(after[0].active).toBe(false); ]),
expect(after[1].name).toBe('inactive-workflow'); ).rejects.toThrowError(
expect(after[1].active).toBe(false); `The credential with id "998" is already owned by the user with the id "${owner.id}". It can't be re-owned by the user with the id "${member.id}"`,
mockExit.mockRestore(); );
//
// ASSERT
//
const after = {
workflows: await getAllWorkflows(),
sharings: await getAllSharedWorkflows(),
};
// Make sure there is no new sharing and that the name DID NOT change.
expect(after).toMatchObject({
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })],
sharings: [
expect.objectContaining({
workflowId: '998',
userId: owner.id,
role: 'workflow:owner',
}),
],
});
});
test("only update the workflow, don't create or update the owner if `--userId` is not passed", async () => {
//
// ARRANGE
//
await createOwner();
const member = await createMember();
// Import workflow the first time, assigning it to a member.
await importWorkflow([
'--input=./test/integration/commands/importWorkflows/combined-with-update/original.json',
`--userId=${member.id}`,
]);
const before = {
workflows: await getAllWorkflows(),
sharings: await getAllSharedWorkflows(),
};
// Make sure the workflow and sharing have been created.
expect(before).toMatchObject({
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow' })],
sharings: [
expect.objectContaining({
workflowId: '998',
userId: member.id,
role: 'workflow:owner',
}),
],
});
//
// ACT
//
// Import the same workflow again, with another name but the same ID.
await importWorkflow([
'--input=./test/integration/commands/importWorkflows/combined-with-update/updated.json',
]);
//
// ASSERT
//
const after = {
workflows: await getAllWorkflows(),
sharings: await getAllSharedWorkflows(),
};
// Make sure there is no new sharing and that the name changed.
expect(after).toMatchObject({
workflows: [expect.objectContaining({ id: '998', name: 'active-workflow updated' })],
sharings: [
expect.objectContaining({
workflowId: '998',
userId: member.id,
role: 'workflow:owner',
}),
],
});
}); });

View file

@ -0,0 +1,14 @@
[
{
"createdAt": "2023-07-10T14:50:49.193Z",
"updatedAt": "2023-10-27T13:34:42.917Z",
"id": "123",
"name": "cred-aws-prod",
"data": {
"region": "eu-west-1",
"accessKeyId": "999999999999",
"secretAccessKey": "aaaaaaaaaaaaa"
},
"type": "aws"
}
]

View file

@ -0,0 +1,12 @@
{
"createdAt": "2023-07-10T14:50:49.193Z",
"updatedAt": "2023-10-27T13:34:42.917Z",
"id": "123",
"name": "cred-aws-test",
"data": {
"region": "eu-west-1",
"accessKeyId": "999999999999",
"secretAccessKey": "aaaaaaaaaaaaa"
},
"type": "aws"
}

View file

@ -0,0 +1,81 @@
[
{
"name": "active-workflow",
"nodes": [
{
"parameters": {
"path": "e20b4873-fcf7-4bce-88fc-a1a56d66b138",
"responseMode": "responseNode",
"options": {}
},
"id": "c26d8782-bd57-43d0-86dc-0c618a7e4024",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [800, 580],
"webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b138"
},
{
"parameters": {
"values": {
"boolean": [
{
"name": "hooked",
"value": true
}
]
},
"options": {}
},
"id": "9701b1ef-9ab0-432a-b086-cf76981b097d",
"name": "Set",
"type": "n8n-nodes-base.set",
"typeVersion": 1,
"position": [1020, 580]
},
{
"parameters": {
"options": {}
},
"id": "d0f086b8-c2b2-4404-b347-95d3f91e555a",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [1240, 580]
}
],
"pinData": {},
"connections": {
"Webhook": {
"main": [
[
{
"node": "Set",
"type": "main",
"index": 0
}
]
]
},
"Set": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {},
"versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f",
"id": "998",
"meta": {
"instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9"
},
"tags": []
}
]

View file

@ -0,0 +1,81 @@
[
{
"name": "active-workflow updated",
"nodes": [
{
"parameters": {
"path": "e20b4873-fcf7-4bce-88fc-a1a56d66b138",
"responseMode": "responseNode",
"options": {}
},
"id": "c26d8782-bd57-43d0-86dc-0c618a7e4024",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [800, 580],
"webhookId": "e20b4873-fcf7-4bce-88fc-a1a56d66b138"
},
{
"parameters": {
"values": {
"boolean": [
{
"name": "hooked",
"value": true
}
]
},
"options": {}
},
"id": "9701b1ef-9ab0-432a-b086-cf76981b097d",
"name": "Set",
"type": "n8n-nodes-base.set",
"typeVersion": 1,
"position": [1020, 580]
},
{
"parameters": {
"options": {}
},
"id": "d0f086b8-c2b2-4404-b347-95d3f91e555a",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [1240, 580]
}
],
"pinData": {},
"connections": {
"Webhook": {
"main": [
[
{
"node": "Set",
"type": "main",
"index": 0
}
]
]
},
"Set": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {},
"versionId": "40a70df1-740f-47e7-8e16-50a0bcd5b70f",
"id": "998",
"meta": {
"instanceId": "95977dc4769098fc608439605527ee75d23f10d551aed6b87a3eea1a252c0ba9"
},
"tags": []
}
]

View file

@ -12,8 +12,13 @@ import { SharedWorkflowRepository } from '@/databases/repositories/sharedWorkflo
import * as testDb from './shared/testDb'; import * as testDb from './shared/testDb';
import { mockInstance } from '../shared/mocking'; import { mockInstance } from '../shared/mocking';
import { createOwner } from './shared/db/users'; import { createMember, createOwner } from './shared/db/users';
import { createWorkflow, getWorkflowById } from './shared/db/workflows'; import {
createWorkflow,
getAllSharedWorkflows,
getWorkflowById,
newWorkflow,
} from './shared/db/workflows';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
@ -57,7 +62,7 @@ describe('ImportService', () => {
}); });
test('should make user owner of imported workflow', async () => { test('should make user owner of imported workflow', async () => {
const workflowToImport = await createWorkflow(); const workflowToImport = newWorkflow();
await importService.importWorkflows([workflowToImport], owner.id); await importService.importWorkflows([workflowToImport], owner.id);
@ -68,6 +73,23 @@ describe('ImportService', () => {
expect(dbSharing.userId).toBe(owner.id); expect(dbSharing.userId).toBe(owner.id);
}); });
test('should not change the owner if it already exists', async () => {
const member = await createMember();
const workflowToImport = await createWorkflow(undefined, owner);
await importService.importWorkflows([workflowToImport], member.id);
const sharings = await getAllSharedWorkflows();
expect(sharings).toMatchObject([
expect.objectContaining({
workflowId: workflowToImport.id,
userId: owner.id,
role: 'workflow:owner',
}),
]);
});
test('should deactivate imported workflow if active', async () => { test('should deactivate imported workflow if active', async () => {
const workflowToImport = await createWorkflow({ active: true }); const workflowToImport = await createWorkflow({ active: true });

View file

@ -91,3 +91,7 @@ export async function getAllCredentials() {
export const getCredentialById = async (id: string) => export const getCredentialById = async (id: string) =>
await Container.get(CredentialsRepository).findOneBy({ id }); await Container.get(CredentialsRepository).findOneBy({ id });
export async function getAllSharedCredentials() {
return await Container.get(SharedCredentialsRepository).find();
}

View file

@ -19,12 +19,7 @@ export async function createManyWorkflows(
return await Promise.all(workflowRequests); return await Promise.all(workflowRequests);
} }
/** export function newWorkflow(attributes: Partial<WorkflowEntity> = {}): WorkflowEntity {
* Store a workflow in the DB (without a trigger) and optionally assign it to a user.
* @param attributes workflow attributes
* @param user user to assign the workflow to
*/
export async function createWorkflow(attributes: Partial<WorkflowEntity> = {}, user?: User) {
const { active, name, nodes, connections, versionId } = attributes; const { active, name, nodes, connections, versionId } = attributes;
const workflowEntity = Container.get(WorkflowRepository).create({ const workflowEntity = Container.get(WorkflowRepository).create({
@ -45,7 +40,16 @@ export async function createWorkflow(attributes: Partial<WorkflowEntity> = {}, u
...attributes, ...attributes,
}); });
const workflow = await Container.get(WorkflowRepository).save(workflowEntity); return workflowEntity;
}
/**
* Store a workflow in the DB (without a trigger) and optionally assign it to a user.
* @param attributes workflow attributes
* @param user user to assign the workflow to
*/
export async function createWorkflow(attributes: Partial<WorkflowEntity> = {}, user?: User) {
const workflow = await Container.get(WorkflowRepository).save(newWorkflow(attributes));
if (user) { if (user) {
await Container.get(SharedWorkflowRepository).save({ await Container.get(SharedWorkflowRepository).save({
@ -121,5 +125,9 @@ export async function getAllWorkflows() {
return await Container.get(WorkflowRepository).find(); return await Container.get(WorkflowRepository).find();
} }
export async function getAllSharedWorkflows() {
return await Container.get(SharedWorkflowRepository).find();
}
export const getWorkflowById = async (id: string) => export const getWorkflowById = async (id: string) =>
await Container.get(WorkflowRepository).findOneBy({ id }); await Container.get(WorkflowRepository).findOneBy({ id });