From 4c06871d8f4ea467d8374b8cb7bd10945c8ab4e6 Mon Sep 17 00:00:00 2001 From: Michael Auerswald Date: Mon, 24 Apr 2023 13:32:54 +0200 Subject: [PATCH 1/5] expand VersionControlPreferences --- .../versionControl/types/requests.ts | 7 ++ .../types/versionControlPreferences.ts | 32 +++++++- .../versionControl.controller.ee.ts | 18 +++- .../versionControl.service.ee.ts | 82 +++++++++++-------- .../versionControl/versionControlHelper.ts | 5 +- 5 files changed, 106 insertions(+), 38 deletions(-) create mode 100644 packages/cli/src/environments/versionControl/types/requests.ts 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 0000000000000..410505bb7703a --- /dev/null +++ b/packages/cli/src/environments/versionControl/types/requests.ts @@ -0,0 +1,7 @@ +import type { AuthenticatedRequest } from '@/requests'; +import type { VersionControlPreferences } from './versionControlPreferences'; + +export declare namespace VersionControlRequest { + type GetPreferences = AuthenticatedRequest<{}, VersionControlPreferences, {}, {}>; + 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 ff2becc5d48fb..e2cd517ade9d5 100644 --- a/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts +++ b/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts @@ -1,9 +1,35 @@ -import { IsString } from 'class-validator'; +import { IsBoolean, IsEmail, IsHexColor, IsOptional, IsString, IsUrl } from 'class-validator'; export class VersionControlPreferences { + @IsBoolean() + connected: boolean; + + @IsString() + @IsUrl() + repositoryUrl: string; + + @IsString() + authorName: string; + + @IsString() + @IsEmail() + authorEmail: string; + + @IsString() + branchName: string; + + @IsBoolean() + branchReadOnly: boolean; + + @IsString() + @IsHexColor() + branchColor: string; + + @IsOptional() @IsString() - privateKey: string; + readonly privateKey?: string; + @IsOptional() @IsString() - publicKey: string; + 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 fd3d38a6eab4a..2749e5c6f3c4f 100644 --- a/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts +++ b/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts @@ -1,19 +1,33 @@ -import { Get, RestController } from '../../decorators'; +import express from 'express'; +import { Get, Post, RestController } from '@/decorators'; import { versionControlLicensedMiddleware, versionControlLicensedOwnerMiddleware, } from './middleware/versionControlEnabledMiddleware'; import { VersionControlService } from './versionControl.service.ee'; +import { VersionControlRequest } from '@/environments/versionControl/types/requests'; +import type { VersionControlPreferences } from '@/environments/versionControl/types/versionControlPreferences'; @RestController('/versionControl') export class VersionControlController { constructor(private versionControlService: VersionControlService) {} @Get('/preferences', { middlewares: [versionControlLicensedMiddleware] }) - async getPreferences() { + async getPreferences(req: express.Request, res: VersionControlRequest.GetPreferences) { + // returns the settings with the privateKey property redacted return this.versionControlService.versionControlPreferences; } + @Post('/preferences', { middlewares: [versionControlLicensedOwnerMiddleware] }) + async setPreferences(req: VersionControlRequest.UpdatePreferences) { + const sanitizedPreferences: Partial = { + ...req.body, + privateKey: undefined, + publicKey: undefined, + }; + return this.versionControlService.setPreferences(sanitizedPreferences); + } + //TODO: temporary function to generate key and save new pair @Get('/generateKeyPair', { middlewares: [versionControlLicensedOwnerMiddleware] }) async generateKeyPair() { diff --git a/packages/cli/src/environments/versionControl/versionControl.service.ee.ts b/packages/cli/src/environments/versionControl/versionControl.service.ee.ts index 3096b5531ddd6..0b01a627d1396 100644 --- a/packages/cli/src/environments/versionControl/versionControl.service.ee.ts +++ b/packages/cli/src/environments/versionControl/versionControl.service.ee.ts @@ -3,7 +3,7 @@ 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'; @Service() export class VersionControlService { @@ -16,55 +16,73 @@ 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 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 4aa9a383c503a..9810977f91983 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, + }; } From adde5d560b5e9e944f411f5f48493851a4ef77e9 Mon Sep 17 00:00:00 2001 From: Michael Auerswald Date: Mon, 24 Apr 2023 14:59:18 +0200 Subject: [PATCH 2/5] use Authorized decorator for vc endpoints instead of middleware --- .../versionControlEnabledMiddleware.ts | 12 ---------- .../versionControl.controller.ee.ts | 22 +++++++++---------- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts b/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts index 55a5e2122e127..bfeb5bf25dff7 100644 --- a/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts +++ b/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts @@ -5,18 +5,6 @@ import { 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/versionControl.controller.ee.ts b/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts index 2749e5c6f3c4f..7333e853edcd8 100644 --- a/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts +++ b/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts @@ -1,24 +1,22 @@ -import express from 'express'; -import { Get, Post, 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 '@/environments/versionControl/types/requests'; -import type { VersionControlPreferences } from '@/environments/versionControl/types/versionControlPreferences'; +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(req: express.Request, res: VersionControlRequest.GetPreferences) { + async getPreferences(): Promise { // returns the settings with the privateKey property redacted return this.versionControlService.versionControlPreferences; } - @Post('/preferences', { middlewares: [versionControlLicensedOwnerMiddleware] }) + @Authorized(['global', 'owner']) + @Post('/preferences', { middlewares: [versionControlLicensedMiddleware] }) async setPreferences(req: VersionControlRequest.UpdatePreferences) { const sanitizedPreferences: Partial = { ...req.body, @@ -29,7 +27,9 @@ export class VersionControlController { } //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(); } From 2694afc663232f74ed24f0c9083f3d4067e70388 Mon Sep 17 00:00:00 2001 From: Michael Auerswald Date: Mon, 24 Apr 2023 15:45:49 +0200 Subject: [PATCH 3/5] validate preferences with class-validator --- .../types/versionControlPreferences.ts | 7 ++++--- .../versionControl.controller.ee.ts | 1 + .../versionControl.service.ee.ts | 20 +++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts b/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts index e2cd517ade9d5..181aebcf76833 100644 --- a/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts +++ b/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts @@ -1,17 +1,19 @@ import { IsBoolean, IsEmail, IsHexColor, IsOptional, IsString, IsUrl } from 'class-validator'; export class VersionControlPreferences { + constructor(preferences: Partial | undefined = undefined) { + if (preferences) Object.assign(this, preferences); + } + @IsBoolean() connected: boolean; @IsString() - @IsUrl() repositoryUrl: string; @IsString() authorName: string; - @IsString() @IsEmail() authorEmail: string; @@ -21,7 +23,6 @@ export class VersionControlPreferences { @IsBoolean() branchReadOnly: boolean; - @IsString() @IsHexColor() branchColor: string; diff --git a/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts b/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts index 7333e853edcd8..5bbac46f0bcb6 100644 --- a/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts +++ b/packages/cli/src/environments/versionControl/versionControl.controller.ee.ts @@ -23,6 +23,7 @@ export class VersionControlController { privateKey: undefined, publicKey: undefined, }; + await this.versionControlService.validateVersionControlPreferences(sanitizedPreferences); return this.versionControlService.setPreferences(sanitizedPreferences); } diff --git a/packages/cli/src/environments/versionControl/versionControl.service.ee.ts b/packages/cli/src/environments/versionControl/versionControl.service.ee.ts index 0b01a627d1396..d531935f00152 100644 --- a/packages/cli/src/environments/versionControl/versionControl.service.ee.ts +++ b/packages/cli/src/environments/versionControl/versionControl.service.ee.ts @@ -4,6 +4,8 @@ import { VersionControlPreferences } from './types/versionControlPreferences'; import { VERSION_CONTROL_PREFERENCES_DB_KEY } from './constants'; import * as Db from '@/Db'; import { jsonParse, LoggerProxy } from 'n8n-workflow'; +import type { ValidationError } from 'class-validator'; +import { validate } from 'class-validator'; @Service() export class VersionControlService { @@ -44,6 +46,24 @@ export class VersionControlService { return keyPair; } + 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, From 6b8d070dea1c5e2f528a0972d9c27bb917ac59bb Mon Sep 17 00:00:00 2001 From: Michael Auerswald Date: Mon, 24 Apr 2023 15:51:26 +0200 Subject: [PATCH 4/5] cleanup --- .../middleware/versionControlEnabledMiddleware.ts | 1 - .../versionControl/types/versionControlPreferences.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts b/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts index bfeb5bf25dff7..5ed3a1293b443 100644 --- a/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts +++ b/packages/cli/src/environments/versionControl/middleware/versionControlEnabledMiddleware.ts @@ -1,5 +1,4 @@ import type { RequestHandler } from 'express'; -import type { AuthenticatedRequest } from '@/requests'; import { isVersionControlLicensed, isVersionControlLicensedAndEnabled, diff --git a/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts b/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts index 181aebcf76833..9481270ec5394 100644 --- a/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts +++ b/packages/cli/src/environments/versionControl/types/versionControlPreferences.ts @@ -1,4 +1,4 @@ -import { IsBoolean, IsEmail, IsHexColor, IsOptional, IsString, IsUrl } from 'class-validator'; +import { IsBoolean, IsEmail, IsHexColor, IsOptional, IsString } from 'class-validator'; export class VersionControlPreferences { constructor(preferences: Partial | undefined = undefined) { From d6f6fdb12385da3fd0fc81f3db14f28488617e4e Mon Sep 17 00:00:00 2001 From: Michael Auerswald Date: Mon, 24 Apr 2023 15:53:13 +0200 Subject: [PATCH 5/5] cleanup --- packages/cli/src/environments/versionControl/types/requests.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/cli/src/environments/versionControl/types/requests.ts b/packages/cli/src/environments/versionControl/types/requests.ts index 410505bb7703a..0782873ba56ae 100644 --- a/packages/cli/src/environments/versionControl/types/requests.ts +++ b/packages/cli/src/environments/versionControl/types/requests.ts @@ -2,6 +2,5 @@ import type { AuthenticatedRequest } from '@/requests'; import type { VersionControlPreferences } from './versionControlPreferences'; export declare namespace VersionControlRequest { - type GetPreferences = AuthenticatedRequest<{}, VersionControlPreferences, {}, {}>; type UpdatePreferences = AuthenticatedRequest<{}, {}, Partial, {}>; }