Skip to content

Commit 3eb5be5

Browse files
authored
fix(core): Don't create multiple owners when importing credentials or workflows (#9112)
1 parent 5433004 commit 3eb5be5

File tree

12 files changed

+820
-178
lines changed

12 files changed

+820
-178
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) {
@@ -142,15 +100,23 @@ export class ImportCredentialsCommand extends BaseCommand {
142100

143101
private async storeCredential(credential: Partial<CredentialsEntity>, user: User) {
144102
const result = await this.transactionManager.upsert(CredentialsEntity, credential, ['id']);
145-
await this.transactionManager.upsert(
146-
SharedCredentials,
147-
{
148-
credentialsId: result.identifiers[0].id as string,
149-
userId: user.id,
150-
role: 'credential:owner',
151-
},
152-
['credentialsId', 'userId'],
153-
);
103+
104+
const sharingExists = await this.transactionManager.existsBy(SharedCredentials, {
105+
credentialsId: credential.id,
106+
role: 'credential:owner',
107+
});
108+
109+
if (!sharingExists) {
110+
await this.transactionManager.upsert(
111+
SharedCredentials,
112+
{
113+
credentialsId: result.identifiers[0].id as string,
114+
userId: user.id,
115+
role: 'credential:owner',
116+
},
117+
['credentialsId', 'userId'],
118+
);
119+
}
154120
}
155121

156122
private async getOwner() {
@@ -162,6 +128,84 @@ export class ImportCredentialsCommand extends BaseCommand {
162128
return owner;
163129
}
164130

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

@@ -171,4 +215,17 @@ export class ImportCredentialsCommand extends BaseCommand {
171215

172216
return user;
173217
}
218+
219+
private async getCredentialOwner(credentialsId: string) {
220+
const sharedCredential = await this.transactionManager.findOneBy(SharedCredentials, {
221+
credentialsId,
222+
role: 'credential:owner',
223+
});
224+
225+
return sharedCredential?.userId;
226+
}
227+
228+
private async credentialExists(credentialId: string) {
229+
return await this.transactionManager.existsBy(CredentialsEntity, { id: credentialId });
230+
}
174231
}

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)