Nodes as JSON and authentication redesign (#2401)

*  change FE to handle new object type

* 🚸 improve UX of handling invalid credentials

* 🚧 WIP

* 🎨 fix typescript issues

* 🐘 add migrations for all supported dbs

* ✏️ add description to migrations

*  add credential update on import

*  resolve after merge issues

* 👕 fix lint issues

*  check credentials on workflow create/update

* update interface

* 👕 fix ts issues

*  adaption to new credentials UI

* 🐛 intialize cache on BE for credentials check

* 🐛 fix undefined oldCredentials

* 🐛 fix deleting credential

* 🐛 fix check for undefined keys

* 🐛 fix disabling edit in execution

* 🎨 just show credential name on execution view

* ✏️  remove TODO

*  implement review suggestions

*  add cache to getCredentialsByType

*  use getter instead of cache

* ✏️ fix variable name typo

* 🐘 include waiting nodes to migrations

* 🐛 fix reverting migrations command

*  update typeorm command

*  create db:revert command

* 👕 fix lint error

*  Add optional authenticate method to credentials

*  Simplify code and add authentication support to MattermostApi

* 👕 Fix lint issue

*  Add support to own-mode

* 👕 Fix lint issue

*  Add support for predefined auth types bearer and headerAuth

*  Make sure that DateTime Node always returns strings

*  Add support for moment types to If Node

*  Make it possible for HTTP Request Node to use all credential types

*  Add basicAuth support

* Add a new dropcontact node

*  First basic implementation of mainly JSON based nodes

*  Add fixedCollection support, added value parameter and
expression support for value and property

* Improvements to #2389

*  Add credentials verification

*  Small improvement

*  set default time to 45 seconds

*  Add support for preSend and postReceive methods

*  Add lodash merge and set depedency to workflow

* 👕 Fix lint issue

*  Improvements

*  Improvements

*  Improvements

*  Improvements

*  Improvements

* 🐛 Set siren and language correctly

*  Add support for requestDefaults

*  Add support for baseURL to httpRequest

*  Move baseURL to correct location

*  Add support for options loading

* 🐛 Fix error with fullAccess nodes

*  Add credential test functionality

* 🐛 Fix issue with OAuth autentication and lint issue

*  Fix build issue

* 🐛 Fix issue that url got always overwritten to empty

*  Add pagination support

*  Code fix required after merge

*  Remove not needed imports

*  Fix credential test

*  Add expression support for request properties and $self
support on properties

*  Rename $self to $value

* 👕 Fix lint issue

*  Add example how to send data in path

*  Make it possible to not sent in dot notation

*  Add support for postReceive:rootProperty

*  Fix typo

*  Add support for postReceive:set

*  Some fixes

*  Small improvement

* ;zap: Separate RoutingNode code

*  Simplify code and fix bug

*  Remove unused code

*  Make it possible to define "request" and "requestProperty" on
options

* 👕 Fix lint issue

*  Change $credentials variables name

*  Enable expressions and access to credentials in requestDefaults

*  Make parameter option loading use RoutingNode.makeRoutingRequest

*  Allow requestOperations overwrite on LoadOptions

*  Make it possible to access current node parameters in loadOptions

*  Rename parameters variable to make future proof

*  Make it possible to use offset-pagination with body

*  Add support for queryAuth

*  Never return more items than requested

*  Make it possible to overwrite requestOperations on parameter
and option level

* 👕 Fix lint issue

*  Allow simplified auth also with regular nodes

*  Add support for receiving binary data

* 🐛 Fix example node

*  Rename property "name" to "displayName" in loadOptions

*  Send data by default as "query" if nothing is set

*  Rename $self to $parent

*  Change to work with INodeExecutionData instead of IDataObject

*  Improve binaryData handling

*  Property design improvements

*  Fix property name

* 🚨 Add some tests

*  Add also test for request

*  Improve test and fix issues

*  Improvements to loadOptions

*  Normalize loadOptions with rest of code

*  Add info text

*  Add support for $value in postReceive

* 🚨 Add tests for RoutingNode.runNode

*  Remove TODOs and make url property optional

*  Fix bug and lint issue

* 🐛 Fix bug that not the correct property got used

* 🚨 Add tests for CredentialsHelper.authenticate

*  Improve code and resolve expressions also everywhere for
loadOptions and credential test requests

*  Make it possible to define multiple preSend and postReceive
actions

*  Allow to define tests on credentials

*  Remove test data

* ⬆️ Update package-lock.json file

*  Remove old not longer used code

Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
Co-authored-by: Mutasem <mutdmour@gmail.com>
Co-authored-by: PaulineDropcontact <pauline@dropcontact.io>
Co-authored-by: ricardo <ricardoespinoza105@gmail.com>
This commit is contained in:
Jan Oberhauser
2022-02-05 22:55:43 +01:00
committed by GitHub
parent f23098e38b
commit 0da398b0e4
66 changed files with 5074 additions and 360 deletions

View File

@@ -15,6 +15,7 @@
/* eslint-disable no-param-reassign */
import {
GenericValue,
IAdditionalCredentialOptions,
IAllExecuteFunctions,
IBinaryData,
IContextObject,
@@ -29,6 +30,8 @@ import {
IN8nHttpFullResponse,
IN8nHttpResponse,
INode,
INodeCredentialDescription,
INodeCredentialsDetails,
INodeExecutionData,
INodeParameters,
INodeType,
@@ -44,6 +47,7 @@ import {
IWorkflowDataProxyData,
IWorkflowExecuteAdditionalData,
IWorkflowMetadata,
NodeApiError,
NodeHelpers,
NodeOperationError,
NodeParameterValue,
@@ -676,6 +680,10 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest
axiosRequest.params = n8nRequest.qs;
if (n8nRequest.baseURL !== undefined) {
axiosRequest.baseURL = n8nRequest.baseURL;
}
if (n8nRequest.disableFollowRedirect === true) {
axiosRequest.maxRedirects = 0;
}
@@ -733,12 +741,11 @@ function convertN8nRequestToAxios(n8nRequest: IHttpRequestOptions): AxiosRequest
}
async function httpRequest(
requestParams: IHttpRequestOptions,
requestOptions: IHttpRequestOptions,
): Promise<IN8nHttpFullResponse | IN8nHttpResponse> {
// tslint:disable-line:no-any
const axiosRequest = convertN8nRequestToAxios(requestParams);
const axiosRequest = convertN8nRequestToAxios(requestOptions);
const result = await axios(axiosRequest);
if (requestParams.returnFullResponse) {
if (requestOptions.returnFullResponse) {
return {
body: result.data,
headers: result.headers,
@@ -853,10 +860,11 @@ export async function prepareBinaryData(
export async function requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions | IHttpRequestOptions,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
oAuth2Options?: IOAuth2Options,
isN8nRequest = false,
) {
const credentials = (await this.getCredentials(
credentialsType,
@@ -952,7 +960,9 @@ export async function requestOAuth2(
// Make the request again with the new token
const newRequestOptions = newToken.sign(requestOptions as clientOAuth2.RequestObject);
if (isN8nRequest) {
return this.helpers.httpRequest(newRequestOptions);
}
return this.helpers.request!(newRequestOptions);
}
@@ -972,7 +982,12 @@ export async function requestOAuth2(
export async function requestOAuth1(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUrl | OptionsWithUri | requestPromise.RequestPromiseOptions,
requestOptions:
| OptionsWithUrl
| OptionsWithUri
| requestPromise.RequestPromiseOptions
| IHttpRequestOptions,
isN8nRequest = false,
) {
const credentials = (await this.getCredentials(
credentialsType,
@@ -1020,12 +1035,71 @@ export async function requestOAuth1(
// @ts-ignore
requestOptions.headers = oauth.toHeader(oauth.authorize(requestOptions, token));
if (isN8nRequest) {
return this.helpers.httpRequest(requestOptions as IHttpRequestOptions);
}
return this.helpers.request!(requestOptions).catch(async (error: IResponseError) => {
// Unknown error so simply throw it
throw error;
});
}
export async function httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
additionalCredentialOptions?: IAdditionalCredentialOptions,
) {
try {
const parentTypes = additionalData.credentialsHelper.getParentTypes(credentialsType);
if (parentTypes.includes('oAuth1Api')) {
return await requestOAuth1.call(this, credentialsType, requestOptions, true);
}
if (parentTypes.includes('oAuth2Api')) {
return await requestOAuth2.call(
this,
credentialsType,
requestOptions,
node,
additionalData,
additionalCredentialOptions?.oauth2,
true,
);
}
let credentialsDecrypted: ICredentialDataDecryptedObject | undefined;
if (additionalCredentialOptions?.credentialsDecrypted) {
credentialsDecrypted = additionalCredentialOptions.credentialsDecrypted.data;
} else {
credentialsDecrypted = await this.getCredentials(credentialsType);
}
if (credentialsDecrypted === undefined) {
throw new NodeOperationError(
node,
`Node "${node.name}" does not have any credentials of type "${credentialsType}" set!`,
);
}
requestOptions = await additionalData.credentialsHelper.authenticate(
credentialsDecrypted,
credentialsType,
requestOptions,
workflow,
node,
);
return await httpRequest(requestOptions);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
/**
* Takes generic input data and brings it into the json format n8n uses.
*
@@ -1047,6 +1121,62 @@ export function returnJsonArray(jsonData: IDataObject | IDataObject[]): INodeExe
return returnData;
}
// TODO: Move up later
export async function requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
workflow: Workflow,
node: INode,
additionalData: IWorkflowExecuteAdditionalData,
additionalCredentialOptions?: IAdditionalCredentialOptions,
) {
try {
const parentTypes = additionalData.credentialsHelper.getParentTypes(credentialsType);
if (parentTypes.includes('oAuth1Api')) {
return await requestOAuth1.call(this, credentialsType, requestOptions, false);
}
if (parentTypes.includes('oAuth2Api')) {
return await requestOAuth2.call(
this,
credentialsType,
requestOptions,
node,
additionalData,
additionalCredentialOptions?.oauth2,
false,
);
}
let credentialsDecrypted: ICredentialDataDecryptedObject | undefined;
if (additionalCredentialOptions?.credentialsDecrypted) {
credentialsDecrypted = additionalCredentialOptions.credentialsDecrypted.data;
} else {
credentialsDecrypted = await this.getCredentials(credentialsType);
}
if (credentialsDecrypted === undefined) {
throw new NodeOperationError(
node,
`Node "${node.name}" does not have any credentials of type "${credentialsType}" set!`,
);
}
requestOptions = await additionalData.credentialsHelper.authenticate(
credentialsDecrypted,
credentialsType,
requestOptions as IHttpRequestOptions,
workflow,
node,
);
return await proxyRequestToAxios(requestOptions as IDataObject);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
}
}
/**
* Returns the additional keys for Expressions and Function-Nodes
*
@@ -1094,39 +1224,46 @@ export async function getCredentials(
);
}
if (nodeType.description.credentials === undefined) {
throw new NodeOperationError(
node,
`Node type "${node.type}" does not have any credentials defined!`,
);
}
// Hardcode for now for security reasons that only a single node can access
// all credentials
const fullAccess = ['n8n-nodes-base.httpRequest'].includes(node.type);
const nodeCredentialDescription = nodeType.description.credentials.find(
(credentialTypeDescription) => credentialTypeDescription.name === type,
);
if (nodeCredentialDescription === undefined) {
throw new NodeOperationError(
node,
`Node type "${node.type}" does not have any credentials of type "${type}" defined!`,
);
}
let nodeCredentialDescription: INodeCredentialDescription | undefined;
if (!fullAccess) {
if (nodeType.description.credentials === undefined) {
throw new NodeOperationError(
node,
`Node type "${node.type}" does not have any credentials defined!`,
);
}
if (
!NodeHelpers.displayParameter(
additionalData.currentNodeParameters || node.parameters,
nodeCredentialDescription,
node.parameters,
)
) {
// Credentials should not be displayed so return undefined even if they would be defined
return undefined;
nodeCredentialDescription = nodeType.description.credentials.find(
(credentialTypeDescription) => credentialTypeDescription.name === type,
);
if (nodeCredentialDescription === undefined) {
throw new NodeOperationError(
node,
`Node type "${node.type}" does not have any credentials of type "${type}" defined!`,
);
}
if (
!NodeHelpers.displayParameter(
additionalData.currentNodeParameters || node.parameters,
nodeCredentialDescription,
node.parameters,
)
) {
// Credentials should not be displayed so return undefined even if they would be defined
return undefined;
}
}
// Check if node has any credentials defined
if (!node.credentials || !node.credentials[type]) {
if (!fullAccess && (!node.credentials || !node.credentials[type])) {
// If none are defined check if the credentials are required or not
if (nodeCredentialDescription.required === true) {
if (nodeCredentialDescription?.required === true) {
// Credentials are required so error
if (!node.credentials) {
throw new NodeOperationError(node, 'Node does not have any credentials set!');
@@ -1140,6 +1277,12 @@ export async function getCredentials(
}
}
if (fullAccess && (!node.credentials || !node.credentials[type])) {
// Make sure that fullAccess nodes still behave like before that if they
// request access to credentials that are currently not set it returns undefined
return undefined;
}
let expressionResolveValues: ICredentialsExpressionResolveValues | undefined;
if (connectionInputData && runExecutionData && runIndex !== undefined) {
expressionResolveValues = {
@@ -1152,7 +1295,9 @@ export async function getCredentials(
} as ICredentialsExpressionResolveValues;
}
const nodeCredentials = node.credentials[type];
const nodeCredentials = node.credentials
? node.credentials[type]
: ({} as INodeCredentialsDetails);
// TODO: solve using credentials via expression
// if (name.charAt(0) === '=') {
@@ -1466,6 +1611,22 @@ export function getExecutePollFunctions(
);
},
request: proxyRequestToAxios,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
async requestOAuth2(
this: IAllExecuteFunctions,
credentialsType: string,
@@ -1488,6 +1649,22 @@ export function getExecutePollFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
returnJsonArray,
},
};
@@ -1570,6 +1747,22 @@ export function getExecuteTriggerFunctions(
},
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
async prepareBinaryData(
binaryData: Buffer,
filePath?: string,
@@ -1606,6 +1799,22 @@ export function getExecuteTriggerFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
returnJsonArray,
},
};
@@ -1784,6 +1993,22 @@ export function getExecuteFunctions(
},
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
async prepareBinaryData(
binaryData: Buffer,
filePath?: string,
@@ -1827,6 +2052,22 @@ export function getExecuteFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
returnJsonArray,
},
};
@@ -1977,6 +2218,22 @@ export function getExecuteSingleFunctions(
},
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
async prepareBinaryData(
binaryData: Buffer,
filePath?: string,
@@ -2013,6 +2270,22 @@ export function getExecuteSingleFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
},
};
})(workflow, runExecutionData, connectionInputData, inputData, node, itemIndex);
@@ -2104,6 +2377,22 @@ export function getLoadOptionsFunctions(
},
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
request: proxyRequestToAxios,
async requestOAuth2(
this: IAllExecuteFunctions,
@@ -2127,6 +2416,22 @@ export function getLoadOptionsFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
},
};
return that;
@@ -2224,6 +2529,22 @@ export function getExecuteHookFunctions(
},
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
request: proxyRequestToAxios,
async requestOAuth2(
this: IAllExecuteFunctions,
@@ -2247,6 +2568,22 @@ export function getExecuteHookFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
},
};
return that;
@@ -2370,6 +2707,22 @@ export function getExecuteWebhookFunctions(
prepareOutputData: NodeHelpers.prepareOutputData,
helpers: {
httpRequest,
async requestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: OptionsWithUri | requestPromise.RequestPromiseOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return requestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
async prepareBinaryData(
binaryData: Buffer,
filePath?: string,
@@ -2406,6 +2759,22 @@ export function getExecuteWebhookFunctions(
): Promise<any> {
return requestOAuth1.call(this, credentialsType, requestOptions);
},
async httpRequestWithAuthentication(
this: IAllExecuteFunctions,
credentialsType: string,
requestOptions: IHttpRequestOptions,
additionalCredentialOptions?: IAdditionalCredentialOptions,
): Promise<any> {
return httpRequestWithAuthentication.call(
this,
credentialsType,
requestOptions,
workflow,
node,
additionalData,
additionalCredentialOptions,
);
},
returnJsonArray,
},
};