diff --git a/packages/cli/commands/execute.ts b/packages/cli/commands/execute.ts index 448af3de3d..211f9e6479 100644 --- a/packages/cli/commands/execute.ts +++ b/packages/cli/commands/execute.ts @@ -6,6 +6,7 @@ import { import { ActiveExecutions, + CredentialsOverwrites, Db, GenericHelpers, IWorkflowBase, @@ -100,6 +101,10 @@ export class Execute extends Command { // Wait till the n8n-packages have been read await loadNodesAndCredentialsPromise; + // Load the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites(); + await credentialsOverwrites.init(); + // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); await nodeTypes.init(loadNodesAndCredentials.nodeTypes); diff --git a/packages/cli/commands/start.ts b/packages/cli/commands/start.ts index e8f1694b9e..15d225ef16 100644 --- a/packages/cli/commands/start.ts +++ b/packages/cli/commands/start.ts @@ -11,6 +11,7 @@ import * as config from '../config'; import { ActiveWorkflowRunner, CredentialTypes, + CredentialsOverwrites, Db, GenericHelpers, LoadNodesAndCredentials, @@ -112,6 +113,10 @@ export class Start extends Command { const loadNodesAndCredentials = LoadNodesAndCredentials(); await loadNodesAndCredentials.init(); + // Load the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites(); + await credentialsOverwrites.init(); + // Add the found types to an instance other parts of the application can use const nodeTypes = NodeTypes(); await nodeTypes.init(loadNodesAndCredentials.nodeTypes); diff --git a/packages/cli/config/index.ts b/packages/cli/config/index.ts index f8b776ec82..3df13d0440 100644 --- a/packages/cli/config/index.ts +++ b/packages/cli/config/index.ts @@ -54,6 +54,19 @@ const config = convict({ }, }, + credentials: { + overwrite: { + // Allows to set default values for credentials which + // get automatically prefilled and the user does not get + // displayed and can not change. + // Format: { CREDENTIAL_NAME: { PARAMTER: VALUE }} + doc: 'Overwrites for credentials', + format: '*', + default: '{}', + env: 'CREDENTIALS_OVERWRITE' + } + }, + executions: { // If a workflow executes all the data gets saved by default. This // could be a problem when a workflow gets executed a lot and processes diff --git a/packages/cli/src/CredentialTypes.ts b/packages/cli/src/CredentialTypes.ts index dc14801cbc..20a0e36e9e 100644 --- a/packages/cli/src/CredentialTypes.ts +++ b/packages/cli/src/CredentialTypes.ts @@ -3,6 +3,9 @@ import { ICredentialTypes as ICredentialTypesInterface, } from 'n8n-workflow'; +import { + CredentialsOverwrites, +} from './'; class CredentialTypesClass implements ICredentialTypesInterface { @@ -13,6 +16,19 @@ class CredentialTypesClass implements ICredentialTypesInterface { async init(credentialTypes: { [key: string]: ICredentialType }): Promise { this.credentialTypes = credentialTypes; + + // Load the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites().getAll(); + + for (const credentialType of Object.keys(credentialsOverwrites)) { + if (credentialTypes[credentialType] === undefined) { + continue; + } + + // Add which properties got overwritten that the Editor-UI knows + // which properties it should hide + credentialTypes[credentialType].__overwrittenProperties = Object.keys(credentialsOverwrites[credentialType]); + } } getAll(): ICredentialType[] { diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts new file mode 100644 index 0000000000..a5dee733f2 --- /dev/null +++ b/packages/cli/src/CredentialsHelper.ts @@ -0,0 +1,81 @@ +import { + Credentials, +} from 'n8n-core'; + +import { + ICredentialDataDecryptedObject, + ICredentialsHelper, +} from 'n8n-workflow'; + +import { + CredentialsOverwrites, + Db, + ICredentialsDb, +} from './'; + + +export class CredentialsHelper extends ICredentialsHelper { + + /** + * Returns the credentials instance + * + * @param {string} name Name of the credentials to return instance of + * @param {string} type Type of the credentials to return instance of + * @returns {Credentials} + * @memberof CredentialsHelper + */ + getCredentials(name: string, type: string): Credentials { + if (!this.workflowCredentials[type]) { + throw new Error(`No credentials of type "${type}" exist.`); + } + if (!this.workflowCredentials[type][name]) { + throw new Error(`No credentials with name "${name}" exist for type "${type}".`); + } + const credentialData = this.workflowCredentials[type][name]; + + return new Credentials(credentialData.name, credentialData.type, credentialData.nodesAccess, credentialData.data); + } + + + /** + * Returns the decrypted credential data with applied overwrites + * + * @param {string} name Name of the credentials to return data of + * @param {string} type Type of the credentials to return data of + * @returns {ICredentialDataDecryptedObject} + * @memberof CredentialsHelper + */ + getDecrypted(name: string, type: string): ICredentialDataDecryptedObject { + const credentials = this.getCredentials(name, type); + + // Load and apply the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites(); + return credentialsOverwrites.applyOverwrite(credentials.type, credentials.getData(this.encryptionKey)); + } + + + /** + * Updates credentials in the database + * + * @param {string} name Name of the credentials to set data of + * @param {string} type Type of the credentials to set data of + * @param {ICredentialDataDecryptedObject} data The data to set + * @returns {Promise} + * @memberof CredentialsHelper + */ + async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise { + const credentials = await this.getCredentials(name, type); + + credentials.setData(data, this.encryptionKey); + const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; + + // Add special database related data + newCredentialsData.updatedAt = new Date(); + + // TODO: also add user automatically depending on who is logged in, if anybody is logged in + + // Save the credentials in DB + await Db.collections.Credentials!.save(newCredentialsData); + } + +} diff --git a/packages/cli/src/CredentialsOverwrites.ts b/packages/cli/src/CredentialsOverwrites.ts new file mode 100644 index 0000000000..6d49092c37 --- /dev/null +++ b/packages/cli/src/CredentialsOverwrites.ts @@ -0,0 +1,56 @@ +import { + ICredentialDataDecryptedObject, +} from 'n8n-workflow'; + +import { + ICredentialsOverwrite, + GenericHelpers, +} from './'; + + +class CredentialsOverwritesClass { + + private overwriteData: ICredentialsOverwrite = {}; + + async init() { + const data = await GenericHelpers.getConfigValue('credentials.overwrite') as string; + + try { + this.overwriteData = JSON.parse(data); + } catch (error) { + throw new Error(`The credentials-overwrite is not valid JSON.`); + } + } + + applyOverwrite(type: string, data: ICredentialDataDecryptedObject) { + const overwrites = this.get(type); + + if (overwrites === undefined) { + return data; + } + + const returnData = JSON.parse(JSON.stringify(data)); + Object.assign(returnData, overwrites); + + return returnData; + } + + get(type: string): ICredentialDataDecryptedObject | undefined { + return this.overwriteData[type]; + } + + getAll(): ICredentialsOverwrite { + return this.overwriteData; + } +} + + +let credentialsOverwritesInstance: CredentialsOverwritesClass | undefined; + +export function CredentialsOverwrites(): CredentialsOverwritesClass { + if (credentialsOverwritesInstance === undefined) { + credentialsOverwritesInstance = new CredentialsOverwritesClass(); + } + + return credentialsOverwritesInstance; +} diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index a5837b8fc6..7542d018b1 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -1,4 +1,5 @@ import { + ICredentialDataDecryptedObject, ICredentialsDecrypted, ICredentialsEncrypted, IDataObject, @@ -34,6 +35,9 @@ export interface ICustomRequest extends Request { parsedUrl: Url | undefined; } +export interface ICredentialsOverwrite { + [key: string]: ICredentialDataDecryptedObject; +} export interface IDatabaseCollections { Credentials: Repository | null; diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index eccfcb0804..25fc77504d 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -137,7 +137,7 @@ class LoadNodesAndCredentialsClass { } } - this.credentialTypes[credentialName] = tempCredential; + this.credentialTypes[tempCredential.name] = tempCredential; } diff --git a/packages/cli/src/NodeTypes.ts b/packages/cli/src/NodeTypes.ts index 66b2363bfe..f600e8b734 100644 --- a/packages/cli/src/NodeTypes.ts +++ b/packages/cli/src/NodeTypes.ts @@ -15,7 +15,7 @@ class NodeTypesClass implements INodeTypes { // Some nodeTypes need to get special parameters applied like the // polling nodes the polling times for (const nodeTypeData of Object.values(nodeTypes)) { - const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeTypeData.type) + const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeTypeData.type); if (applyParameters.length) { nodeTypeData.type.description.properties.unshift.apply(nodeTypeData.type.description.properties, applyParameters); diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 900dcb012f..9da615f7a3 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -18,6 +18,7 @@ import * as csrf from 'csrf'; import { ActiveExecutions, ActiveWorkflowRunner, + CredentialsOverwrites, CredentialTypes, Db, IActivationError, @@ -648,6 +649,10 @@ class App { this.app.post('/rest/credentials', ResponseHelper.send(async (req: express.Request, res: express.Response): Promise => { const incomingData = req.body; + if (!incomingData.name || incomingData.name.length < 3) { + throw new ResponseHelper.ResponseError(`Credentials name must be at least 3 characters long.`, undefined, 400); + } + // Add the added date for node access permissions for (const nodeAccess of incomingData.nodesAccess) { nodeAccess.date = this.getCurrentDate(); @@ -684,6 +689,7 @@ class App { // Save the credentials in DB const result = await Db.collections.Credentials!.save(newCredentialsData); + result.data = incomingData.data; // Convert to response format in which the id is a string (result as unknown as ICredentialsResponse).id = result.id.toString(); @@ -805,14 +811,6 @@ class App { const results = await Db.collections.Credentials!.find(findQuery) as unknown as ICredentialsResponse[]; - let encryptionKey = undefined; - if (req.query.includeData === true) { - encryptionKey = await UserSettings.getEncryptionKey(); - if (encryptionKey === undefined) { - throw new Error('No encryption key got found to decrypt the credentials!'); - } - } - let result; for (result of results) { (result as ICredentialsDecryptedResponse).id = result.id.toString(); @@ -866,19 +864,17 @@ class App { } const credentials = new Credentials(result.name, result.type, result.nodesAccess, result.data); - (result as ICredentialsDecryptedDb).data = credentials.getData(encryptionKey!); - (result as ICredentialsDecryptedResponse).id = result.id.toString(); + const savedCredentialsData = credentials.getData(encryptionKey); - const oauthCredentials = (result as ICredentialsDecryptedDb).data; - if (oauthCredentials === undefined) { - throw new Error('Unable to read OAuth credentials'); - } + // Load the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites(); + const oauthCredentials = credentialsOverwrites.applyOverwrite(credentials.type, savedCredentialsData); const token = new csrf(); // Generate a CSRF prevention token and send it as a OAuth2 state stringma/ERR - oauthCredentials.csrfSecret = token.secretSync(); + const csrfSecret = token.secretSync(); const state = { - token: token.create(oauthCredentials.csrfSecret), + token: token.create(csrfSecret), cid: req.query.id }; const stateEncodedStr = Buffer.from(JSON.stringify(state)).toString('base64') as string; @@ -893,7 +889,8 @@ class App { state: stateEncodedStr, }); - credentials.setData(oauthCredentials, encryptionKey); + savedCredentialsData.csrfSecret = csrfSecret; + credentials.setData(savedCredentialsData, encryptionKey); const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; // Add special database related data @@ -939,12 +936,11 @@ class App { } const credentials = new Credentials(result.name, result.type, result.nodesAccess, result.data); - (result as ICredentialsDecryptedDb).data = credentials.getData(encryptionKey!); - const oauthCredentials = (result as ICredentialsDecryptedDb).data; - if (oauthCredentials === undefined) { - const errorResponse = new ResponseHelper.ResponseError('Unable to read OAuth credentials!', undefined, 503); - return ResponseHelper.sendErrorResponse(res, errorResponse); - } + const savedCredentialsData = credentials.getData(encryptionKey!); + + // Load the credentials overwrites if any exist + const credentialsOverwrites = CredentialsOverwrites(); + const oauthCredentials = credentialsOverwrites.applyOverwrite(credentials.type, savedCredentialsData); const token = new csrf(); if (oauthCredentials.csrfSecret === undefined || !token.verify(oauthCredentials.csrfSecret as string, state.token)) { @@ -968,9 +964,10 @@ class App { return ResponseHelper.sendErrorResponse(res, errorResponse); } - oauthCredentials.oauthTokenData = JSON.stringify(oauthToken.data); - _.unset(oauthCredentials, 'csrfSecret'); - credentials.setData(oauthCredentials, encryptionKey); + savedCredentialsData.oauthTokenData = JSON.stringify(oauthToken.data); + _.unset(savedCredentialsData, 'csrfSecret'); + + credentials.setData(savedCredentialsData, encryptionKey); const newCredentialsData = credentials.getDataToSave() as unknown as ICredentialsDb; // Add special database related data newCredentialsData.updatedAt = this.getCurrentDate(); diff --git a/packages/cli/src/WorkflowExecuteAdditionalData.ts b/packages/cli/src/WorkflowExecuteAdditionalData.ts index e234c11410..99fde65d1c 100644 --- a/packages/cli/src/WorkflowExecuteAdditionalData.ts +++ b/packages/cli/src/WorkflowExecuteAdditionalData.ts @@ -1,6 +1,6 @@ import { + CredentialsHelper, Db, - ICredentialsDb, IExecutionDb, IExecutionFlattedDb, IPushDataExecutionFinished, @@ -14,13 +14,11 @@ import { } from './'; import { - Credentials, UserSettings, WorkflowExecute, } from 'n8n-core'; import { - ICredentialDataDecryptedObject, IDataObject, IExecuteData, IExecuteWorkflowInfo, @@ -372,40 +370,6 @@ export async function executeWorkflow(workflowInfo: IExecuteWorkflowInfo, additi } -/** - * Updates credentials with new data - * - * @export - * @param {string} name Name of the credentials to update - * @param {string} type Type of the credentials to update - * @param {ICredentialDataDecryptedObject} data The new credential data - * @param {string} encryptionKey The encryption key to use - * @returns {Promise} - */ -export async function updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject, encryptionKey: string): Promise { - const foundCredentials = await Db.collections.Credentials!.find({ name, type }); - - if (!foundCredentials.length) { - throw new Error(`Could not find credentials for type "${type}" with name "${name}".`); - } - - const credentialsDb = foundCredentials[0]; - - // Encrypt the data - const credentials = new Credentials(credentialsDb.name, credentialsDb.type, credentialsDb.nodesAccess); - credentials.setData(data, encryptionKey); - const newCredentialsData = credentials.getDataToSave() as ICredentialsDb; - - // Add special database related data - newCredentialsData.updatedAt = this.getCurrentDate(); - - // TODO: also add user automatically depending on who is logged in, if anybody is logged in - - // Save the credentials in DB - await Db.collections.Credentials!.save(newCredentialsData); -} - - /** * Returns the base additional data without webhooks * @@ -428,11 +392,11 @@ export async function getBase(credentials: IWorkflowCredentials, currentNodePara return { credentials, + credentialsHelper: new CredentialsHelper(credentials, encryptionKey), encryptionKey, executeWorkflow, restApiUrl: urlBaseWebhook + config.get('endpoints.rest') as string, timezone, - updateCredentials, webhookBaseUrl, webhookTestBaseUrl, currentNodeParameters, diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 0b7ae6ad0a..3916e79edd 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,4 +1,6 @@ +export * from './CredentialsHelper'; export * from './CredentialTypes'; +export * from './CredentialsOverwrites'; export * from './Interfaces'; export * from './LoadNodesAndCredentials'; export * from './NodeTypes'; diff --git a/packages/core/src/Credentials.ts b/packages/core/src/Credentials.ts index 2559d78dda..e692f597cd 100644 --- a/packages/core/src/Credentials.ts +++ b/packages/core/src/Credentials.ts @@ -1,25 +1,14 @@ import { - ICredentialDataDecryptedObject, CredentialInformation, + ICredentialDataDecryptedObject, + ICredentials, ICredentialsEncrypted, - ICredentialNodeAccess, } from 'n8n-workflow'; import { enc, AES } from 'crypto-js'; -export class Credentials implements ICredentialsEncrypted { - name: string; - type: string; - data: string | undefined; - nodesAccess: ICredentialNodeAccess[]; - - constructor(name: string, type: string, nodesAccess: ICredentialNodeAccess[], data?: string) { - this.name = name; - this.type = type; - this.nodesAccess = nodesAccess; - this.data = data; - } +export class Credentials extends ICredentials { /** diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index cfdcdc8061..2c13b2c389 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -1,5 +1,4 @@ import { - Credentials, IHookFunctions, ILoadOptionsFunctions, IResponseError, @@ -154,7 +153,7 @@ export function requestOAuth(this: IAllExecuteFunctions, credentialsType: string const name = node.credentials[credentialsType]; // Save the refreshed token - await additionalData.updateCredentials(name, credentialsType, newCredentialsData, additionalData.encryptionKey); + await additionalData.credentialsHelper.updateCredentials(name, credentialsType, newCredentialsData); // Make the request again with the new token const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject); @@ -244,20 +243,7 @@ export function getCredentials(workflow: Workflow, node: INode, type: string, ad const name = node.credentials[type]; - if (!additionalData.credentials[type]) { - throw new Error(`No credentials of type "${type}" exist.`); - } - if (!additionalData.credentials[type][name]) { - throw new Error(`No credentials with name "${name}" exist for type "${type}".`); - } - const credentialData = additionalData.credentials[type][name]; - - const credentials = new Credentials(name, type, credentialData.nodesAccess, credentialData.data); - const decryptedDataObject = credentials.getData(additionalData.encryptionKey, node.type); - - if (decryptedDataObject === null) { - throw new Error('Could not get the credentials'); - } + const decryptedDataObject = additionalData.credentialsHelper.getDecrypted(name, type); return decryptedDataObject; } @@ -423,7 +409,7 @@ export function getExecutePollFunctions(workflow: Workflow, node: INode, additio helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, returnJsonArray, @@ -477,7 +463,7 @@ export function getExecuteTriggerFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, returnJsonArray, @@ -558,7 +544,7 @@ export function getExecuteFunctions(workflow: Workflow, runExecutionData: IRunEx helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, returnJsonArray, @@ -640,7 +626,7 @@ export function getExecuteSingleFunctions(workflow: Workflow, runExecutionData: helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, }, @@ -690,7 +676,7 @@ export function getLoadOptionsFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, }, @@ -748,7 +734,7 @@ export function getExecuteHookFunctions(workflow: Workflow, node: INode, additio }, helpers: { request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, }, @@ -833,7 +819,7 @@ export function getExecuteWebhookFunctions(workflow: Workflow, node: INode, addi helpers: { prepareBinaryData, request: requestPromise, - requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { + requestOAuth(this: IAllExecuteFunctions, credentialsType: string, requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions): Promise { // tslint:disable-line:no-any return requestOAuth.call(this, credentialsType, requestOptions, node, additionalData); }, returnJsonArray, diff --git a/packages/core/src/UserSettings.ts b/packages/core/src/UserSettings.ts index af32d5e36c..211341ed25 100644 --- a/packages/core/src/UserSettings.ts +++ b/packages/core/src/UserSettings.ts @@ -143,7 +143,6 @@ export async function writeUserSettings(userSettings: IUserSettings, settingsPat */ export async function getUserSettings(settingsPath?: string, ignoreCache?: boolean): Promise { if (settingsCache !== undefined && ignoreCache !== true) { - return settingsCache; } diff --git a/packages/core/test/Helpers.ts b/packages/core/test/Helpers.ts index 8a92f35718..790025dcda 100644 --- a/packages/core/test/Helpers.ts +++ b/packages/core/test/Helpers.ts @@ -2,6 +2,7 @@ import { set } from 'lodash'; import { ICredentialDataDecryptedObject, + ICredentialsHelper, IExecuteWorkflowInfo, INodeExecutionData, INodeParameters, @@ -16,11 +17,25 @@ import { } from 'n8n-workflow'; import { + Credentials, IDeferredPromise, IExecuteFunctions, } from '../src'; +export class CredentialsHelper extends ICredentialsHelper { + getDecrypted(name: string, type: string): ICredentialDataDecryptedObject { + return {}; + } + + getCredentials(name: string, type: string): Credentials { + return new Credentials('', '', [], ''); + } + + async updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise {} +} + + class NodeTypesClass implements INodeTypes { nodeTypes: INodeTypeData = { @@ -276,12 +291,12 @@ export function WorkflowExecuteAdditionalData(waitPromise: IDeferredPromise => {}, // tslint:disable-line:no-any restApiUrl: '', encryptionKey: 'test', timezone: 'America/New_York', - updateCredentials: async (name: string, type: string, data: ICredentialDataDecryptedObject, encryptionKey: string): Promise => {}, webhookBaseUrl: 'webhook', webhookTestBaseUrl: 'webhook-test', }; diff --git a/packages/editor-ui/src/components/CredentialsEdit.vue b/packages/editor-ui/src/components/CredentialsEdit.vue index 735c1adfc2..256cf70311 100644 --- a/packages/editor-ui/src/components/CredentialsEdit.vue +++ b/packages/editor-ui/src/components/CredentialsEdit.vue @@ -8,7 +8,7 @@ Credential type: - +
-
+
Credential Data:
- + {{parameter.displayName}}: @@ -97,7 +97,11 @@ import { restApi } from '@/components/mixins/restApi'; import { nodeHelpers } from '@/components/mixins/nodeHelpers'; import { showMessage } from '@/components/mixins/showMessage'; -import { ICredentialsDecryptedResponse, IUpdateInformation } from '@/Interface'; +import { + ICredentialsDecryptedResponse, + ICredentialsResponse, + IUpdateInformation, +} from '@/Interface'; import { CredentialInformation, ICredentialDataDecryptedObject, @@ -105,6 +109,7 @@ import { ICredentialType, ICredentialNodeAccess, INodeCredentialDescription, + INodeProperties, INodeTypeDescription, } from 'n8n-workflow'; @@ -172,15 +177,20 @@ export default mixins( }; }); }, + credentialProperties (): INodeProperties[] { + return this.credentialTypeData.properties.filter((propertyData: INodeProperties) => { + return !this.credentialTypeData.__overwrittenProperties || !this.credentialTypeData.__overwrittenProperties.includes(propertyData.name); + }); + }, isOAuthType (): boolean { - return this.credentialData && this.credentialData.type === 'oAuth2Api'; + return this.credentialTypeData.name === 'oAuth2Api' || (this.credentialTypeData.extends !== undefined && this.credentialTypeData.extends.includes('oAuth2Api')); }, isOAuthConnected (): boolean { if (this.isOAuthType === false) { return false; } - return !!this.credentialData.data.oauthTokenData; + return this.credentialData !== null && !!this.credentialData.data.oauthTokenData; }, }, methods: { @@ -192,7 +202,7 @@ export default mixins( tempValue[name] = parameterData.value; Vue.set(this, 'propertyValue', tempValue); }, - async createCredentials (): Promise { + async createCredentials (doNotEmitData?: boolean): Promise { const nodesAccess = this.nodesAccess.map((nodeType) => { return { nodeType, @@ -211,18 +221,35 @@ export default mixins( result = await this.restApi().createNewCredentials(newCredentials); } catch (error) { this.$showError(error, 'Problem Creating Credentials', 'There was a problem creating the credentials:'); - return; + return null; } // Add also to local store this.$store.commit('addCredentials', result); - this.$emit('credentialsCreated', result); + if (doNotEmitData !== true) { + this.$emit('credentialsCreated', result); + } + + return result; }, async oAuth2CredentialAuthorize () { let url; + + let credentialData = this.credentialData; + let newCredentials = false; + if (!credentialData) { + // Credentials did not get created yet. So create first before + // doing oauth authorize + credentialData = await this.createCredentials(true); + newCredentials = true; + if (credentialData === null) { + return; + } + } + try { - url = await this.restApi().oAuth2CredentialAuthorize(this.credentialData) as string; + url = await this.restApi().oAuth2CredentialAuthorize(credentialData) as string; } catch (error) { this.$showError(error, 'OAuth Authorization Error', 'Error generating authorization URL:'); return; @@ -241,13 +268,17 @@ export default mixins( // Set some kind of data that status changes. // As data does not get displayed directly it does not matter what data. - this.credentialData.data.oauthTokenData = {}; + credentialData.data.oauthTokenData = {}; // Close the window if (oauthPopup) { oauthPopup.close(); } + if (newCredentials === true) { + this.$emit('credentialsCreated', credentialData); + } + this.$showMessage({ title: 'Connected', message: 'Got connected!', diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 0a69f2e53a..6b8f98fe04 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -35,6 +35,27 @@ export interface IGetCredentials { get(type: string, name: string): Promise; } +export abstract class ICredentials { + name: string; + type: string; + data: string | undefined; + nodesAccess: ICredentialNodeAccess[]; + + constructor(name: string, type: string, nodesAccess: ICredentialNodeAccess[], data?: string) { + this.name = name; + this.type = type; + this.nodesAccess = nodesAccess; + this.data = data; + } + + abstract getData(encryptionKey: string, nodeType?: string): ICredentialDataDecryptedObject; + abstract getDataKey(key: string, encryptionKey: string, nodeType?: string): CredentialInformation; + abstract getDataToSave(): ICredentialsEncrypted; + abstract hasNodeAccess(nodeType: string): boolean; + abstract setData(data: ICredentialDataDecryptedObject, encryptionKey: string): void; + abstract setDataKey(key: string, data: CredentialInformation, encryptionKey: string): void; +} + // Defines which nodes are allowed to access the credentials and // when that access got grented from which user export interface ICredentialNodeAccess { @@ -57,11 +78,26 @@ export interface ICredentialsEncrypted { data?: string; } +export abstract class ICredentialsHelper { + encryptionKey: string; + workflowCredentials: IWorkflowCredentials; + + constructor(workflowCredentials: IWorkflowCredentials, encryptionKey: string) { + this.encryptionKey = encryptionKey; + this.workflowCredentials = workflowCredentials; + } + + abstract getCredentials(name: string, type: string): ICredentials; + abstract getDecrypted(name: string, type: string): ICredentialDataDecryptedObject; + abstract updateCredentials(name: string, type: string, data: ICredentialDataDecryptedObject): Promise; +} + export interface ICredentialType { name: string; displayName: string; extends?: string[]; properties: INodeProperties[]; + __overwrittenProperties?: string[]; } export interface ICredentialTypes { @@ -644,6 +680,7 @@ export interface IWorkflowExecuteHooks { export interface IWorkflowExecuteAdditionalData { credentials: IWorkflowCredentials; + credentialsHelper: ICredentialsHelper; encryptionKey: string; executeWorkflow: (workflowInfo: IExecuteWorkflowInfo, additionalData: IWorkflowExecuteAdditionalData, inputData?: INodeExecutionData[]) => Promise; // tslint:disable-line:no-any // hooks?: IWorkflowExecuteHooks; @@ -652,7 +689,6 @@ export interface IWorkflowExecuteAdditionalData { httpRequest?: express.Request; restApiUrl: string; timezone: string; - updateCredentials: (name: string, type: string, data: ICredentialDataDecryptedObject, encryptionKey: string) => Promise; webhookBaseUrl: string; webhookTestBaseUrl: string; currentNodeParameters? : INodeParameters[];