From f3b470186360dc3c3a3df599f0a9740183e86696 Mon Sep 17 00:00:00 2001 From: Michael Auerswald Date: Mon, 24 Apr 2023 17:13:25 +0200 Subject: [PATCH] feat(core): Manage version control settings (#6079) * expand VersionControlPreferences * use Authorized decorator for vc endpoints instead of middleware * validate preferences with class-validator * cleanup * cleanup --- .../versionControlEnabledMiddleware.ts | 13 --- .../versionControl/types/requests.ts | 6 ++ .../types/versionControlPreferences.ts | 35 +++++- .../versionControl.controller.ee.ts | 29 +++-- .../versionControl.service.ee.ts | 102 ++++++++++++------ .../versionControl/versionControlHelper.ts | 5 +- 6 files changed, 133 insertions(+), 57 deletions(-) create mode 100644 packages/cli/src/environments/versionControl/types/requests.ts diff --git a/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts b/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts index 55a5e2122e..5ed3a1293b 100644 --- a/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts +++ b/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts @@ -1,22 +1,9 @@ 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(); diff --git a/packages/cli/src/environments/versionControl/types/requests.ts b/packages/cli/src/environments/versionControl/types/requests.ts new file mode 100644 index 0000000000..0782873ba5 --- /dev/null +++ b/packages/cli/src/environments/versionControl/types/requests.ts @@ -0,0 +1,6 @@ +import type { AuthenticatedRequest } from '@/requests'; +import type { VersionControlPreferences } from './versionControlPreferences'; + +export declare namespace VersionControlRequest { + type UpdatePreferences = AuthenticatedRequest<{}, {}, Partial, {}>; +} diff --git a/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts b/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts index ff2becc5d4..9481270ec5 100644 --- a/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts +++ b/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts @@ -1,9 +1,36 @@ -import { IsString } from 'class-validator'; +import { IsBoolean, IsEmail, IsHexColor, IsOptional, IsString } from 'class-validator'; export class VersionControlPreferences { - @IsString() - privateKey: string; + constructor(preferences: Partial | undefined = undefined) { + if (preferences) Object.assign(this, preferences); + } + + @IsBoolean() + connected: boolean; @IsString() - publicKey: string; + repositoryUrl: string; + + @IsString() + authorName: string; + + @IsEmail() + authorEmail: string; + + @IsString() + branchName: string; + + @IsBoolean() + branchReadOnly: boolean; + + @IsHexColor() + branchColor: string; + + @IsOptional() + @IsString() + readonly privateKey?: string; + + @IsOptional() + @IsString() + readonly publicKey?: string; } diff --git a/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts b/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts index fd3d38a6ea..5bbac46f0b 100644 --- a/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts +++ b/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts @@ -1,21 +1,36 @@ -import { Get, RestController } from '../../decorators'; -import { - versionControlLicensedMiddleware, - versionControlLicensedOwnerMiddleware, -} from './middleware/versionControlEnabledMiddleware'; +import { Authorized, Get, Post, RestController } from '@/decorators'; +import { versionControlLicensedMiddleware } from './middleware/versionControlEnabledMiddleware'; import { VersionControlService } from './versionControl.service.ee'; +import { VersionControlRequest } from './types/requests'; +import type { VersionControlPreferences } from './types/versionControlPreferences'; @RestController('/versionControl') export class VersionControlController { constructor(private versionControlService: VersionControlService) {} + @Authorized('any') @Get('/preferences', { middlewares: [versionControlLicensedMiddleware] }) - async getPreferences() { + async getPreferences(): Promise { + // returns the settings with the privateKey property redacted return this.versionControlService.versionControlPreferences; } + @Authorized(['global', 'owner']) + @Post('/preferences', { middlewares: [versionControlLicensedMiddleware] }) + async setPreferences(req: VersionControlRequest.UpdatePreferences) { + const sanitizedPreferences: Partial = { + ...req.body, + privateKey: undefined, + publicKey: undefined, + }; + await this.versionControlService.validateVersionControlPreferences(sanitizedPreferences); + return this.versionControlService.setPreferences(sanitizedPreferences); + } + //TODO: temporary function to generate key and save new pair - @Get('/generateKeyPair', { middlewares: [versionControlLicensedOwnerMiddleware] }) + // REMOVE THIS FUNCTION AFTER TESTING + @Authorized(['global', 'owner']) + @Get('/generateKeyPair', { middlewares: [versionControlLicensedMiddleware] }) async generateKeyPair() { return this.versionControlService.generateAndSaveKeyPair(); } diff --git a/packages/cli/src/environments/versionControl/versionControl.service.ee.ts b/packages/cli/src/environments/versionControl/versionControl.service.ee.ts index 3096b5531d..d531935f00 100644 --- a/packages/cli/src/environments/versionControl/versionControl.service.ee.ts +++ b/packages/cli/src/environments/versionControl/versionControl.service.ee.ts @@ -3,7 +3,9 @@ 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'; +import { jsonParse, LoggerProxy } from 'n8n-workflow'; +import type { ValidationError } from 'class-validator'; +import { validate } from 'class-validator'; @Service() export class VersionControlService { @@ -16,55 +18,91 @@ export class VersionControlService { public get versionControlPreferences(): VersionControlPreferences { return { ...this._versionControlPreferences, - privateKey: '', + privateKey: '(redacted)', }; } - async generateAndSaveKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') { - const keyPair = generateSshKeyPair(keyType); + public set versionControlPreferences(preferences: Partial) { + this._versionControlPreferences = { + connected: preferences.connected ?? this._versionControlPreferences.connected, + authorEmail: preferences.authorEmail ?? this._versionControlPreferences.authorEmail, + authorName: preferences.authorName ?? this._versionControlPreferences.authorName, + branchName: preferences.branchName ?? this._versionControlPreferences.branchName, + branchColor: preferences.branchColor ?? this._versionControlPreferences.branchColor, + branchReadOnly: preferences.branchReadOnly ?? this._versionControlPreferences.branchReadOnly, + privateKey: preferences.privateKey ?? this._versionControlPreferences.privateKey, + publicKey: preferences.publicKey ?? this._versionControlPreferences.publicKey, + repositoryUrl: preferences.repositoryUrl ?? this._versionControlPreferences.repositoryUrl, + }; + } + + async generateAndSaveKeyPair() { + const keyPair = generateSshKeyPair('ed25519'); if (keyPair.publicKey && keyPair.privateKey) { - this.setPreferences({ ...keyPair }); - await this.saveSamlPreferencesToDb(); + await this.setPreferences({ ...keyPair }); + } else { + LoggerProxy.error('Failed to generate key pair'); } return keyPair; } - setPreferences(prefs: Partial) { - this._versionControlPreferences = { - ...this._versionControlPreferences, - ...prefs, - }; + async validateVersionControlPreferences( + preferences: Partial, + ): Promise { + const preferencesObject = new VersionControlPreferences(preferences); + const validationResult = await validate(preferencesObject, { + forbidUnknownValues: false, + skipMissingProperties: true, + stopAtFirstError: false, + validationError: { target: false }, + }); + if (validationResult.length > 0) { + throw new Error(`Invalid version control preferences: ${JSON.stringify(validationResult)}`); + } + // TODO: if repositoryUrl is changed, check if it is valid + // TODO: if branchName is changed, check if it is valid + return validationResult; + } + + async setPreferences( + preferences: Partial, + saveToDb = true, + ): Promise { + this.versionControlPreferences = preferences; + if (saveToDb) { + const settingsValue = JSON.stringify(this._versionControlPreferences); + try { + await Db.collections.Settings.save({ + key: VERSION_CONTROL_PREFERENCES_DB_KEY, + value: settingsValue, + loadOnStartup: true, + }); + } catch (error) { + throw new Error(`Failed to save version control preferences: ${(error as Error).message}`); + } + } + return this.versionControlPreferences; } async loadFromDbAndApplyVersionControlPreferences(): Promise< VersionControlPreferences | undefined > { - const loadedPrefs = await Db.collections.Settings.findOne({ + const loadedPreferences = await Db.collections.Settings.findOne({ where: { key: VERSION_CONTROL_PREFERENCES_DB_KEY }, }); - if (loadedPrefs) { + if (loadedPreferences) { try { - const prefs = jsonParse(loadedPrefs.value); - if (prefs) { - this.setPreferences(prefs); - return prefs; + const preferences = jsonParse(loadedPreferences.value); + if (preferences) { + await this.setPreferences(preferences, false); + return preferences; } - } catch {} + } catch (error) { + LoggerProxy.warn( + `Could not parse Version Control settings from database: ${(error as Error).message}`, + ); + } } return; } - - async saveSamlPreferencesToDb(): Promise { - 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(result.value); - } catch {} - return; - } } diff --git a/packages/cli/src/environments/versionControl/versionControlHelper.ts b/packages/cli/src/environments/versionControl/versionControlHelper.ts index 4aa9a383c5..9810977f91 100644 --- a/packages/cli/src/environments/versionControl/versionControlHelper.ts +++ b/packages/cli/src/environments/versionControl/versionControlHelper.ts @@ -49,5 +49,8 @@ export function generateSshKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') { keyPair.publicKey = keyPublic.toString('ssh'); const keyPrivate = sshpk.parsePrivateKey(generatedKeyPair.privateKey, 'pem'); keyPair.privateKey = keyPrivate.toString('ssh-private'); - return keyPair; + return { + privateKey: keyPair.privateKey, + publicKey: keyPair.publicKey, + }; }