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 { 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();

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 {
@IsString()
privateKey: string;
constructor(preferences: Partial<VersionControlPreferences> | 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;
}

View file

@ -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<VersionControlPreferences> {
// 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<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
@Get('/generateKeyPair', { middlewares: [versionControlLicensedOwnerMiddleware] })
// REMOVE THIS FUNCTION AFTER TESTING
@Authorized(['global', 'owner'])
@Get('/generateKeyPair', { middlewares: [versionControlLicensedMiddleware] })
async generateKeyPair() {
return this.versionControlService.generateAndSaveKeyPair();
}

View file

@ -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<VersionControlPreferences>) {
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<VersionControlPreferences>) {
this._versionControlPreferences = {
...this._versionControlPreferences,
...prefs,
};
async validateVersionControlPreferences(
preferences: Partial<VersionControlPreferences>,
): Promise<ValidationError[]> {
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<
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<VersionControlPreferences>(loadedPrefs.value);
if (prefs) {
this.setPreferences(prefs);
return prefs;
const preferences = jsonParse<VersionControlPreferences>(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<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

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