Skip to content

Commit

Permalink
feat: External Secrets storage for credentials (#6477)
Browse files Browse the repository at this point in the history
Github issue / Community forum post (link here to close automatically):

---------

Co-authored-by: Romain Minaud <romain.minaud@gmail.com>
Co-authored-by: Valya Bullions <valya@n8n.io>
Co-authored-by: Csaba Tuncsik <csaba@n8n.io>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
Co-authored-by: Omar Ajoue <krynble@gmail.com>
  • Loading branch information
6 people committed Aug 25, 2023
1 parent c833078 commit ed927d3
Show file tree
Hide file tree
Showing 89 changed files with 4,164 additions and 57 deletions.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
"formidable": "^3.5.0",
"google-timezones-json": "^1.1.0",
"handlebars": "4.7.7",
"infisical-node": "^1.3.0",
"inquirer": "^7.0.1",
"ioredis": "^5.2.4",
"json-diff": "^1.0.6",
Expand Down
70 changes: 64 additions & 6 deletions packages/cli/src/CredentialsHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type {
IHttpRequestHelper,
INodeTypeData,
INodeTypes,
IWorkflowExecuteAdditionalData,
ICredentialTestFunctions,
} from 'n8n-workflow';
import {
Expand Down Expand Up @@ -342,6 +343,7 @@ export class CredentialsHelper extends ICredentialsHelper {
* @param {boolean} [raw] Return the data as supplied without defaults or overwrites
*/
async getDecrypted(
additionalData: IWorkflowExecuteAdditionalData,
nodeCredentials: INodeCredentialsDetails,
type: string,
mode: WorkflowExecuteMode,
Expand All @@ -356,24 +358,32 @@ export class CredentialsHelper extends ICredentialsHelper {
return decryptedDataOriginal;
}

await additionalData?.secretsHelpers?.waitForInit();

const canUseSecrets = await this.credentialOwnedByOwner(nodeCredentials);

return this.applyDefaultsAndOverwrites(
additionalData,
decryptedDataOriginal,
type,
mode,
defaultTimezone,
expressionResolveValues,
canUseSecrets,
);
}

/**
* Applies credential default data and overwrites
*/
applyDefaultsAndOverwrites(
additionalData: IWorkflowExecuteAdditionalData,
decryptedDataOriginal: ICredentialDataDecryptedObject,
type: string,
mode: WorkflowExecuteMode,
defaultTimezone: string,
expressionResolveValues?: ICredentialsExpressionResolveValues,
canUseSecrets?: boolean,
): ICredentialDataDecryptedObject {
const credentialsProperties = this.getCredentialsProperties(type);

Expand All @@ -395,6 +405,10 @@ export class CredentialsHelper extends ICredentialsHelper {
decryptedData.oauthTokenData = decryptedDataOriginal.oauthTokenData;
}

const additionalKeys = NodeExecuteFunctions.getAdditionalKeys(additionalData, mode, null, {
secretsEnabled: canUseSecrets,
});

if (expressionResolveValues) {
const timezone = expressionResolveValues.workflow.settings.timezone ?? defaultTimezone;

Expand All @@ -408,7 +422,7 @@ export class CredentialsHelper extends ICredentialsHelper {
expressionResolveValues.connectionInputData,
mode,
timezone,
{},
additionalKeys,
undefined,
false,
decryptedData,
Expand All @@ -431,7 +445,7 @@ export class CredentialsHelper extends ICredentialsHelper {
decryptedData as INodeParameters,
mode,
defaultTimezone,
{},
additionalKeys,
undefined,
undefined,
decryptedData,
Expand Down Expand Up @@ -573,10 +587,24 @@ export class CredentialsHelper extends ICredentialsHelper {
}

if (credentialsDecrypted.data) {
credentialsDecrypted.data = CredentialsOverwrites().applyOverwrite(
credentialType,
credentialsDecrypted.data,
);
try {
const additionalData = await WorkflowExecuteAdditionalData.getBase(user.id);
credentialsDecrypted.data = this.applyDefaultsAndOverwrites(
additionalData,
credentialsDecrypted.data,
credentialType,
'internal' as WorkflowExecuteMode,
additionalData.timezone,
undefined,
user.isOwner,
);
} catch (error) {
Logger.debug('Credential test failed', error);
return {
status: 'Error',
message: error.message.toString(),
};
}
}

if (typeof credentialTestFunction === 'function') {
Expand Down Expand Up @@ -759,6 +787,36 @@ export class CredentialsHelper extends ICredentialsHelper {
message: 'Connection successful!',
};
}

async credentialOwnedByOwner(nodeCredential: INodeCredentialsDetails): Promise<boolean> {
if (!nodeCredential.id) {
return false;
}

const credential = await Db.collections.SharedCredentials.findOne({
where: {
role: {
scope: 'credential',
name: 'owner',
},
user: {
globalRole: {
scope: 'global',
name: 'owner',
},
},
credentials: {
id: nodeCredential.id,
},
},
});

if (!credential) {
return false;
}

return true;
}
}

/**
Expand Down
102 changes: 102 additions & 0 deletions packages/cli/src/ExternalSecrets/ExternalSecrets.controller.ee.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Authorized, Get, Post, RestController } from '@/decorators';
import { ExternalSecretsRequest } from '@/requests';
import { NotFoundError } from '@/ResponseHelper';
import { Response } from 'express';
import { Service } from 'typedi';
import { ProviderNotFoundError, ExternalSecretsService } from './ExternalSecrets.service.ee';

@Service()
@Authorized(['global', 'owner'])
@RestController('/external-secrets')
export class ExternalSecretsController {
constructor(private readonly secretsService: ExternalSecretsService) {}

@Get('/providers')
async getProviders() {
return this.secretsService.getProviders();
}

@Get('/providers/:provider')
async getProvider(req: ExternalSecretsRequest.GetProvider) {
const providerName = req.params.provider;
try {
return this.secretsService.getProvider(providerName);
} catch (e) {
if (e instanceof ProviderNotFoundError) {
throw new NotFoundError(`Could not find provider "${e.providerName}"`);
}
throw e;
}
}

@Post('/providers/:provider/test')
async testProviderSettings(req: ExternalSecretsRequest.TestProviderSettings, res: Response) {
const providerName = req.params.provider;
try {
const result = await this.secretsService.testProviderSettings(providerName, req.body);
if (result.success) {
res.statusCode = 200;
} else {
res.statusCode = 400;
}
return result;
} catch (e) {
if (e instanceof ProviderNotFoundError) {
throw new NotFoundError(`Could not find provider "${e.providerName}"`);
}
throw e;
}
}

@Post('/providers/:provider')
async setProviderSettings(req: ExternalSecretsRequest.SetProviderSettings) {
const providerName = req.params.provider;
try {
await this.secretsService.saveProviderSettings(providerName, req.body, req.user.id);
} catch (e) {
if (e instanceof ProviderNotFoundError) {
throw new NotFoundError(`Could not find provider "${e.providerName}"`);
}
throw e;
}
return {};
}

@Post('/providers/:provider/connect')
async setProviderConnected(req: ExternalSecretsRequest.SetProviderConnected) {
const providerName = req.params.provider;
try {
await this.secretsService.saveProviderConnected(providerName, req.body.connected);
} catch (e) {
if (e instanceof ProviderNotFoundError) {
throw new NotFoundError(`Could not find provider "${e.providerName}"`);
}
throw e;
}
return {};
}

@Post('/providers/:provider/update')
async updateProvider(req: ExternalSecretsRequest.UpdateProvider, res: Response) {
const providerName = req.params.provider;
try {
const resp = await this.secretsService.updateProvider(providerName);
if (resp) {
res.statusCode = 200;
} else {
res.statusCode = 400;
}
return { updated: resp };
} catch (e) {
if (e instanceof ProviderNotFoundError) {
throw new NotFoundError(`Could not find provider "${e.providerName}"`);
}
throw e;
}
}

@Get('/secrets')
getSecretNames() {
return this.secretsService.getAllSecrets();
}
}
Loading

0 comments on commit ed927d3

Please sign in to comment.