Skip to content

Commit

Permalink
🛠 ✅ Create credentials namespace and add tests (#2831)
Browse files Browse the repository at this point in the history
* 🧪 Fix failing test

* 📘 Improve `createAgent` signature

* 🚚 Fix `LoggerProxy` import

* ✨ Create credentials endpoints namespace

* 🧪 Set up initial tests

* ⚡ Add validation to model

* ⚡ Adjust validation

* 🧪 Add test

* 🚚 Sort creds endpoints

* ✏️ Plan out pending tests

* 🧪 Add deletion tests

* 🧪 Add patch tests

* 🧪 Add get cred tests

* 🚚 Hoist import

* ✏️ Make test descriptions consistent

* ✏️ Adjust description

* 🧪 Add missing test

* ✏️ Make get descriptions consistent

* ⏪ Undo line break

* ⚡ Refactor to simplify `saveCredential`

* 🧪 Add non-owned tests for owner

* ✏️ Improve naming

* ✏️ Add clarifying comments

* 🚚 Improve imports

* ⚡ Initialize config file

* 🔥 Remove unneeded import

* 🚚 Rename dir

* ⚡ Adjust deletion call

* ⚡ Adjust error code

* ✏️ Touch up comment

* ⚡ Optimize fetching with `@RelationId`

* 🧪 Add expectations

* ⚡ Simplify mock calls

* 📘 Set deep readonly to object constants

* 🔥 Remove unused param and encryption key

* ⚡ Add more `@RelationId` calls in models

* ⏪ Restore
  • Loading branch information
ivov authored Feb 23, 2022
1 parent bc88e7b commit 4f5b0c6
Show file tree
Hide file tree
Showing 13 changed files with 1,093 additions and 392 deletions.
345 changes: 2 additions & 343 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,6 @@ import {
IDataObject,
INodeCredentials,
INodeCredentialsDetails,
INodeCredentialTestRequest,
INodeCredentialTestResult,
INodeParameters,
INodePropertyOptions,
INodeType,
Expand Down Expand Up @@ -114,10 +112,7 @@ import {
GenericHelpers,
ICredentialsDb,
ICredentialsOverwrite,
ICredentialsResponse,
ICustomRequest,
IExecutionDeleteFilter,
IExecutionFlatted,
IExecutionFlattedDb,
IExecutionFlattedResponse,
IExecutionPushResponse,
Expand All @@ -132,7 +127,6 @@ import {
ITagWithCountDb,
IWorkflowExecutionDataProcess,
IWorkflowResponse,
IPersonalizationSurveyAnswers,
NodeTypes,
Push,
ResponseHelper,
Expand Down Expand Up @@ -172,8 +166,8 @@ import type {
import { DEFAULT_EXECUTIONS_GET_ALL_LIMIT, validateEntity } from './GenericHelpers';
import { ExecutionEntity } from './databases/entities/ExecutionEntity';
import { SharedWorkflow } from './databases/entities/SharedWorkflow';
import { SharedCredentials } from './databases/entities/SharedCredentials';
import { RESPONSE_ERROR_MESSAGES } from './constants';
import { credentialsEndpoints } from './api/namespaces/credentials';

require('body-parser-xml')(bodyParser);

Expand Down Expand Up @@ -1567,342 +1561,7 @@ class App {
// Credentials
// ----------------------------------------

this.app.get(
`/${this.restEndpoint}/credentials/new`,
ResponseHelper.send(async (req: WorkflowRequest.NewName): Promise<{ name: string }> => {
const requestedName =
req.query.name && req.query.name !== '' ? req.query.name : this.defaultCredentialsName;

return await GenericHelpers.generateUniqueName(requestedName, 'credentials');
}),
);

// Deletes a specific credential
this.app.delete(
`/${this.restEndpoint}/credentials/:id`,
ResponseHelper.send(async (req: CredentialRequest.Delete) => {
const { id: credentialId } = req.params;

const shared = await Db.collections.SharedCredentials!.findOne({
relations: ['credentials'],
where: whereClause({
user: req.user,
entityType: 'credentials',
entityId: credentialId,
}),
});

if (!shared) {
throw new ResponseHelper.ResponseError(
`Credential with ID "${credentialId}" could not be found to be deleted.`,
undefined,
404,
);
}

await this.externalHooks.run('credentials.delete', [credentialId]);

await Db.collections.Credentials!.delete(credentialId);

return true;
}),
);

// Creates new credentials
this.app.post(
`/${this.restEndpoint}/credentials`,
ResponseHelper.send(async (req: CredentialRequest.Create) => {
delete req.body.id; // delete if sent

const newCredential = new CredentialsEntity();

Object.assign(newCredential, req.body);

await validateEntity(newCredential);

// Add the added date for node access permissions
for (const nodeAccess of newCredential.nodesAccess) {
nodeAccess.date = this.getCurrentDate();
}

const encryptionKey = await UserSettings.getEncryptionKey();

if (!encryptionKey) {
throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
}

// Encrypt the data
const coreCredential = new Credentials(
{ id: null, name: newCredential.name },
newCredential.type,
newCredential.nodesAccess,
);

// @ts-ignore
coreCredential.setData(newCredential.data, encryptionKey);

const encryptedData = coreCredential.getDataToSave() as ICredentialsDb;

Object.assign(newCredential, encryptedData);

await this.externalHooks.run('credentials.create', [encryptedData]);

const role = await Db.collections.Role!.findOneOrFail({
name: 'owner',
scope: 'credential',
});

const savedCredential = await getConnection().transaction(async (transactionManager) => {
const savedCredential = await transactionManager.save<CredentialsEntity>(newCredential);

savedCredential.data = newCredential.data;

const newSharedCredential = new SharedCredentials();

Object.assign(newSharedCredential, {
role,
user: req.user,
credentials: savedCredential,
});

await transactionManager.save<SharedCredentials>(newSharedCredential);

return savedCredential;
});

const { id, ...rest } = savedCredential;

return { id: id.toString(), ...rest };
}),
);

// Test credentials
this.app.post(
`/${this.restEndpoint}/credentials-test`,
ResponseHelper.send(
async (req: express.Request, res: express.Response): Promise<INodeCredentialTestResult> => {
const incomingData = req.body as INodeCredentialTestRequest;

const encryptionKey = await UserSettings.getEncryptionKey();
if (encryptionKey === undefined) {
return {
status: 'Error',
message: 'No encryption key got found to decrypt the credentials!',
};
}

const credentialsHelper = new CredentialsHelper(encryptionKey);

const credentialType = incomingData.credentials.type;
return credentialsHelper.testCredentials(
credentialType,
incomingData.credentials,
incomingData.nodeToTestWith,
);
},
),
);

// Updates existing credentials
this.app.patch(
`/${this.restEndpoint}/credentials/:id`,
ResponseHelper.send(async (req: CredentialRequest.Update): Promise<ICredentialsResponse> => {
const { id: credentialId } = req.params;

const updateData = new CredentialsEntity();
Object.assign(updateData, req.body);

const shared = await Db.collections.SharedCredentials!.findOne({
relations: ['credentials'],
where: whereClause({
user: req.user,
entityType: 'credentials',
entityId: credentialId,
}),
});

if (!shared) {
throw new ResponseHelper.ResponseError(
`Credential with ID "${credentialId}" could not be found to be updated.`,
undefined,
404,
);
}

const { credentials: credential } = shared;

// Add the date for newly added node access permissions
for (const nodeAccess of updateData.nodesAccess) {
if (!nodeAccess.date) {
nodeAccess.date = this.getCurrentDate();
}
}

const encryptionKey = await UserSettings.getEncryptionKey();

if (!encryptionKey) {
throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
}

const coreCredential = new Credentials(
{ id: credential.id.toString(), name: credential.name },
credential.type,
credential.nodesAccess,
credential.data,
);

const decryptedData = coreCredential.getData(encryptionKey);

// Do not overwrite the oauth data else data like the access or refresh token would get lost
// everytime anybody changes anything on the credentials even if it is just the name.
if (decryptedData.oauthTokenData) {
// @ts-ignore
updateData.data.oauthTokenData = decryptedData.oauthTokenData;
}

// Encrypt the data
const credentials = new Credentials(
{ id: credentialId, name: updateData.name },
updateData.type,
updateData.nodesAccess,
);

// @ts-ignore
credentials.setData(updateData.data, encryptionKey);

const newCredentialData = credentials.getDataToSave() as ICredentialsDb;

// Add special database related data
newCredentialData.updatedAt = this.getCurrentDate();

await this.externalHooks.run('credentials.update', [newCredentialData]);

// Update the credentials in DB
await Db.collections.Credentials!.update(credentialId, newCredentialData);

// We sadly get nothing back from "update". Neither if it updated a record
// nor the new value. So query now the hopefully updated entry.
const responseData = await Db.collections.Credentials!.findOne(credentialId);

if (responseData === undefined) {
throw new ResponseHelper.ResponseError(
`Credential with ID "${credentialId}" could not be found to be updated.`,
undefined,
400,
);
}

// Remove the encrypted data as it is not needed in the frontend
const { id, data, ...rest } = responseData;

return {
id: id.toString(),
...rest,
};
}),
);

// Returns specific credentials
this.app.get(
`/${this.restEndpoint}/credentials/:id`,
ResponseHelper.send(async (req: CredentialRequest.Get) => {
const { id: credentialId } = req.params;

const shared = await Db.collections.SharedCredentials!.findOne({
relations: ['credentials'],
where: whereClause({
user: req.user,
entityType: 'credentials',
entityId: credentialId,
}),
});

if (!shared) return {};

const { credentials: credential } = shared;

if (req.query.includeData !== 'true') {
const { data, id, ...rest } = credential;

return {
id: id.toString(),
...rest,
};
}

const { data, id, ...rest } = credential;

const encryptionKey = await UserSettings.getEncryptionKey();

if (!encryptionKey) {
throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
}

const coreCredential = new Credentials(
{ id: credential.id.toString(), name: credential.name },
credential.type,
credential.nodesAccess,
credential.data,
);

return {
id: id.toString(),
data: coreCredential.getData(encryptionKey),
...rest,
};
}),
);

// Returns all the saved credentials
this.app.get(
`/${this.restEndpoint}/credentials`,
ResponseHelper.send(
async (req: CredentialRequest.GetAll): Promise<ICredentialsResponse[]> => {
let credentials: ICredentialsDb[] = [];

const filter: Record<string, string> = req.query.filter
? JSON.parse(req.query.filter)
: {};

if (req.user.globalRole.name === 'owner') {
credentials = await Db.collections.Credentials!.find({
select: ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'],
where: filter,
});
} else {
const shared = await Db.collections.SharedCredentials!.find({
relations: ['credentials'],
where: whereClause({
user: req.user,
entityType: 'credentials',
}),
});

if (!shared.length) return [];

credentials = await Db.collections.Credentials!.find({
select: ['id', 'name', 'type', 'nodesAccess', 'createdAt', 'updatedAt'],
where: {
id: In(shared.map(({ credentials }) => credentials.id)),
...filter,
},
});
}

let encryptionKey;

if (req.query.includeData === 'true') {
encryptionKey = await UserSettings.getEncryptionKey();

if (!encryptionKey) {
throw new Error(RESPONSE_ERROR_MESSAGES.NO_ENCRYPTION_KEY);
}
}

return credentials.map(({ id, ...rest }) => ({ id: id.toString(), ...rest }));
},
),
);
credentialsEndpoints.apply(this);

// ----------------------------------------
// Credential-Types
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/UserManagement/Interfaces.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable import/no-cycle */
import { Application } from 'express';
import { JwtFromRequestFunction } from 'passport-jwt';
import type { IExternalHooksClass, IPersonalizationSurveyAnswers } from '../Interfaces';
import { ActiveWorkflowRunner } from '..';
import { IPersonalizationSurveyAnswers } from '../Interfaces';

export interface JwtToken {
token: string;
Expand Down Expand Up @@ -33,5 +33,7 @@ export interface PublicUser {
export interface N8nApp {
app: Application;
restEndpoint: string;
externalHooks: IExternalHooksClass;
defaultCredentialsName: string;
activeWorkflowRunner: ActiveWorkflowRunner.ActiveWorkflowRunner;
}
Loading

0 comments on commit 4f5b0c6

Please sign in to comment.