mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-21 02:56:40 -08:00
feat: Add support for preAuthentication and add Metabase credentials (#3399)
* ⚡ Add preAuthentication method to credentials * Improvements * ⚡ Improvements * ⚡ Add feedback * 🔥 Remove comments * ⚡ Add generic type to autheticate method * ⚡ Fix typo * ⚡ Remove console.log and fix indentation * ⚡ Minor improvements * ⚡ Expire credentials in every credential test run Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
parent
f958e6ffab
commit
994c89a6c6
|
@ -37,6 +37,7 @@ import {
|
|||
WorkflowExecuteMode,
|
||||
ITaskDataConnections,
|
||||
LoggerProxy as Logger,
|
||||
IHttpRequestHelper,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
|
@ -140,6 +141,61 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
return requestOptions as IHttpRequestOptions;
|
||||
}
|
||||
|
||||
async preAuthentication(
|
||||
helpers: IHttpRequestHelper,
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
typeName: string,
|
||||
node: INode,
|
||||
credentialsExpired: boolean,
|
||||
): Promise<ICredentialDataDecryptedObject | undefined> {
|
||||
const credentialType = this.credentialTypes.getByName(typeName);
|
||||
|
||||
const expirableProperty = credentialType.properties.find(
|
||||
(property) => property.type === 'hidden' && property?.typeOptions?.expirable === true,
|
||||
);
|
||||
|
||||
if (expirableProperty === undefined || expirableProperty.name === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// check if the node is the mockup node used for testing
|
||||
// if so, it means this is a credential test and not normal node execution
|
||||
const isTestingCredentials =
|
||||
node?.parameters?.temp === '' && node?.type === 'n8n-nodes-base.noOp';
|
||||
|
||||
if (credentialType.preAuthentication) {
|
||||
if (typeof credentialType.preAuthentication === 'function') {
|
||||
// if the expirable property is empty in the credentials
|
||||
// or are expired, call pre authentication method
|
||||
// or the credentials are being tested
|
||||
if (
|
||||
credentials[expirableProperty?.name] === '' ||
|
||||
credentialsExpired ||
|
||||
isTestingCredentials
|
||||
) {
|
||||
const output = await credentialType.preAuthentication.call(helpers, credentials);
|
||||
|
||||
// if there is data in the output, make sure the returned
|
||||
// property is the expirable property
|
||||
// else the database will not get updated
|
||||
if (output[expirableProperty.name] === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (node.credentials) {
|
||||
await this.updateCredentials(
|
||||
node.credentials[credentialType.name],
|
||||
credentialType.name,
|
||||
Object.assign(credentials, output),
|
||||
);
|
||||
return Object.assign(credentials, output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the given value in case it is an expression
|
||||
*/
|
||||
|
@ -538,6 +594,12 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
? nodeType.description.version.slice(-1)[0]
|
||||
: nodeType.description.version,
|
||||
position: [0, 0],
|
||||
credentials: {
|
||||
[credentialType]: {
|
||||
id: credentialsDecrypted.id.toString(),
|
||||
name: credentialsDecrypted.name,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const workflowData = {
|
||||
|
@ -622,7 +684,7 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
} catch (error) {
|
||||
// Do not fail any requests to allow custom error messages and
|
||||
// make logic easier
|
||||
if (error.cause.response) {
|
||||
if (error.cause?.response) {
|
||||
const errorResponseData = {
|
||||
statusCode: error.cause.response.status,
|
||||
statusMessage: error.cause.response.statusText,
|
||||
|
|
|
@ -6,7 +6,12 @@
|
|||
import express from 'express';
|
||||
import { In } from 'typeorm';
|
||||
import { UserSettings, Credentials } from 'n8n-core';
|
||||
import { INodeCredentialTestResult, LoggerProxy } from 'n8n-workflow';
|
||||
import {
|
||||
INodeCredentialsDetails,
|
||||
INodeCredentialTestResult,
|
||||
LoggerProxy,
|
||||
WorkflowExecuteMode,
|
||||
} from 'n8n-workflow';
|
||||
import { getLogger } from '../Logger';
|
||||
|
||||
import {
|
||||
|
@ -17,6 +22,7 @@ import {
|
|||
ICredentialsResponse,
|
||||
whereClause,
|
||||
ResponseHelper,
|
||||
CredentialTypes,
|
||||
} from '..';
|
||||
|
||||
import { RESPONSE_ERROR_MESSAGES } from '../constants';
|
||||
|
@ -130,7 +136,6 @@ credentialsController.post(
|
|||
}
|
||||
|
||||
const helper = new CredentialsHelper(encryptionKey);
|
||||
|
||||
return helper.testCredentials(req.user, credentials.type, credentials, nodeToTestWith);
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -1182,6 +1182,7 @@ export async function httpRequestWithAuthentication(
|
|||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
additionalCredentialOptions?: IAdditionalCredentialOptions,
|
||||
) {
|
||||
let credentialsDecrypted: ICredentialDataDecryptedObject | undefined;
|
||||
try {
|
||||
const parentTypes = additionalData.credentialsHelper.getParentTypes(credentialsType);
|
||||
if (parentTypes.includes('oAuth1Api')) {
|
||||
|
@ -1199,7 +1200,6 @@ export async function httpRequestWithAuthentication(
|
|||
);
|
||||
}
|
||||
|
||||
let credentialsDecrypted: ICredentialDataDecryptedObject | undefined;
|
||||
if (additionalCredentialOptions?.credentialsDecrypted) {
|
||||
credentialsDecrypted = additionalCredentialOptions.credentialsDecrypted.data;
|
||||
} else {
|
||||
|
@ -1213,6 +1213,20 @@ export async function httpRequestWithAuthentication(
|
|||
);
|
||||
}
|
||||
|
||||
const data = await additionalData.credentialsHelper.preAuthentication(
|
||||
{ helpers: { httpRequest: this.helpers.httpRequest } },
|
||||
credentialsDecrypted,
|
||||
credentialsType,
|
||||
node,
|
||||
false,
|
||||
);
|
||||
|
||||
if (data) {
|
||||
// make the updated property in the credentials
|
||||
// available to the authenticate method
|
||||
Object.assign(credentialsDecrypted, data);
|
||||
}
|
||||
|
||||
requestOptions = await additionalData.credentialsHelper.authenticate(
|
||||
credentialsDecrypted,
|
||||
credentialsType,
|
||||
|
@ -1223,6 +1237,45 @@ export async function httpRequestWithAuthentication(
|
|||
);
|
||||
return await httpRequest(requestOptions);
|
||||
} catch (error) {
|
||||
// if there is a pre authorization method defined and
|
||||
// the method failed due to unathorized request
|
||||
if (
|
||||
error.response?.status === 401 &&
|
||||
additionalData.credentialsHelper.preAuthentication !== undefined
|
||||
) {
|
||||
try {
|
||||
if (credentialsDecrypted !== undefined) {
|
||||
// try to refresh the credentials
|
||||
const data = await additionalData.credentialsHelper.preAuthentication(
|
||||
{ helpers: { httpRequest: this.helpers.httpRequest } },
|
||||
credentialsDecrypted,
|
||||
credentialsType,
|
||||
node,
|
||||
true,
|
||||
);
|
||||
|
||||
if (data) {
|
||||
// make the updated property in the credentials
|
||||
// available to the authenticate method
|
||||
Object.assign(credentialsDecrypted, data);
|
||||
}
|
||||
|
||||
requestOptions = await additionalData.credentialsHelper.authenticate(
|
||||
credentialsDecrypted,
|
||||
credentialsType,
|
||||
requestOptions,
|
||||
workflow,
|
||||
node,
|
||||
additionalData.timezone,
|
||||
);
|
||||
}
|
||||
// retry the request
|
||||
return await httpRequest(requestOptions);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error);
|
||||
}
|
||||
}
|
||||
|
||||
throw new NodeApiError(this.getNode(), error);
|
||||
}
|
||||
}
|
||||
|
@ -1303,6 +1356,8 @@ export async function requestWithAuthentication(
|
|||
additionalData: IWorkflowExecuteAdditionalData,
|
||||
additionalCredentialOptions?: IAdditionalCredentialOptions,
|
||||
) {
|
||||
let credentialsDecrypted: ICredentialDataDecryptedObject | undefined;
|
||||
|
||||
try {
|
||||
const parentTypes = additionalData.credentialsHelper.getParentTypes(credentialsType);
|
||||
|
||||
|
@ -1321,7 +1376,6 @@ export async function requestWithAuthentication(
|
|||
);
|
||||
}
|
||||
|
||||
let credentialsDecrypted: ICredentialDataDecryptedObject | undefined;
|
||||
if (additionalCredentialOptions?.credentialsDecrypted) {
|
||||
credentialsDecrypted = additionalCredentialOptions.credentialsDecrypted.data;
|
||||
} else {
|
||||
|
@ -1335,6 +1389,20 @@ export async function requestWithAuthentication(
|
|||
);
|
||||
}
|
||||
|
||||
const data = await additionalData.credentialsHelper.preAuthentication(
|
||||
{ helpers: { httpRequest: this.helpers.httpRequest } },
|
||||
credentialsDecrypted,
|
||||
credentialsType,
|
||||
node,
|
||||
false,
|
||||
);
|
||||
|
||||
if (data) {
|
||||
// make the updated property in the credentials
|
||||
// available to the authenticate method
|
||||
Object.assign(credentialsDecrypted, data);
|
||||
}
|
||||
|
||||
requestOptions = await additionalData.credentialsHelper.authenticate(
|
||||
credentialsDecrypted,
|
||||
credentialsType,
|
||||
|
@ -1346,7 +1414,37 @@ export async function requestWithAuthentication(
|
|||
|
||||
return await proxyRequestToAxios(requestOptions as IDataObject);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error);
|
||||
try {
|
||||
if (credentialsDecrypted !== undefined) {
|
||||
// try to refresh the credentials
|
||||
const data = await additionalData.credentialsHelper.preAuthentication(
|
||||
{ helpers: { httpRequest: this.helpers.httpRequest } },
|
||||
credentialsDecrypted,
|
||||
credentialsType,
|
||||
node,
|
||||
true,
|
||||
);
|
||||
|
||||
if (data) {
|
||||
// make the updated property in the credentials
|
||||
// available to the authenticate method
|
||||
Object.assign(credentialsDecrypted, data);
|
||||
}
|
||||
|
||||
requestOptions = await additionalData.credentialsHelper.authenticate(
|
||||
credentialsDecrypted,
|
||||
credentialsType,
|
||||
requestOptions as IHttpRequestOptions,
|
||||
workflow,
|
||||
node,
|
||||
additionalData.timezone,
|
||||
);
|
||||
}
|
||||
// retry the request
|
||||
return await proxyRequestToAxios(requestOptions as IDataObject);
|
||||
} catch (error) {
|
||||
throw new NodeApiError(this.getNode(), error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,9 @@ import {
|
|||
IDataObject,
|
||||
IDeferredPromise,
|
||||
IExecuteWorkflowInfo,
|
||||
IHttpRequestHelper,
|
||||
IHttpRequestOptions,
|
||||
INode,
|
||||
INodeCredentialsDetails,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
|
@ -33,6 +35,17 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
return requestParams;
|
||||
}
|
||||
|
||||
async preAuthentication(
|
||||
helpers: IHttpRequestHelper,
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
typeName: string,
|
||||
node: INode,
|
||||
credentialsExpired: boolean,
|
||||
): Promise<ICredentialDataDecryptedObject | undefined> {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
||||
getParentTypes(name: string): string[] {
|
||||
return [];
|
||||
}
|
||||
|
|
|
@ -639,10 +639,11 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
|
||||
if (this.isCredentialTestable) {
|
||||
this.isTesting = true;
|
||||
|
||||
// Add the full data including defaults for testing
|
||||
credentialDetails.data = this.credentialData;
|
||||
|
||||
credentialDetails.id = this.credentialId;
|
||||
|
||||
await this.testCredential(credentialDetails);
|
||||
this.isTesting = false;
|
||||
}
|
||||
|
|
76
packages/nodes-base/credentials/MetabaseApi.credentials.ts
Normal file
76
packages/nodes-base/credentials/MetabaseApi.credentials.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import {
|
||||
IAuthenticateGeneric,
|
||||
ICredentialDataDecryptedObject,
|
||||
ICredentialTestRequest,
|
||||
ICredentialType,
|
||||
IHttpRequestHelper,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class MetabaseApi implements ICredentialType {
|
||||
name = 'metabaseApi';
|
||||
displayName = 'Metabase API';
|
||||
documentationUrl = 'metabase';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Session Token',
|
||||
name: 'sessionToken',
|
||||
type: 'hidden',
|
||||
typeOptions: {
|
||||
expirable: true,
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'URL',
|
||||
name: 'url',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Username',
|
||||
name: 'username',
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Password',
|
||||
name: 'password',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
password: true,
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
// method will only be called if "sessionToken" (the expirable property)
|
||||
// is empty or is expired
|
||||
async preAuthentication(this: IHttpRequestHelper, credentials: ICredentialDataDecryptedObject) {
|
||||
// make reques to get session token
|
||||
const url = credentials.url as string;
|
||||
const { id } = (await this.helpers.httpRequest({
|
||||
method: 'POST',
|
||||
url: `${url.endsWith('/') ? url.slice(0, -1) : url}/api/session`,
|
||||
body: {
|
||||
username: credentials.username,
|
||||
password: credentials.password,
|
||||
},
|
||||
})) as { id: string };
|
||||
return { sessionToken: id };
|
||||
}
|
||||
authenticate: IAuthenticateGeneric = {
|
||||
type: 'generic',
|
||||
properties: {
|
||||
headers: {
|
||||
'X-Metabase-Session': '={{$credentials.sessionToken}}',
|
||||
},
|
||||
},
|
||||
};
|
||||
test: ICredentialTestRequest = {
|
||||
request: {
|
||||
baseURL: '={{$credentials?.url}}',
|
||||
url: '/api/user/current',
|
||||
},
|
||||
};
|
||||
}
|
|
@ -183,6 +183,7 @@
|
|||
"dist/credentials/MediumApi.credentials.js",
|
||||
"dist/credentials/MediumOAuth2Api.credentials.js",
|
||||
"dist/credentials/MessageBirdApi.credentials.js",
|
||||
"dist/credentials/MetabaseApi.credentials.js",
|
||||
"dist/credentials/MicrosoftDynamicsOAuth2Api.credentials.js",
|
||||
"dist/credentials/MicrosoftExcelOAuth2Api.credentials.js",
|
||||
"dist/credentials/MicrosoftGraphSecurityOAuth2Api.credentials.js",
|
||||
|
|
|
@ -166,6 +166,9 @@ export interface IRequestOptionsSimplifiedAuth {
|
|||
skipSslCertificateValidation?: boolean | string;
|
||||
}
|
||||
|
||||
export interface IHttpRequestHelper {
|
||||
helpers: { httpRequest: IAllExecuteFunctions['helpers']['httpRequest'] };
|
||||
}
|
||||
export abstract class ICredentialsHelper {
|
||||
encryptionKey: string;
|
||||
|
||||
|
@ -184,6 +187,14 @@ export abstract class ICredentialsHelper {
|
|||
defaultTimezone: string,
|
||||
): Promise<IHttpRequestOptions>;
|
||||
|
||||
abstract preAuthentication(
|
||||
helpers: IHttpRequestHelper,
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
typeName: string,
|
||||
node: INode,
|
||||
credentialsExpired: boolean,
|
||||
): Promise<ICredentialDataDecryptedObject | undefined>;
|
||||
|
||||
abstract getCredentials(
|
||||
nodeCredentials: INodeCredentialsDetails,
|
||||
type: string,
|
||||
|
@ -269,6 +280,10 @@ export interface ICredentialType {
|
|||
documentationUrl?: string;
|
||||
__overwrittenProperties?: string[];
|
||||
authenticate?: IAuthenticate;
|
||||
preAuthentication?: (
|
||||
this: IHttpRequestHelper,
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
) => Promise<IDataObject>;
|
||||
test?: ICredentialTestRequest;
|
||||
genericAuth?: boolean;
|
||||
}
|
||||
|
@ -894,6 +909,7 @@ export interface INodePropertyTypeOptions {
|
|||
rows?: number; // Supported by: string
|
||||
showAlpha?: boolean; // Supported by: color
|
||||
sortable?: boolean; // Supported when "multipleValues" set to true
|
||||
expirable?: boolean; // Supported by: hidden (only in the credentials)
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
IExecuteResponsePromiseData,
|
||||
IExecuteSingleFunctions,
|
||||
IExecuteWorkflowInfo,
|
||||
IHttpRequestHelper,
|
||||
IHttpRequestOptions,
|
||||
IN8nHttpFullResponse,
|
||||
IN8nHttpResponse,
|
||||
|
@ -111,6 +112,16 @@ export class CredentialsHelper extends ICredentialsHelper {
|
|||
return requestParams;
|
||||
}
|
||||
|
||||
async preAuthentication(
|
||||
helpers: IHttpRequestHelper,
|
||||
credentials: ICredentialDataDecryptedObject,
|
||||
typeName: string,
|
||||
node: INode,
|
||||
credentialsExpired: boolean,
|
||||
): Promise<{ updatedCredentials: boolean; data: ICredentialDataDecryptedObject }> {
|
||||
return { updatedCredentials: false, data: {} }
|
||||
};
|
||||
|
||||
getParentTypes(name: string): string[] {
|
||||
return [];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue