feat(core): Add SSH key generation (#6006)

* basic prefs and ssh key generation

* review change

* cleanup save

* lint fix
This commit is contained in:
Michael Auerswald
2023-04-19 17:46:10 +02:00
committed by GitHub
parent 953198e092
commit 71ed1f410c
18 changed files with 313 additions and 19 deletions

View File

@@ -0,0 +1 @@
export const VERSION_CONTROL_PREFERENCES_DB_KEY = 'features.versionControl';

View File

@@ -0,0 +1,34 @@
import type { RequestHandler } from 'express';
import type { AuthenticatedRequest } from '@/requests';
import {
isVersionControlLicensed,
isVersionControlLicensedAndEnabled,
} from '../versionControlHelper';
export const versionControlLicensedOwnerMiddleware: RequestHandler = (
req: AuthenticatedRequest,
res,
next,
) => {
if (isVersionControlLicensed() && req.user?.globalRole.name === 'owner') {
next();
} else {
res.status(401).json({ status: 'error', message: 'Unauthorized' });
}
};
export const versionControlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => {
if (isVersionControlLicensedAndEnabled()) {
next();
} else {
res.status(401).json({ status: 'error', message: 'Unauthorized' });
}
};
export const versionControlLicensedMiddleware: RequestHandler = (req, res, next) => {
if (isVersionControlLicensed()) {
next();
} else {
res.status(401).json({ status: 'error', message: 'Unauthorized' });
}
};

View File

@@ -0,0 +1,4 @@
export interface KeyPair {
privateKey: string;
publicKey: string;
}

View File

@@ -0,0 +1,9 @@
import { IsString } from 'class-validator';
export class VersionControlPreferences {
@IsString()
privateKey: string;
@IsString()
publicKey: string;
}

View File

@@ -0,0 +1,22 @@
import { Get, RestController } from '../../decorators';
import {
versionControlLicensedMiddleware,
versionControlLicensedOwnerMiddleware,
} from './middleware/versionControlEnabledMiddleware';
import { VersionControlService } from './versionControl.service.ee';
@RestController('/versionControl')
export class VersionControlController {
constructor(private versionControlService: VersionControlService) {}
@Get('/preferences', { middlewares: [versionControlLicensedMiddleware] })
async getPreferences() {
return this.versionControlService.versionControlPreferences;
}
//TODO: temporary function to generate key and save new pair
@Get('/generateKeyPair', { middlewares: [versionControlLicensedOwnerMiddleware] })
async generateKeyPair() {
return this.versionControlService.generateAndSaveKeyPair();
}
}

View File

@@ -0,0 +1,70 @@
import { Service } from 'typedi';
import { generateSshKeyPair } from './versionControlHelper';
import { VersionControlPreferences } from './types/versionControlPreferences';
import { VERSION_CONTROL_PREFERENCES_DB_KEY } from './constants';
import * as Db from '@/Db';
import { jsonParse } from 'n8n-workflow';
@Service()
export class VersionControlService {
private _versionControlPreferences: VersionControlPreferences = new VersionControlPreferences();
async init(): Promise<void> {
await this.loadFromDbAndApplyVersionControlPreferences();
}
public get versionControlPreferences(): VersionControlPreferences {
return {
...this._versionControlPreferences,
privateKey: '',
};
}
async generateAndSaveKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') {
const keyPair = generateSshKeyPair(keyType);
if (keyPair.publicKey && keyPair.privateKey) {
this.setPreferences({ ...keyPair });
await this.saveSamlPreferencesToDb();
}
return keyPair;
}
setPreferences(prefs: Partial<VersionControlPreferences>) {
this._versionControlPreferences = {
...this._versionControlPreferences,
...prefs,
};
}
async loadFromDbAndApplyVersionControlPreferences(): Promise<
VersionControlPreferences | undefined
> {
const loadedPrefs = await Db.collections.Settings.findOne({
where: { key: VERSION_CONTROL_PREFERENCES_DB_KEY },
});
if (loadedPrefs) {
try {
const prefs = jsonParse<VersionControlPreferences>(loadedPrefs.value);
if (prefs) {
this.setPreferences(prefs);
return prefs;
}
} catch {}
}
return;
}
async saveSamlPreferencesToDb(): Promise<VersionControlPreferences | undefined> {
const settingsValue = JSON.stringify(this._versionControlPreferences);
const result = await Db.collections.Settings.save({
key: VERSION_CONTROL_PREFERENCES_DB_KEY,
value: settingsValue,
loadOnStartup: true,
});
if (result)
try {
return jsonParse<VersionControlPreferences>(result.value);
} catch {}
return;
}
}

View File

@@ -0,0 +1,53 @@
import Container from 'typedi';
import { License } from '../../License';
import { generateKeyPairSync } from 'crypto';
import sshpk from 'sshpk';
import type { KeyPair } from './types/keyPair';
export function isVersionControlLicensed() {
const license = Container.get(License);
return license.isVersionControlLicensed();
}
export function isVersionControlEnabled() {
// TODO: VERSION CONTROL check if enabled
return true;
}
export function isVersionControlLicensedAndEnabled() {
return isVersionControlLicensed() && isVersionControlEnabled();
}
export function generateSshKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') {
const keyPair: KeyPair = {
publicKey: '',
privateKey: '',
};
let generatedKeyPair: KeyPair;
switch (keyType) {
case 'ed25519':
generatedKeyPair = generateKeyPairSync('ed25519', {
privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
publicKeyEncoding: { format: 'pem', type: 'spki' },
});
break;
case 'rsa':
generatedKeyPair = generateKeyPairSync('rsa', {
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
},
});
break;
}
const keyPublic = sshpk.parseKey(generatedKeyPair.publicKey, 'pem');
keyPair.publicKey = keyPublic.toString('ssh');
const keyPrivate = sshpk.parsePrivateKey(generatedKeyPair.privateKey, 'pem');
keyPair.privateKey = keyPrivate.toString('ssh-private');
return keyPair;
}