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
12 changed files with 826 additions and 184 deletions

View File

@@ -6,10 +6,17 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { mockInstance } from '../../shared/mocking';
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 });
async function importCredential(argv: string[]) {
const importer = new ImportCredentialsCommand(argv, oclifConfig);
await importer.init();
await importer.run();
}
beforeAll(async () => {
mockInstance(InternalHooks);
mockInstance(LoadNodesAndCredentials);
@@ -17,7 +24,7 @@ beforeAll(async () => {
});
beforeEach(async () => {
await testDb.truncate(['Credentials']);
await testDb.truncate(['Credentials', 'SharedCredentials', 'User']);
});
afterAll(async () => {
@@ -25,25 +32,202 @@ afterAll(async () => {
});
test('import:credentials should import a credential', async () => {
const before = await getAllCredentials();
expect(before.length).toBe(0);
const importer = new ImportCredentialsCommand(
['--input=./test/integration/commands/importCredentials/credentials.json'],
oclifConfig,
);
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit');
//
// ARRANGE
//
const owner = await createOwner();
//
// ACT
//
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 {
await importer.run();
} catch (error) {
expect(error.message).toBe('process.exit');
}
const after = await getAllCredentials();
expect(after.length).toBe(1);
expect(after[0].name).toBe('cred-aws-test');
expect(after[0].id).toBe('123');
mockExit.mockRestore();
//
// ACT
//
// Import again while updating the name we try to assign the
// credential to another user.
await expect(
importCredential([
'--input=./test/integration/commands/importCredentials/credentials-updated.json',
`--userId=${member.id}`,
]),
).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 * 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 });
async function importWorkflow(argv: string[]) {
const importer = new ImportWorkflowsCommand(argv, oclifConfig);
await importer.init();
await importer.run();
}
beforeAll(async () => {
mockInstance(InternalHooks);
mockInstance(LoadNodesAndCredentials);
@@ -17,7 +24,7 @@ beforeAll(async () => {
});
beforeEach(async () => {
await testDb.truncate(['Workflow']);
await testDb.truncate(['Workflow', 'SharedWorkflow', 'User']);
});
afterAll(async () => {
@@ -25,53 +32,186 @@ afterAll(async () => {
});
test('import:workflow should import active workflow and deactivate it', async () => {
const before = await getAllWorkflows();
expect(before.length).toBe(0);
const importer = new ImportWorkflowsCommand(
['--separate', '--input=./test/integration/commands/importWorkflows/separate'],
oclifConfig,
);
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit');
});
//
// ARRANGE
//
const owner = await createOwner();
await importer.init();
try {
await importer.run();
} catch (error) {
expect(error.message).toBe('process.exit');
}
const after = await getAllWorkflows();
expect(after.length).toBe(2);
expect(after[0].name).toBe('active-workflow');
expect(after[0].active).toBe(false);
expect(after[1].name).toBe('inactive-workflow');
expect(after[1].active).toBe(false);
mockExit.mockRestore();
//
// ACT
//
await importWorkflow([
'--separate',
'--input=./test/integration/commands/importWorkflows/separate',
]);
//
// 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 should import active workflow from combined file and deactivate it', async () => {
const before = await getAllWorkflows();
expect(before.length).toBe(0);
const importer = new ImportWorkflowsCommand(
['--input=./test/integration/commands/importWorkflows/combined/combined.json'],
oclifConfig,
);
const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit');
//
// ARRANGE
//
const owner = await createOwner();
//
// ACT
//
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 {
await importer.run();
} catch (error) {
expect(error.message).toBe('process.exit');
}
const after = await getAllWorkflows();
expect(after.length).toBe(2);
expect(after[0].name).toBe('active-workflow');
expect(after[0].active).toBe(false);
expect(after[1].name).toBe('inactive-workflow');
expect(after[1].active).toBe(false);
mockExit.mockRestore();
//
// ACT
//
// Import the same workflow again, with another name but the same ID, and try
// to assign it to the member.
await expect(
importWorkflow([
'--input=./test/integration/commands/importWorkflows/combined-with-update/updated.json',
`--userId=${member.id}`,
]),
).rejects.toThrowError(
`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}"`,
);
//
// 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": []
}
]