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]); 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 privateKeyPath = await this.sourceControlPreferencesService.getPrivateKeyPath();
const sshKnownHosts = path.join(sshFolder, 'known_hosts'); const sshKnownHosts = path.join(sshFolder, 'known_hosts');
@ -94,21 +114,8 @@ export class SourceControlGitService {
const { simpleGit } = await import('simple-git'); const { simpleGit } = await import('simple-git');
this.git = simpleGit(this.gitOptions) 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_SSH_COMMAND', sshCommand)
.env('GIT_TERMINAL_PROMPT', '0'); .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() { resetService() {
@ -273,6 +280,7 @@ export class SourceControlGitService {
if (!this.git) { if (!this.git) {
throw new ApplicationError('Git is not initialized (fetch)'); throw new ApplicationError('Git is not initialized (fetch)');
} }
await this.setGitSshCommand();
return await this.git.fetch(); return await this.git.fetch();
} }
@ -280,6 +288,7 @@ export class SourceControlGitService {
if (!this.git) { if (!this.git) {
throw new ApplicationError('Git is not initialized (pull)'); throw new ApplicationError('Git is not initialized (pull)');
} }
await this.setGitSshCommand();
const params = {}; const params = {};
if (options.ffOnly) { if (options.ffOnly) {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
@ -298,6 +307,7 @@ export class SourceControlGitService {
if (!this.git) { if (!this.git) {
throw new ApplicationError('Git is not initialized ({)'); throw new ApplicationError('Git is not initialized ({)');
} }
await this.setGitSshCommand();
if (force) { if (force) {
return await this.git.push(SOURCE_CONTROL_ORIGIN, branch, ['-f']); 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 { writeFile, chmod, readFile } from 'node:fs/promises';
import Container, { Service } from 'typedi'; import Container, { Service } from 'typedi';
import { SourceControlPreferences } from './types/sourceControlPreferences'; import { SourceControlPreferences } from './types/sourceControlPreferences';
import type { ValidationError } from 'class-validator'; import type { ValidationError } from 'class-validator';
import { validate } from 'class-validator'; import { validate } from 'class-validator';
import { writeFile as fsWriteFile, rm as fsRm } from 'fs/promises'; import { rm as fsRm } from 'fs/promises';
import { import { generateSshKeyPair, isSourceControlLicensed } from './sourceControlHelper.ee';
generateSshKeyPair,
isSourceControlLicensed,
sourceControlFoldersExistCheck,
} from './sourceControlHelper.ee';
import { Cipher, InstanceSettings } from 'n8n-core'; import { Cipher, InstanceSettings } from 'n8n-core';
import { ApplicationError, jsonParse } from 'n8n-workflow'; import { ApplicationError, jsonParse } from 'n8n-workflow';
import { import {
@ -35,7 +30,7 @@ export class SourceControlPreferencesService {
readonly gitFolder: string; readonly gitFolder: string;
constructor( constructor(
instanceSettings: InstanceSettings, private readonly instanceSettings: InstanceSettings,
private readonly logger: Logger, private readonly logger: Logger,
private readonly cipher: Cipher, private readonly cipher: Cipher,
) { ) {
@ -82,7 +77,7 @@ export class SourceControlPreferencesService {
private async getPrivateKeyFromDatabase() { private async getPrivateKeyFromDatabase() {
const dbKeyPair = await this.getKeyPairFromDatabase(); 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); return this.cipher.decrypt(dbKeyPair.encryptedPrivateKey);
} }
@ -90,7 +85,7 @@ export class SourceControlPreferencesService {
private async getPublicKeyFromDatabase() { private async getPublicKeyFromDatabase() {
const dbKeyPair = await this.getKeyPairFromDatabase(); const dbKeyPair = await this.getKeyPairFromDatabase();
if (!dbKeyPair) return null; if (!dbKeyPair) throw new ApplicationError('Failed to find key pair in database');
return dbKeyPair.publicKey; return dbKeyPair.publicKey;
} }
@ -98,8 +93,7 @@ export class SourceControlPreferencesService {
async getPrivateKeyPath() { async getPrivateKeyPath() {
const dbPrivateKey = await this.getPrivateKeyFromDatabase(); const dbPrivateKey = await this.getPrivateKeyFromDatabase();
if (dbPrivateKey) { const tempFilePath = path.join(this.instanceSettings.n8nFolder, 'ssh_private_key_temp');
const tempFilePath = path.join(os.tmpdir(), 'ssh_private_key_temp');
await writeFile(tempFilePath, dbPrivateKey); await writeFile(tempFilePath, dbPrivateKey);
@ -108,9 +102,6 @@ export class SourceControlPreferencesService {
return tempFilePath; return tempFilePath;
} }
return this.sshKeyName; // fall back to key in filesystem
}
async getPublicKey() { async getPublicKey() {
try { try {
const dbPublicKey = await this.getPublicKeyFromDatabase(); const dbPublicKey = await this.getPublicKeyFromDatabase();
@ -136,11 +127,9 @@ export class SourceControlPreferencesService {
} }
/** /**
* Will generate an ed25519 key pair and save it to the database and the file system * Generate an SSH key pair and write it to the database, overwriting any existing key pair.
* Note: this will overwrite any existing key pair
*/ */
async generateAndSaveKeyPair(keyPairType?: KeyPairType): Promise<SourceControlPreferences> { async generateAndSaveKeyPair(keyPairType?: KeyPairType): Promise<SourceControlPreferences> {
sourceControlFoldersExistCheck([this.gitFolder, this.sshFolder]);
if (!keyPairType) { if (!keyPairType) {
keyPairType = keyPairType =
this.getPreferences().keyGeneratorType ?? this.getPreferences().keyGeneratorType ??
@ -148,21 +137,6 @@ export class SourceControlPreferencesService {
'ed25519'; 'ed25519';
} }
const keyPair = await generateSshKeyPair(keyPairType); 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 { try {
await Container.get(SettingsRepository).save({ await Container.get(SettingsRepository).save({
@ -177,6 +151,11 @@ export class SourceControlPreferencesService {
throw new ApplicationError('Failed to write key pair to database', { cause: error }); 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(); return this.getPreferences();
} }
@ -223,6 +202,10 @@ export class SourceControlPreferencesService {
preferences: Partial<SourceControlPreferences>, preferences: Partial<SourceControlPreferences>,
saveToDb = true, saveToDb = true,
): Promise<SourceControlPreferences> { ): Promise<SourceControlPreferences> {
const noKeyPair = (await this.getKeyPairFromDatabase()) === null;
if (noKeyPair) await this.generateAndSaveKeyPair();
this.sourceControlPreferences = preferences; this.sourceControlPreferences = preferences;
if (saveToDb) { if (saveToDb) {
const settingsValue = JSON.stringify(this._sourceControlPreferences); const settingsValue = JSON.stringify(this._sourceControlPreferences);