mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge pull request #3164 from n8n-io/n8n-3369-provide-scopes
Provide scopes for custom node operations
This commit is contained in:
commit
f8e992c453
|
@ -3,6 +3,7 @@ import {
|
||||||
ICredentialTypeData,
|
ICredentialTypeData,
|
||||||
ICredentialTypes as ICredentialTypesInterface,
|
ICredentialTypes as ICredentialTypesInterface,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
import { RESPONSE_ERROR_MESSAGES } from './constants';
|
||||||
|
|
||||||
class CredentialTypesClass implements ICredentialTypesInterface {
|
class CredentialTypesClass implements ICredentialTypesInterface {
|
||||||
credentialTypes: ICredentialTypeData = {};
|
credentialTypes: ICredentialTypeData = {};
|
||||||
|
@ -16,7 +17,11 @@ class CredentialTypesClass implements ICredentialTypesInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
getByName(credentialType: string): ICredentialType {
|
getByName(credentialType: string): ICredentialType {
|
||||||
return this.credentialTypes[credentialType].type;
|
try {
|
||||||
|
return this.credentialTypes[credentialType].type;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL}: ${credentialType}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -286,6 +286,33 @@ export class CredentialsHelper extends ICredentialsHelper {
|
||||||
return combineProperties;
|
return combineProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the scope of a credential type
|
||||||
|
*
|
||||||
|
* @param {string} type
|
||||||
|
* @returns {string[]}
|
||||||
|
* @memberof CredentialsHelper
|
||||||
|
*/
|
||||||
|
getScopes(type: string): string[] {
|
||||||
|
const scopeProperty = this.getCredentialsProperties(type).find(({ name }) => name === 'scope');
|
||||||
|
|
||||||
|
if (!scopeProperty?.default || typeof scopeProperty.default !== 'string') {
|
||||||
|
const errorMessage = `No \`scope\` property found for credential type: ${type}`;
|
||||||
|
|
||||||
|
Logger.error(errorMessage);
|
||||||
|
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { default: scopeDefault } = scopeProperty;
|
||||||
|
|
||||||
|
if (/ /.test(scopeDefault)) return scopeDefault.split(' ');
|
||||||
|
|
||||||
|
if (/,/.test(scopeDefault)) return scopeDefault.split(',');
|
||||||
|
|
||||||
|
return [scopeDefault];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the decrypted credential data with applied overwrites
|
* Returns the decrypted credential data with applied overwrites
|
||||||
*
|
*
|
||||||
|
|
|
@ -58,6 +58,7 @@ class LoadNodesAndCredentialsClass {
|
||||||
// In case "n8n" package is the root and the packages are
|
// In case "n8n" package is the root and the packages are
|
||||||
// in the "node_modules" folder underneath it.
|
// in the "node_modules" folder underneath it.
|
||||||
path.join(__dirname, '..', '..', 'node_modules', 'n8n-workflow'),
|
path.join(__dirname, '..', '..', 'node_modules', 'n8n-workflow'),
|
||||||
|
path.join(__dirname, '..', 'node_modules', 'n8n-workflow'), // for test run
|
||||||
];
|
];
|
||||||
for (const checkPath of checkPaths) {
|
for (const checkPath of checkPaths) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -169,6 +169,7 @@ import { ExecutionEntity } from './databases/entities/ExecutionEntity';
|
||||||
import { SharedWorkflow } from './databases/entities/SharedWorkflow';
|
import { SharedWorkflow } from './databases/entities/SharedWorkflow';
|
||||||
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
|
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
|
||||||
import { credentialsController } from './api/credentials.api';
|
import { credentialsController } from './api/credentials.api';
|
||||||
|
import { oauth2CredentialController } from './api/oauth2Credential.api';
|
||||||
import { getInstanceBaseUrl, isEmailSetUp } from './UserManagement/UserManagementHelper';
|
import { getInstanceBaseUrl, isEmailSetUp } from './UserManagement/UserManagementHelper';
|
||||||
|
|
||||||
require('body-parser-xml')(bodyParser);
|
require('body-parser-xml')(bodyParser);
|
||||||
|
@ -1933,6 +1934,8 @@ class App {
|
||||||
// OAuth2-Credential/Auth
|
// OAuth2-Credential/Auth
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
|
this.app.use(`/${this.restEndpoint}/oauth2-credential`, oauth2CredentialController);
|
||||||
|
|
||||||
// Authorize OAuth Data
|
// Authorize OAuth Data
|
||||||
this.app.get(
|
this.app.get(
|
||||||
`/${this.restEndpoint}/oauth2-credential/auth`,
|
`/${this.restEndpoint}/oauth2-credential/auth`,
|
||||||
|
|
61
packages/cli/src/api/oauth2Credential.api.ts
Normal file
61
packages/cli/src/api/oauth2Credential.api.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/* eslint-disable import/no-cycle */
|
||||||
|
import express from 'express';
|
||||||
|
import { UserSettings } from 'n8n-core';
|
||||||
|
import { LoggerProxy } from 'n8n-workflow';
|
||||||
|
import { ResponseHelper } from '..';
|
||||||
|
import { RESPONSE_ERROR_MESSAGES } from '../constants';
|
||||||
|
import { CredentialsHelper } from '../CredentialsHelper';
|
||||||
|
import { getLogger } from '../Logger';
|
||||||
|
import { OAuthRequest } from '../requests';
|
||||||
|
|
||||||
|
export const oauth2CredentialController = express.Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize Logger if needed
|
||||||
|
*/
|
||||||
|
oauth2CredentialController.use((req, res, next) => {
|
||||||
|
try {
|
||||||
|
LoggerProxy.getInstance();
|
||||||
|
} catch (error) {
|
||||||
|
LoggerProxy.init(getLogger());
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /oauth2-credential/scopes
|
||||||
|
*/
|
||||||
|
oauth2CredentialController.get(
|
||||||
|
'/scopes',
|
||||||
|
ResponseHelper.send(async (req: OAuthRequest.OAuth2Credential.Scopes): Promise<string[]> => {
|
||||||
|
const { credentialType: type } = req.query;
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
LoggerProxy.debug(
|
||||||
|
'Request for OAuth2 credential scopes failed because of missing credential type in query string',
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new ResponseHelper.ResponseError(
|
||||||
|
RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL_TYPE,
|
||||||
|
undefined,
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!type.endsWith('OAuth2Api')) {
|
||||||
|
LoggerProxy.debug(
|
||||||
|
'Request for OAuth2 credential scopes failed because requested credential type is not OAuth2',
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new ResponseHelper.ResponseError(
|
||||||
|
RESPONSE_ERROR_MESSAGES.CREDENTIAL_TYPE_NOT_OAUTH2,
|
||||||
|
undefined,
|
||||||
|
400,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptionKey = await UserSettings.getEncryptionKey();
|
||||||
|
|
||||||
|
return new CredentialsHelper(encryptionKey).getScopes(type);
|
||||||
|
}),
|
||||||
|
);
|
|
@ -7,6 +7,8 @@ import { RESPONSE_ERROR_MESSAGES as CORE_RESPONSE_ERROR_MESSAGES } from 'n8n-cor
|
||||||
export const RESPONSE_ERROR_MESSAGES = {
|
export const RESPONSE_ERROR_MESSAGES = {
|
||||||
NO_CREDENTIAL: 'Credential not found',
|
NO_CREDENTIAL: 'Credential not found',
|
||||||
NO_ENCRYPTION_KEY: CORE_RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
NO_ENCRYPTION_KEY: CORE_RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY,
|
||||||
|
NO_CREDENTIAL_TYPE: 'Missing query string param: `credentialType`',
|
||||||
|
CREDENTIAL_TYPE_NOT_OAUTH2: 'Credential type is not OAuth2 - no scopes can be provided',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AUTH_COOKIE_NAME = 'n8n-auth';
|
export const AUTH_COOKIE_NAME = 'n8n-auth';
|
||||||
|
|
5
packages/cli/src/requests.d.ts
vendored
5
packages/cli/src/requests.d.ts
vendored
|
@ -244,9 +244,8 @@ export declare namespace OAuthRequest {
|
||||||
|
|
||||||
namespace OAuth2Credential {
|
namespace OAuth2Credential {
|
||||||
type Auth = OAuth1Credential.Auth;
|
type Auth = OAuth1Credential.Auth;
|
||||||
type Callback = AuthenticatedRequest<{}, {}, {}, { code: string; state: string }> & {
|
type Callback = AuthenticatedRequest<{}, {}, {}, { code: string; state: string }>;
|
||||||
user?: User;
|
type Scopes = AuthenticatedRequest<{}, {}, {}, { credentialType: string }>;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
125
packages/cli/test/integration/oauth2.api.test.ts
Normal file
125
packages/cli/test/integration/oauth2.api.test.ts
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import * as utils from './shared/utils';
|
||||||
|
import * as testDb from './shared/testDb';
|
||||||
|
import { RESPONSE_ERROR_MESSAGES } from '../../src/constants';
|
||||||
|
|
||||||
|
import type { Role } from '../../src/databases/entities/Role';
|
||||||
|
import { CredentialTypes, LoadNodesAndCredentials } from '../../src';
|
||||||
|
|
||||||
|
const SCOPES_ENDPOINT = '/oauth2-credential/scopes';
|
||||||
|
|
||||||
|
let app: express.Application;
|
||||||
|
let testDbName = '';
|
||||||
|
let globalOwnerRole: Role;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = utils.initTestServer({ endpointGroups: ['oauth2-credential'], applyAuth: true });
|
||||||
|
const initResult = await testDb.init();
|
||||||
|
testDbName = initResult.testDbName;
|
||||||
|
|
||||||
|
utils.initConfigFile();
|
||||||
|
|
||||||
|
globalOwnerRole = await testDb.getGlobalOwnerRole();
|
||||||
|
utils.initTestLogger();
|
||||||
|
|
||||||
|
const loadNodesAndCredentials = LoadNodesAndCredentials();
|
||||||
|
await loadNodesAndCredentials.init();
|
||||||
|
|
||||||
|
const credentialTypes = CredentialTypes();
|
||||||
|
await credentialTypes.init(loadNodesAndCredentials.credentialTypes);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await testDb.terminate(testDbName);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OAuth2 scopes', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await testDb.truncate(['User'], testDbName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`GET ${SCOPES_ENDPOINT} should return scopes - comma-delimited`, async () => {
|
||||||
|
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||||
|
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||||
|
|
||||||
|
const response = await authOwnerShellAgent
|
||||||
|
.get(SCOPES_ENDPOINT)
|
||||||
|
.query({ credentialType: 'twistOAuth2Api' });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
const scopes = response.body.data;
|
||||||
|
const TWIST_OAUTH2_API_SCOPES_TOTAL = 6;
|
||||||
|
|
||||||
|
expect(Array.isArray(scopes)).toBe(true);
|
||||||
|
expect(scopes.length).toBe(TWIST_OAUTH2_API_SCOPES_TOTAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`GET ${SCOPES_ENDPOINT} should return scopes - whitespace-delimited`, async () => {
|
||||||
|
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||||
|
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||||
|
|
||||||
|
const response = await authOwnerShellAgent
|
||||||
|
.get(SCOPES_ENDPOINT)
|
||||||
|
.query({ credentialType: 'dropboxOAuth2Api' });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
const scopes = response.body.data;
|
||||||
|
const DROPBOX_OAUTH2_API_SCOPES_TOTAL = 4;
|
||||||
|
|
||||||
|
expect(Array.isArray(scopes)).toBe(true);
|
||||||
|
expect(scopes.length).toBe(DROPBOX_OAUTH2_API_SCOPES_TOTAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`GET ${SCOPES_ENDPOINT} should return scope - non-delimited`, async () => {
|
||||||
|
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||||
|
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||||
|
|
||||||
|
const response = await authOwnerShellAgent
|
||||||
|
.get(SCOPES_ENDPOINT)
|
||||||
|
.query({ credentialType: 'harvestOAuth2Api' });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
const scopes = response.body.data;
|
||||||
|
|
||||||
|
expect(Array.isArray(scopes)).toBe(true);
|
||||||
|
expect(scopes.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`GET ${SCOPES_ENDPOINT} should fail with missing credential type`, async () => {
|
||||||
|
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||||
|
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||||
|
|
||||||
|
const response = await authOwnerShellAgent.get(SCOPES_ENDPOINT);
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(400);
|
||||||
|
expect(response.body.message).toBe(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL_TYPE);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`GET ${SCOPES_ENDPOINT} should fail with non-OAuth2 credential type`, async () => {
|
||||||
|
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||||
|
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||||
|
|
||||||
|
const response = await authOwnerShellAgent
|
||||||
|
.get(SCOPES_ENDPOINT)
|
||||||
|
.query({ credentialType: 'disqusApi' });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(400);
|
||||||
|
expect(response.body.message).toBe(RESPONSE_ERROR_MESSAGES.CREDENTIAL_TYPE_NOT_OAUTH2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`GET ${SCOPES_ENDPOINT} should fail with wrong OAuth2 credential type`, async () => {
|
||||||
|
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||||
|
const authOwnerShellAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||||
|
|
||||||
|
const response = await authOwnerShellAgent
|
||||||
|
.get(SCOPES_ENDPOINT)
|
||||||
|
.query({ credentialType: 'wrongOAuth2Api' });
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(500);
|
||||||
|
expect(response.body.message).toBe(`${RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL}: wrongOAuth2Api`);
|
||||||
|
});
|
||||||
|
});
|
|
@ -15,7 +15,7 @@ export type SmtpTestAccount = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type EndpointGroup = 'me' | 'users' | 'auth' | 'owner' | 'passwordReset' | 'credentials';
|
type EndpointGroup = 'me' | 'users' | 'auth' | 'owner' | 'passwordReset' | 'credentials' | 'oauth2-credential';
|
||||||
|
|
||||||
export type CredentialPayload = {
|
export type CredentialPayload = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -26,6 +26,7 @@ import { credentialsController } from '../../../src/api/credentials.api';
|
||||||
import type { User } from '../../../src/databases/entities/User';
|
import type { User } from '../../../src/databases/entities/User';
|
||||||
import type { EndpointGroup, SmtpTestAccount } from './types';
|
import type { EndpointGroup, SmtpTestAccount } from './types';
|
||||||
import type { N8nApp } from '../../../src/UserManagement/Interfaces';
|
import type { N8nApp } from '../../../src/UserManagement/Interfaces';
|
||||||
|
import { oauth2CredentialController } from '../../../src/api/oauth2Credential.api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a test server.
|
* Initialize a test server.
|
||||||
|
@ -63,6 +64,7 @@ export function initTestServer({
|
||||||
if (routerEndpoints.length) {
|
if (routerEndpoints.length) {
|
||||||
const map: Record<string, express.Router> = {
|
const map: Record<string, express.Router> = {
|
||||||
credentials: credentialsController,
|
credentials: credentialsController,
|
||||||
|
'oauth2-credential': oauth2CredentialController,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const group of routerEndpoints) {
|
for (const group of routerEndpoints) {
|
||||||
|
@ -105,7 +107,10 @@ const classifyEndpointGroups = (endpointGroups: string[]) => {
|
||||||
const functionEndpoints: string[] = [];
|
const functionEndpoints: string[] = [];
|
||||||
|
|
||||||
endpointGroups.forEach((group) =>
|
endpointGroups.forEach((group) =>
|
||||||
(group === 'credentials' ? routerEndpoints : functionEndpoints).push(group),
|
(['credentials', 'oauth2-credential'].includes(group)
|
||||||
|
? routerEndpoints
|
||||||
|
: functionEndpoints
|
||||||
|
).push(group),
|
||||||
);
|
);
|
||||||
|
|
||||||
return [routerEndpoints, functionEndpoints];
|
return [routerEndpoints, functionEndpoints];
|
||||||
|
|
Loading…
Reference in a new issue