Skip to content

Commit

Permalink
fix(core): Stop relying on filesystem for SSH keys (#9217)
Browse files Browse the repository at this point in the history
  • Loading branch information
ivov committed Apr 25, 2024
1 parent d0250b2 commit 3418dfb
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 49 deletions.
Expand Up @@ -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');
Expand All @@ -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() {
Expand Down Expand Up @@ -273,13 +280,15 @@ export class SourceControlGitService {
if (!this.git) {
throw new ApplicationError('Git is not initialized (fetch)');
}
await this.setGitSshCommand();
return await this.git.fetch();
}

async pull(options: { ffOnly: boolean } = { ffOnly: true }): Promise<PullResult> {
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
Expand All @@ -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']);
}
Expand Down
@@ -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 {
Expand All @@ -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,
) {
Expand Down Expand Up @@ -82,33 +77,29 @@ 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);
}

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;
}

async getPrivateKeyPath() {
const dbPrivateKey = await this.getPrivateKeyFromDatabase();

if (dbPrivateKey) {
const tempFilePath = path.join(os.tmpdir(), 'ssh_private_key_temp');

await writeFile(tempFilePath, dbPrivateKey);
const tempFilePath = path.join(this.instanceSettings.n8nFolder, 'ssh_private_key_temp');

await chmod(tempFilePath, 0o600);
await writeFile(tempFilePath, dbPrivateKey);

return tempFilePath;
}
await chmod(tempFilePath, 0o600);

return this.sshKeyName; // fall back to key in filesystem
return tempFilePath;
}

async getPublicKey() {
Expand Down Expand Up @@ -136,33 +127,16 @@ 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 ??
(config.get('sourceControl.defaultKeyPairType') as KeyPairType) ??
'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({
Expand All @@ -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();
}

Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 3418dfb

Please sign in to comment.