Skip to content

Commit a5d47db

Browse files
despairbluenetroy
authored andcommitted
fix(core): Don't create multiple owners when importing credentials or workflows (#9112)
1 parent ae634bd commit a5d47db

File tree

12 files changed

+820
-179
lines changed

12 files changed

+820
-179
lines changed

packages/cli/src/commands/import/credentials.ts

Lines changed: 116 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -64,67 +64,25 @@ export class ImportCredentialsCommand extends BaseCommand {
6464
}
6565
}
6666

67-
let totalImported = 0;
68-
69-
const cipher = Container.get(Cipher);
7067
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner();
7168

72-
if (flags.separate) {
73-
let { input: inputPath } = flags;
74-
75-
if (process.platform === 'win32') {
76-
inputPath = inputPath.replace(/\\/g, '/');
77-
}
78-
79-
const files = await glob('*.json', {
80-
cwd: inputPath,
81-
absolute: true,
82-
});
83-
84-
totalImported = files.length;
85-
86-
await Db.getConnection().transaction(async (transactionManager) => {
87-
this.transactionManager = transactionManager;
88-
for (const file of files) {
89-
const credential = jsonParse<ICredentialsEncrypted>(
90-
fs.readFileSync(file, { encoding: 'utf8' }),
91-
);
92-
if (typeof credential.data === 'object') {
93-
// plain data / decrypted input. Should be encrypted first.
94-
credential.data = cipher.encrypt(credential.data);
95-
}
96-
await this.storeCredential(credential, user);
97-
}
98-
});
69+
const credentials = await this.readCredentials(flags.input, flags.separate);
9970

100-
this.reportSuccess(totalImported);
101-
return;
102-
}
103-
104-
const credentials = jsonParse<ICredentialsEncrypted[]>(
105-
fs.readFileSync(flags.input, { encoding: 'utf8' }),
106-
);
71+
await Db.getConnection().transaction(async (transactionManager) => {
72+
this.transactionManager = transactionManager;
10773

108-
totalImported = credentials.length;
74+
const result = await this.checkRelations(credentials, flags.userId);
10975

110-
if (!Array.isArray(credentials)) {
111-
throw new ApplicationError(
112-
'File does not seem to contain credentials. Make sure the credentials are contained in an array.',
113-
);
114-
}
76+
if (!result.success) {
77+
throw new ApplicationError(result.message);
78+
}
11579

116-
await Db.getConnection().transaction(async (transactionManager) => {
117-
this.transactionManager = transactionManager;
11880
for (const credential of credentials) {
119-
if (typeof credential.data === 'object') {
120-
// plain data / decrypted input. Should be encrypted first.
121-
credential.data = cipher.encrypt(credential.data);
122-
}
12381
await this.storeCredential(credential, user);
12482
}
12583
});
12684

127-
this.reportSuccess(totalImported);
85+
this.reportSuccess(credentials.length);
12886
}
12987

13088
async catch(error: Error) {
@@ -145,15 +103,23 @@ export class ImportCredentialsCommand extends BaseCommand {
145103
credential.nodesAccess = [];
146104
}
147105
const result = await this.transactionManager.upsert(CredentialsEntity, credential, ['id']);
148-
await this.transactionManager.upsert(
149-
SharedCredentials,
150-
{
151-
credentialsId: result.identifiers[0].id as string,
152-
userId: user.id,
153-
role: 'credential:owner',
154-
},
155-
['credentialsId', 'userId'],
156-
);
106+
107+
const sharingExists = await this.transactionManager.existsBy(SharedCredentials, {
108+
credentialsId: credential.id,
109+
role: 'credential:owner',
110+
});
111+
112+
if (!sharingExists) {
113+
await this.transactionManager.upsert(
114+
SharedCredentials,
115+
{
116+
credentialsId: result.identifiers[0].id as string,
117+
userId: user.id,
118+
role: 'credential:owner',
119+
},
120+
['credentialsId', 'userId'],
121+
);
122+
}
157123
}
158124

159125
private async getOwner() {
@@ -165,6 +131,84 @@ export class ImportCredentialsCommand extends BaseCommand {
165131
return owner;
166132
}
167133

134+
private async checkRelations(credentials: ICredentialsEncrypted[], userId?: string) {
135+
if (!userId) {
136+
return {
137+
success: true as const,
138+
message: undefined,
139+
};
140+
}
141+
142+
for (const credential of credentials) {
143+
if (credential.id === undefined) {
144+
continue;
145+
}
146+
147+
if (!(await this.credentialExists(credential.id))) {
148+
continue;
149+
}
150+
151+
const ownerId = await this.getCredentialOwner(credential.id);
152+
if (!ownerId) {
153+
continue;
154+
}
155+
156+
if (ownerId !== userId) {
157+
return {
158+
success: false as const,
159+
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}"`,
160+
};
161+
}
162+
}
163+
164+
return {
165+
success: true as const,
166+
message: undefined,
167+
};
168+
}
169+
170+
private async readCredentials(path: string, separate: boolean): Promise<ICredentialsEncrypted[]> {
171+
const cipher = Container.get(Cipher);
172+
173+
if (process.platform === 'win32') {
174+
path = path.replace(/\\/g, '/');
175+
}
176+
177+
let credentials: ICredentialsEncrypted[];
178+
179+
if (separate) {
180+
const files = await glob('*.json', {
181+
cwd: path,
182+
absolute: true,
183+
});
184+
185+
credentials = files.map((file) =>
186+
jsonParse<ICredentialsEncrypted>(fs.readFileSync(file, { encoding: 'utf8' })),
187+
);
188+
} else {
189+
const credentialsUnchecked = jsonParse<ICredentialsEncrypted[]>(
190+
fs.readFileSync(path, { encoding: 'utf8' }),
191+
);
192+
193+
if (!Array.isArray(credentialsUnchecked)) {
194+
throw new ApplicationError(
195+
'File does not seem to contain credentials. Make sure the credentials are contained in an array.',
196+
);
197+
}
198+
199+
credentials = credentialsUnchecked;
200+
}
201+
202+
return credentials.map((credential) => {
203+
if (typeof credential.data === 'object') {
204+
// plain data / decrypted input. Should be encrypted first.
205+
credential.data = cipher.encrypt(credential.data);
206+
}
207+
208+
return credential;
209+
});
210+
}
211+
168212
private async getAssignee(userId: string) {
169213
const user = await Container.get(UserRepository).findOneBy({ id: userId });
170214

@@ -174,4 +218,17 @@ export class ImportCredentialsCommand extends BaseCommand {
174218

175219
return user;
176220
}
221+
222+
private async getCredentialOwner(credentialsId: string) {
223+
const sharedCredential = await this.transactionManager.findOneBy(SharedCredentials, {
224+
credentialsId,
225+
role: 'credential:owner',
226+
});
227+
228+
return sharedCredential?.userId;
229+
}
230+
231+
private async credentialExists(credentialId: string) {
232+
return await this.transactionManager.existsBy(CredentialsEntity, { id: credentialId });
233+
}
177234
}

packages/cli/src/commands/import/workflow.ts

Lines changed: 75 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { WorkflowRepository } from '@db/repositories/workflow.repository';
1313
import type { IWorkflowToImport } from '@/Interfaces';
1414
import { ImportService } from '@/services/import.service';
1515
import { BaseCommand } from '../BaseCommand';
16+
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
1617

1718
function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] {
1819
if (!Array.isArray(workflows)) {
@@ -78,53 +79,52 @@ export class ImportWorkflowsCommand extends BaseCommand {
7879
}
7980
}
8081

81-
const user = flags.userId ? await this.getAssignee(flags.userId) : await this.getOwner();
82+
const owner = await this.getOwner();
8283

83-
let totalImported = 0;
84+
const workflows = await this.readWorkflows(flags.input, flags.separate);
8485

85-
if (flags.separate) {
86-
let { input: inputPath } = flags;
86+
const result = await this.checkRelations(workflows, flags.userId);
87+
if (!result.success) {
88+
throw new ApplicationError(result.message);
89+
}
8790

88-
if (process.platform === 'win32') {
89-
inputPath = inputPath.replace(/\\/g, '/');
90-
}
91+
this.logger.info(`Importing ${workflows.length} workflows...`);
9192

92-
const files = await glob('*.json', {
93-
cwd: inputPath,
94-
absolute: true,
95-
});
93+
await Container.get(ImportService).importWorkflows(workflows, flags.userId ?? owner.id);
9694

97-
totalImported = files.length;
98-
this.logger.info(`Importing ${totalImported} workflows...`);
95+
this.reportSuccess(workflows.length);
96+
}
9997

100-
for (const file of files) {
101-
const workflow = jsonParse<IWorkflowToImport>(fs.readFileSync(file, { encoding: 'utf8' }));
102-
if (!workflow.id) {
103-
workflow.id = generateNanoId();
104-
}
98+
private async checkRelations(workflows: WorkflowEntity[], userId: string | undefined) {
99+
if (!userId) {
100+
return {
101+
success: true as const,
102+
message: undefined,
103+
};
104+
}
105105

106-
const _workflow = Container.get(WorkflowRepository).create(workflow);
106+
for (const workflow of workflows) {
107+
if (!(await this.workflowExists(workflow))) {
108+
continue;
109+
}
107110

108-
await Container.get(ImportService).importWorkflows([_workflow], user.id);
111+
const ownerId = await this.getWorkflowOwner(workflow);
112+
if (!ownerId) {
113+
continue;
109114
}
110115

111-
this.reportSuccess(totalImported);
112-
process.exit();
116+
if (ownerId !== userId) {
117+
return {
118+
success: false as const,
119+
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}"`,
120+
};
121+
}
113122
}
114123

115-
const workflows = jsonParse<IWorkflowToImport[]>(
116-
fs.readFileSync(flags.input, { encoding: 'utf8' }),
117-
);
118-
119-
const _workflows = workflows.map((w) => Container.get(WorkflowRepository).create(w));
120-
121-
assertHasWorkflowsToImport(workflows);
122-
123-
totalImported = workflows.length;
124-
125-
await Container.get(ImportService).importWorkflows(_workflows, user.id);
126-
127-
this.reportSuccess(totalImported);
124+
return {
125+
success: true as const,
126+
message: undefined,
127+
};
128128
}
129129

130130
async catch(error: Error) {
@@ -145,13 +145,48 @@ export class ImportWorkflowsCommand extends BaseCommand {
145145
return owner;
146146
}
147147

148-
private async getAssignee(userId: string) {
149-
const user = await Container.get(UserRepository).findOneBy({ id: userId });
148+
private async getWorkflowOwner(workflow: WorkflowEntity) {
149+
const sharing = await Container.get(SharedWorkflowRepository).findOneBy({
150+
workflowId: workflow.id,
151+
role: 'workflow:owner',
152+
});
153+
154+
return sharing?.userId;
155+
}
156+
157+
private async workflowExists(workflow: WorkflowEntity) {
158+
return await Container.get(WorkflowRepository).existsBy({ id: workflow.id });
159+
}
150160

151-
if (!user) {
152-
throw new ApplicationError('Failed to find user', { extra: { userId } });
161+
private async readWorkflows(path: string, separate: boolean): Promise<WorkflowEntity[]> {
162+
if (process.platform === 'win32') {
163+
path = path.replace(/\\/g, '/');
153164
}
154165

155-
return user;
166+
if (separate) {
167+
const files = await glob('*.json', {
168+
cwd: path,
169+
absolute: true,
170+
});
171+
const workflowInstances = files.map((file) => {
172+
const workflow = jsonParse<IWorkflowToImport>(fs.readFileSync(file, { encoding: 'utf8' }));
173+
if (!workflow.id) {
174+
workflow.id = generateNanoId();
175+
}
176+
177+
const workflowInstance = Container.get(WorkflowRepository).create(workflow);
178+
179+
return workflowInstance;
180+
});
181+
182+
return workflowInstances;
183+
} else {
184+
const workflows = jsonParse<IWorkflowToImport[]>(fs.readFileSync(path, { encoding: 'utf8' }));
185+
186+
const workflowInstances = workflows.map((w) => Container.get(WorkflowRepository).create(w));
187+
assertHasWorkflowsToImport(workflows);
188+
189+
return workflowInstances;
190+
}
156191
}
157192
}

packages/cli/src/services/import.service.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,18 @@ export class ImportService {
5353
this.logger.info(`Deactivating workflow "${workflow.name}". Remember to activate later.`);
5454
}
5555

56-
const upsertResult = await tx.upsert(WorkflowEntity, workflow, ['id']);
56+
const exists = workflow.id ? await tx.existsBy(WorkflowEntity, { id: workflow.id }) : false;
5757

58+
const upsertResult = await tx.upsert(WorkflowEntity, workflow, ['id']);
5859
const workflowId = upsertResult.identifiers.at(0)?.id as string;
5960

60-
await tx.upsert(SharedWorkflow, { workflowId, userId, role: 'workflow:owner' }, [
61-
'workflowId',
62-
'userId',
63-
]);
61+
// Create relationship if the workflow was inserted instead of updated.
62+
if (!exists) {
63+
await tx.upsert(SharedWorkflow, { workflowId, userId, role: 'workflow:owner' }, [
64+
'workflowId',
65+
'userId',
66+
]);
67+
}
6468

6569
if (!workflow.tags?.length) continue;
6670

0 commit comments

Comments
 (0)