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
This commit is contained in:
Michael Auerswald 2023-04-24 17:13:25 +02:00 committed by GitHub
parent 124f41faa6
commit f3b4701863
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 133 additions and 57 deletions

View file

@ -1,22 +1,9 @@
import type { RequestHandler } from 'express'; import type { RequestHandler } from 'express';
import type { AuthenticatedRequest } from '@/requests';
import { import {
isVersionControlLicensed, isVersionControlLicensed,
isVersionControlLicensedAndEnabled, isVersionControlLicensedAndEnabled,
} from '../versionControlHelper'; } 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) => { export const versionControlLicensedAndEnabledMiddleware: RequestHandler = (req, res, next) => {
if (isVersionControlLicensedAndEnabled()) { if (isVersionControlLicensedAndEnabled()) {
next(); next();

View file

@ -0,0 +1,6 @@
import type { AuthenticatedRequest } from '@/requests';
import type { VersionControlPreferences } from './versionControlPreferences';
export declare namespace VersionControlRequest {
type UpdatePreferences = AuthenticatedRequest<{}, {}, Partial<VersionControlPreferences>, {}>;
}

View file

@ -1,9 +1,36 @@
import { IsString } from 'class-validator'; import { IsBoolean, IsEmail, IsHexColor, IsOptional, IsString } from 'class-validator';
export class VersionControlPreferences { export class VersionControlPreferences {
@IsString() constructor(preferences: Partial<VersionControlPreferences> | undefined = undefined) {
privateKey: string; if (preferences) Object.assign(this, preferences);
}
@IsBoolean()
connected: boolean;
@IsString() @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;
} }

View file

@ -1,21 +1,36 @@
import { Get, RestController } from '../../decorators'; import { Authorized, Get, Post, RestController } from '@/decorators';
import { import { versionControlLicensedMiddleware } from './middleware/versionControlEnabledMiddleware';
versionControlLicensedMiddleware,
versionControlLicensedOwnerMiddleware,
} from './middleware/versionControlEnabledMiddleware';
import { VersionControlService } from './versionControl.service.ee'; import { VersionControlService } from './versionControl.service.ee';
import { VersionControlRequest } from './types/requests';
import type { VersionControlPreferences } from './types/versionControlPreferences';
@RestController('/versionControl') @RestController('/versionControl')
export class VersionControlController { export class VersionControlController {
constructor(private versionControlService: VersionControlService) {} constructor(private versionControlService: VersionControlService) {}
@Authorized('any')
@Get('/preferences', { middlewares: [versionControlLicensedMiddleware] }) @Get('/preferences', { middlewares: [versionControlLicensedMiddleware] })
async getPreferences() { async getPreferences(): Promise<VersionControlPreferences> {
// returns the settings with the privateKey property redacted
return this.versionControlService.versionControlPreferences; return this.versionControlService.versionControlPreferences;
} }
@Authorized(['global', 'owner'])
@Post('/preferences', { middlewares: [versionControlLicensedMiddleware] })
async setPreferences(req: VersionControlRequest.UpdatePreferences) {
const sanitizedPreferences: Partial<VersionControlPreferences> = {
...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 //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() { async generateKeyPair() {
return this.versionControlService.generateAndSaveKeyPair(); return this.versionControlService.generateAndSaveKeyPair();
} }

View file

@ -3,7 +3,9 @@ import { generateSshKeyPair } from './versionControlHelper';
import { VersionControlPreferences } from './types/versionControlPreferences'; import { VersionControlPreferences } from './types/versionControlPreferences';
import { VERSION_CONTROL_PREFERENCES_DB_KEY } from './constants'; import { VERSION_CONTROL_PREFERENCES_DB_KEY } from './constants';
import * as Db from '@/Db'; 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() @Service()
export class VersionControlService { export class VersionControlService {
@ -16,55 +18,91 @@ export class VersionControlService {
public get versionControlPreferences(): VersionControlPreferences { public get versionControlPreferences(): VersionControlPreferences {
return { return {
...this._versionControlPreferences, ...this._versionControlPreferences,
privateKey: '', privateKey: '(redacted)',
}; };
} }
async generateAndSaveKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') { public set versionControlPreferences(preferences: Partial<VersionControlPreferences>) {
const keyPair = generateSshKeyPair(keyType); 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) { if (keyPair.publicKey && keyPair.privateKey) {
this.setPreferences({ ...keyPair }); await this.setPreferences({ ...keyPair });
await this.saveSamlPreferencesToDb(); } else {
LoggerProxy.error('Failed to generate key pair');
} }
return keyPair; return keyPair;
} }
setPreferences(prefs: Partial<VersionControlPreferences>) { async validateVersionControlPreferences(
this._versionControlPreferences = { preferences: Partial<VersionControlPreferences>,
...this._versionControlPreferences, ): Promise<ValidationError[]> {
...prefs, 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<VersionControlPreferences>,
saveToDb = true,
): Promise<VersionControlPreferences> {
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< async loadFromDbAndApplyVersionControlPreferences(): Promise<
VersionControlPreferences | undefined VersionControlPreferences | undefined
> { > {
const loadedPrefs = await Db.collections.Settings.findOne({ const loadedPreferences = await Db.collections.Settings.findOne({
where: { key: VERSION_CONTROL_PREFERENCES_DB_KEY }, where: { key: VERSION_CONTROL_PREFERENCES_DB_KEY },
}); });
if (loadedPrefs) { if (loadedPreferences) {
try { try {
const prefs = jsonParse<VersionControlPreferences>(loadedPrefs.value); const preferences = jsonParse<VersionControlPreferences>(loadedPreferences.value);
if (prefs) { if (preferences) {
this.setPreferences(prefs); await this.setPreferences(preferences, false);
return prefs; return preferences;
} }
} catch {} } catch (error) {
LoggerProxy.warn(
`Could not parse Version Control settings from database: ${(error as Error).message}`,
);
} }
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; return;
} }
} }

View file

@ -49,5 +49,8 @@ export function generateSshKeyPair(keyType: 'ed25519' | 'rsa' = 'ed25519') {
keyPair.publicKey = keyPublic.toString('ssh'); keyPair.publicKey = keyPublic.toString('ssh');
const keyPrivate = sshpk.parsePrivateKey(generatedKeyPair.privateKey, 'pem'); const keyPrivate = sshpk.parsePrivateKey(generatedKeyPair.privateKey, 'pem');
keyPair.privateKey = keyPrivate.toString('ssh-private'); keyPair.privateKey = keyPrivate.toString('ssh-private');
return keyPair; return {
privateKey: keyPair.privateKey,
publicKey: keyPair.publicKey,
};
} }