fix(core): Don't create multiple owners when importing credentials or workflows (#9112)
This commit is contained in:
@@ -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();
|
||||
|
||||
if (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) => {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
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.',
|
||||
);
|
||||
}
|
||||
const credentials = await this.readCredentials(flags.input, flags.separate);
|
||||
|
||||
await Db.getConnection().transaction(async (transactionManager) => {
|
||||
this.transactionManager = transactionManager;
|
||||
|
||||
const result = await this.checkRelations(credentials, flags.userId);
|
||||
|
||||
if (!result.success) {
|
||||
throw new ApplicationError(result.message);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
this.reportSuccess(totalImported);
|
||||
this.reportSuccess(credentials.length);
|
||||
}
|
||||
|
||||
async catch(error: Error) {
|
||||
@@ -142,15 +100,23 @@ export class ImportCredentialsCommand extends BaseCommand {
|
||||
|
||||
private async storeCredential(credential: Partial<CredentialsEntity>, user: User) {
|
||||
const result = await this.transactionManager.upsert(CredentialsEntity, credential, ['id']);
|
||||
await this.transactionManager.upsert(
|
||||
SharedCredentials,
|
||||
{
|
||||
credentialsId: result.identifiers[0].id as string,
|
||||
userId: user.id,
|
||||
role: 'credential:owner',
|
||||
},
|
||||
['credentialsId', 'userId'],
|
||||
);
|
||||
|
||||
const sharingExists = await this.transactionManager.existsBy(SharedCredentials, {
|
||||
credentialsId: credential.id,
|
||||
role: 'credential:owner',
|
||||
});
|
||||
|
||||
if (!sharingExists) {
|
||||
await this.transactionManager.upsert(
|
||||
SharedCredentials,
|
||||
{
|
||||
credentialsId: result.identifiers[0].id as string,
|
||||
userId: user.id,
|
||||
role: 'credential:owner',
|
||||
},
|
||||
['credentialsId', 'userId'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async getOwner() {
|
||||
@@ -162,6 +128,84 @@ export class ImportCredentialsCommand extends BaseCommand {
|
||||
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) {
|
||||
const user = await Container.get(UserRepository).findOneBy({ id: userId });
|
||||
|
||||
@@ -171,4 +215,17 @@ export class ImportCredentialsCommand extends BaseCommand {
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { WorkflowRepository } from '@db/repositories/workflow.repository';
|
||||
import type { IWorkflowToImport } from '@/Interfaces';
|
||||
import { ImportService } from '@/services/import.service';
|
||||
import { BaseCommand } from '../BaseCommand';
|
||||
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
|
||||
|
||||
function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] {
|
||||
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) {
|
||||
let { input: inputPath } = flags;
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
inputPath = inputPath.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
const files = await glob('*.json', {
|
||||
cwd: inputPath,
|
||||
absolute: true,
|
||||
});
|
||||
|
||||
totalImported = files.length;
|
||||
this.logger.info(`Importing ${totalImported} workflows...`);
|
||||
|
||||
for (const file of files) {
|
||||
const workflow = jsonParse<IWorkflowToImport>(fs.readFileSync(file, { encoding: 'utf8' }));
|
||||
if (!workflow.id) {
|
||||
workflow.id = generateNanoId();
|
||||
}
|
||||
|
||||
const _workflow = Container.get(WorkflowRepository).create(workflow);
|
||||
|
||||
await Container.get(ImportService).importWorkflows([_workflow], user.id);
|
||||
}
|
||||
|
||||
this.reportSuccess(totalImported);
|
||||
process.exit();
|
||||
const result = await this.checkRelations(workflows, flags.userId);
|
||||
if (!result.success) {
|
||||
throw new ApplicationError(result.message);
|
||||
}
|
||||
|
||||
const workflows = jsonParse<IWorkflowToImport[]>(
|
||||
fs.readFileSync(flags.input, { encoding: 'utf8' }),
|
||||
);
|
||||
this.logger.info(`Importing ${workflows.length} workflows...`);
|
||||
|
||||
const _workflows = workflows.map((w) => Container.get(WorkflowRepository).create(w));
|
||||
await Container.get(ImportService).importWorkflows(workflows, flags.userId ?? owner.id);
|
||||
|
||||
assertHasWorkflowsToImport(workflows);
|
||||
this.reportSuccess(workflows.length);
|
||||
}
|
||||
|
||||
totalImported = workflows.length;
|
||||
private async checkRelations(workflows: WorkflowEntity[], userId: string | undefined) {
|
||||
if (!userId) {
|
||||
return {
|
||||
success: true as const,
|
||||
message: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
await Container.get(ImportService).importWorkflows(_workflows, user.id);
|
||||
for (const workflow of workflows) {
|
||||
if (!(await this.workflowExists(workflow))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.reportSuccess(totalImported);
|
||||
const ownerId = await this.getWorkflowOwner(workflow);
|
||||
if (!ownerId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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}"`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true as const,
|
||||
message: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async catch(error: Error) {
|
||||
@@ -145,13 +145,48 @@ export class ImportWorkflowsCommand extends BaseCommand {
|
||||
return owner;
|
||||
}
|
||||
|
||||
private async getAssignee(userId: string) {
|
||||
const user = await Container.get(UserRepository).findOneBy({ id: userId });
|
||||
private async getWorkflowOwner(workflow: WorkflowEntity) {
|
||||
const sharing = await Container.get(SharedWorkflowRepository).findOneBy({
|
||||
workflowId: workflow.id,
|
||||
role: 'workflow:owner',
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new ApplicationError('Failed to find user', { extra: { userId } });
|
||||
return sharing?.userId;
|
||||
}
|
||||
|
||||
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, '/');
|
||||
}
|
||||
|
||||
return user;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,14 +53,18 @@ export class ImportService {
|
||||
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;
|
||||
|
||||
await tx.upsert(SharedWorkflow, { workflowId, userId, role: 'workflow:owner' }, [
|
||||
'workflowId',
|
||||
'userId',
|
||||
]);
|
||||
// Create relationship if the workflow was inserted instead of updated.
|
||||
if (!exists) {
|
||||
await tx.upsert(SharedWorkflow, { workflowId, userId, role: 'workflow:owner' }, [
|
||||
'workflowId',
|
||||
'userId',
|
||||
]);
|
||||
}
|
||||
|
||||
if (!workflow.tags?.length) continue;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user