1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-09-17 16:08:12 +03:00

fix(core): Stop relying on filesystem for SSH keys (#9217)

This commit is contained in:
Iván Ovejero 2024-04-25 15:09:12 +02:00 committed by GitHub
parent 3986356c89
commit 093dcefafc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 42 additions and 49 deletions

View File

@ -79,6 +79,26 @@ export class SourceControlGitService {
sourceControlFoldersExistCheck([gitFolder, sshFolder]);
await this.setGitSshCommand(gitFolder, sshFolder);
if (!(await this.checkRepositorySetup())) {
await (this.git as unknown as SimpleGit).init();
}
if (!(await this.hasRemote(sourceControlPreferences.repositoryUrl))) {
if (sourceControlPreferences.connected && sourceControlPreferences.repositoryUrl) {
const instanceOwner = await this.ownershipService.getInstanceOwner();
await this.initRepository(sourceControlPreferences, instanceOwner);
}
}
}
/**
* Update the SSH command with the path to the temp file containing the private key from the DB.
*/
async setGitSshCommand(
gitFolder = this.sourceControlPreferencesService.gitFolder,
sshFolder = this.sourceControlPreferencesService.sshFolder,
) {
const privateKeyPath = await this.sourceControlPreferencesService.getPrivateKeyPath();
const sshKnownHosts = path.join(sshFolder, 'known_hosts');
@ -94,21 +114,8 @@ export class SourceControlGitService {
const { simpleGit } = await import('simple-git');
this.git = simpleGit(this.gitOptions)
// Tell git not to ask for any information via the terminal like for
// example the username. As nobody will be able to answer it would
// n8n keep on waiting forever.
.env('GIT_SSH_COMMAND', sshCommand)
.env('GIT_TERMINAL_PROMPT', '0');
if (!(await this.checkRepositorySetup())) {
await this.git.init();
}
if (!(await this.hasRemote(sourceControlPreferences.repositoryUrl))) {
if (sourceControlPreferences.connected && sourceControlPreferences.repositoryUrl) {
const instanceOwner = await this.ownershipService.getInstanceOwner();
await this.initRepository(sourceControlPreferences, instanceOwner);
}
}
}
resetService() {
@ -273,6 +280,7 @@ export class SourceControlGitService {
if (!this.git) {
throw new ApplicationError('Git is not initialized (fetch)');
}
await this.setGitSshCommand();
return await this.git.fetch();
}
@ -280,6 +288,7 @@ export class SourceControlGitService {
if (!this.git) {
throw new ApplicationError('Git is not initialized (pull)');
}
await this.setGitSshCommand();
const params = {};
if (options.ffOnly) {
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -298,6 +307,7 @@ export class SourceControlGitService {
if (!this.git) {
throw new ApplicationError('Git is not initialized ({)');
}
await this.setGitSshCommand();
if (force) {
return await this.git.push(SOURCE_CONTROL_ORIGIN, branch, ['-f']);
}

View File

@ -1,15 +1,10 @@
import os from 'node:os';
import { writeFile, chmod, readFile } from 'node:fs/promises';
import Container, { Service } from 'typedi';
import { SourceControlPreferences } from './types/sourceControlPreferences';
import type { ValidationError } from 'class-validator';
import { validate } from 'class-validator';
import { writeFile as fsWriteFile, rm as fsRm } from 'fs/promises';
import {
generateSshKeyPair,
isSourceControlLicensed,
sourceControlFoldersExistCheck,
} from './sourceControlHelper.ee';
import { rm as fsRm } from 'fs/promises';
import { generateSshKeyPair, isSourceControlLicensed } from './sourceControlHelper.ee';
import { Cipher, InstanceSettings } from 'n8n-core';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import {
@ -35,7 +30,7 @@ export class SourceControlPreferencesService {
readonly gitFolder: string;
constructor(
instanceSettings: InstanceSettings,
private readonly instanceSettings: InstanceSettings,
private readonly logger: Logger,
private readonly cipher: Cipher,
) {
@ -82,7 +77,7 @@ export class SourceControlPreferencesService {
private async getPrivateKeyFromDatabase() {
const dbKeyPair = await this.getKeyPairFromDatabase();
if (!dbKeyPair) return null;
if (!dbKeyPair) throw new ApplicationError('Failed to find key pair in database');
return this.cipher.decrypt(dbKeyPair.encryptedPrivateKey);
}
@ -90,7 +85,7 @@ export class SourceControlPreferencesService {
private async getPublicKeyFromDatabase() {
const dbKeyPair = await this.getKeyPairFromDatabase();
if (!dbKeyPair) return null;
if (!dbKeyPair) throw new ApplicationError('Failed to find key pair in database');
return dbKeyPair.publicKey;
}
@ -98,17 +93,13 @@ export class SourceControlPreferencesService {
async getPrivateKeyPath() {
const dbPrivateKey = await this.getPrivateKeyFromDatabase();
if (dbPrivateKey) {
const tempFilePath = path.join(os.tmpdir(), 'ssh_private_key_temp');
const tempFilePath = path.join(this.instanceSettings.n8nFolder, 'ssh_private_key_temp');
await writeFile(tempFilePath, dbPrivateKey);
await writeFile(tempFilePath, dbPrivateKey);
await chmod(tempFilePath, 0o600);
await chmod(tempFilePath, 0o600);
return tempFilePath;
}
return this.sshKeyName; // fall back to key in filesystem
return tempFilePath;
}
async getPublicKey() {
@ -136,11 +127,9 @@ export class SourceControlPreferencesService {
}
/**
* Will generate an ed25519 key pair and save it to the database and the file system
* Note: this will overwrite any existing key pair
* Generate an SSH key pair and write it to the database, overwriting any existing key pair.
*/
async generateAndSaveKeyPair(keyPairType?: KeyPairType): Promise<SourceControlPreferences> {
sourceControlFoldersExistCheck([this.gitFolder, this.sshFolder]);
if (!keyPairType) {
keyPairType =
this.getPreferences().keyGeneratorType ??
@ -148,21 +137,6 @@ export class SourceControlPreferencesService {
'ed25519';
}
const keyPair = await generateSshKeyPair(keyPairType);
if (keyPair.publicKey && keyPair.privateKey) {
try {
await fsWriteFile(this.sshKeyName + '.pub', keyPair.publicKey, {
encoding: 'utf8',
mode: 0o666,
});
await fsWriteFile(this.sshKeyName, keyPair.privateKey, { encoding: 'utf8', mode: 0o600 });
} catch (error) {
throw new ApplicationError('Failed to save key pair to disk', { cause: error });
}
}
// update preferences only after generating key pair to prevent endless loop
if (keyPairType !== this.getPreferences().keyGeneratorType) {
await this.setPreferences({ keyGeneratorType: keyPairType });
}
try {
await Container.get(SettingsRepository).save({
@ -177,6 +151,11 @@ export class SourceControlPreferencesService {
throw new ApplicationError('Failed to write key pair to database', { cause: error });
}
// update preferences only after generating key pair to prevent endless loop
if (keyPairType !== this.getPreferences().keyGeneratorType) {
await this.setPreferences({ keyGeneratorType: keyPairType });
}
return this.getPreferences();
}
@ -223,6 +202,10 @@ export class SourceControlPreferencesService {
preferences: Partial<SourceControlPreferences>,
saveToDb = true,
): Promise<SourceControlPreferences> {
const noKeyPair = (await this.getKeyPairFromDatabase()) === null;
if (noKeyPair) await this.generateAndSaveKeyPair();
this.sourceControlPreferences = preferences;
if (saveToDb) {
const settingsValue = JSON.stringify(this._sourceControlPreferences);