Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Manage version control settings #6079

Merged
merged 6 commits into from
Apr 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { AuthenticatedRequest } from '@/requests';
import type { VersionControlPreferences } from './versionControlPreferences';

export declare namespace VersionControlRequest {
type UpdatePreferences = AuthenticatedRequest<{}, {}, Partial<VersionControlPreferences>, {}>;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,36 @@
import { IsString } from 'class-validator';
import { IsBoolean, IsEmail, IsHexColor, IsOptional, IsString } from 'class-validator';

export class VersionControlPreferences {
constructor(preferences: Partial<VersionControlPreferences> | undefined = undefined) {
if (preferences) Object.assign(this, preferences);
}

@IsBoolean()
connected: boolean;

@IsString()
repositoryUrl: string;

@IsString()
authorName: string;

@IsEmail()
authorEmail: string;

@IsString()
branchName: string;

@IsBoolean()
branchReadOnly: boolean;

@IsHexColor()
branchColor: string;

@IsOptional()
@IsString()
privateKey: string;
readonly privateKey?: string;

@IsOptional()
@IsString()
publicKey: string;
readonly publicKey?: string;
}
Original file line number Diff line number Diff line change
@@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}