2022-09-21 01:20:29 -07:00
|
|
|
import express from 'express';
|
2023-01-27 05:56:56 -08:00
|
|
|
import type { INodeCredentialTestResult } from 'n8n-workflow';
|
2023-10-25 07:35:22 -07:00
|
|
|
import { deepCopy } from 'n8n-workflow';
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2022-11-09 06:25:00 -08:00
|
|
|
import * as ResponseHelper from '@/ResponseHelper';
|
|
|
|
import config from '@/config';
|
2022-09-21 01:20:29 -07:00
|
|
|
import { EECredentialsController } from './credentials.controller.ee';
|
|
|
|
import { CredentialsService } from './credentials.service';
|
|
|
|
|
2023-01-02 08:42:32 -08:00
|
|
|
import type { ICredentialsDb } from '@/Interfaces';
|
2023-09-04 06:00:25 -07:00
|
|
|
import type { CredentialRequest, ListQuery } from '@/requests';
|
2023-02-21 10:21:56 -08:00
|
|
|
import { Container } from 'typedi';
|
|
|
|
import { InternalHooks } from '@/InternalHooks';
|
2023-09-04 06:00:25 -07:00
|
|
|
import { listQueryMiddleware } from '@/middlewares';
|
2023-10-25 07:35:22 -07:00
|
|
|
import { Logger } from '@/Logger';
|
2023-11-28 01:19:27 -08:00
|
|
|
import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
2023-12-07 02:35:40 -08:00
|
|
|
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
|
2023-12-11 03:35:14 -08:00
|
|
|
import { NamingService } from '@/services/naming.service';
|
2022-09-21 01:20:29 -07:00
|
|
|
|
|
|
|
export const credentialsController = express.Router();
|
|
|
|
credentialsController.use('/', EECredentialsController);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* GET /credentials
|
|
|
|
*/
|
|
|
|
credentialsController.get(
|
|
|
|
'/',
|
2023-09-04 06:00:25 -07:00
|
|
|
listQueryMiddleware,
|
|
|
|
ResponseHelper.send(async (req: ListQuery.Request) => {
|
|
|
|
return CredentialsService.getMany(req.user, { listQueryOptions: req.listQueryOptions });
|
2022-09-21 01:20:29 -07:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* GET /credentials/new
|
|
|
|
*
|
|
|
|
* Generate a unique credential name.
|
|
|
|
*/
|
|
|
|
credentialsController.get(
|
|
|
|
'/new',
|
2023-12-11 03:35:14 -08:00
|
|
|
ResponseHelper.send(async (req: CredentialRequest.NewName) => {
|
|
|
|
const requestedName = req.query.name ?? config.getEnv('credentials.defaultName');
|
2022-09-21 01:20:29 -07:00
|
|
|
|
|
|
|
return {
|
2023-12-11 03:35:14 -08:00
|
|
|
name: await Container.get(NamingService).getUniqueCredentialName(requestedName),
|
2022-09-21 01:20:29 -07:00
|
|
|
};
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* GET /credentials/:id
|
|
|
|
*/
|
|
|
|
credentialsController.get(
|
2023-06-20 10:13:18 -07:00
|
|
|
'/:id(\\w+)',
|
2022-09-21 01:20:29 -07:00
|
|
|
ResponseHelper.send(async (req: CredentialRequest.Get) => {
|
|
|
|
const { id: credentialId } = req.params;
|
|
|
|
const includeDecryptedData = req.query.includeData === 'true';
|
|
|
|
|
2023-11-29 06:48:36 -08:00
|
|
|
const sharing = await CredentialsService.getSharing(
|
|
|
|
req.user,
|
|
|
|
credentialId,
|
|
|
|
{ allowGlobalScope: true, globalScope: 'credential:read' },
|
|
|
|
['credentials'],
|
|
|
|
);
|
2022-09-21 01:20:29 -07:00
|
|
|
|
|
|
|
if (!sharing) {
|
2023-11-28 01:19:27 -08:00
|
|
|
throw new NotFoundError(`Credential with ID "${credentialId}" could not be found.`);
|
2022-09-21 01:20:29 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const { credentials: credential } = sharing;
|
|
|
|
|
2023-01-02 08:42:32 -08:00
|
|
|
const { data: _, ...rest } = credential;
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2022-12-15 05:31:06 -08:00
|
|
|
if (!includeDecryptedData) {
|
2023-01-02 08:42:32 -08:00
|
|
|
return { ...rest };
|
2022-09-21 01:20:29 -07:00
|
|
|
}
|
|
|
|
|
2022-12-15 05:31:06 -08:00
|
|
|
const decryptedData = CredentialsService.redact(
|
2023-10-23 04:39:35 -07:00
|
|
|
CredentialsService.decrypt(credential),
|
2022-12-15 05:31:06 -08:00
|
|
|
credential,
|
|
|
|
);
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2023-01-02 08:42:32 -08:00
|
|
|
return { data: decryptedData, ...rest };
|
2022-09-21 01:20:29 -07:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* POST /credentials/test
|
|
|
|
*
|
|
|
|
* Test if a credential is valid.
|
|
|
|
*/
|
|
|
|
credentialsController.post(
|
|
|
|
'/test',
|
|
|
|
ResponseHelper.send(async (req: CredentialRequest.Test): Promise<INodeCredentialTestResult> => {
|
2022-11-30 01:28:18 -08:00
|
|
|
const { credentials } = req.body;
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2023-11-29 06:48:36 -08:00
|
|
|
const sharing = await CredentialsService.getSharing(req.user, credentials.id, {
|
|
|
|
allowGlobalScope: true,
|
|
|
|
globalScope: 'credential:read',
|
|
|
|
});
|
2022-12-15 05:31:06 -08:00
|
|
|
|
|
|
|
const mergedCredentials = deepCopy(credentials);
|
|
|
|
if (mergedCredentials.data && sharing?.credentials) {
|
2023-10-23 04:39:35 -07:00
|
|
|
const decryptedData = CredentialsService.decrypt(sharing.credentials);
|
2022-12-15 05:31:06 -08:00
|
|
|
mergedCredentials.data = CredentialsService.unredact(mergedCredentials.data, decryptedData);
|
|
|
|
}
|
|
|
|
|
2023-10-23 04:39:35 -07:00
|
|
|
return CredentialsService.test(req.user, mergedCredentials);
|
2022-09-21 01:20:29 -07:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* POST /credentials
|
|
|
|
*/
|
|
|
|
credentialsController.post(
|
|
|
|
'/',
|
|
|
|
ResponseHelper.send(async (req: CredentialRequest.Create) => {
|
|
|
|
const newCredential = await CredentialsService.prepareCreateData(req.body);
|
|
|
|
|
2023-10-23 04:39:35 -07:00
|
|
|
const encryptedData = CredentialsService.createEncryptedData(null, newCredential);
|
2023-01-02 08:42:32 -08:00
|
|
|
const credential = await CredentialsService.save(newCredential, encryptedData, req.user);
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2023-02-21 10:21:56 -08:00
|
|
|
void Container.get(InternalHooks).onUserCreatedCredentials({
|
2023-01-04 00:47:48 -08:00
|
|
|
user: req.user,
|
|
|
|
credential_name: newCredential.name,
|
2023-01-02 08:42:32 -08:00
|
|
|
credential_type: credential.type,
|
|
|
|
credential_id: credential.id,
|
2022-09-21 01:20:29 -07:00
|
|
|
public_api: false,
|
|
|
|
});
|
|
|
|
|
2023-01-02 08:42:32 -08:00
|
|
|
return credential;
|
2022-09-21 01:20:29 -07:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* PATCH /credentials/:id
|
|
|
|
*/
|
|
|
|
credentialsController.patch(
|
2023-06-20 10:13:18 -07:00
|
|
|
'/:id(\\w+)',
|
2023-01-02 08:42:32 -08:00
|
|
|
ResponseHelper.send(async (req: CredentialRequest.Update): Promise<ICredentialsDb> => {
|
2022-09-21 01:20:29 -07:00
|
|
|
const { id: credentialId } = req.params;
|
|
|
|
|
2023-12-07 02:35:40 -08:00
|
|
|
const sharing = await CredentialsService.getSharing(
|
|
|
|
req.user,
|
|
|
|
credentialId,
|
|
|
|
{
|
|
|
|
allowGlobalScope: true,
|
|
|
|
globalScope: 'credential:update',
|
|
|
|
},
|
|
|
|
['credentials', 'role'],
|
|
|
|
);
|
2022-09-21 01:20:29 -07:00
|
|
|
|
|
|
|
if (!sharing) {
|
2023-10-25 07:35:22 -07:00
|
|
|
Container.get(Logger).info(
|
|
|
|
'Attempt to update credential blocked due to lack of permissions',
|
|
|
|
{
|
|
|
|
credentialId,
|
|
|
|
userId: req.user.id,
|
|
|
|
},
|
|
|
|
);
|
2023-11-28 01:19:27 -08:00
|
|
|
throw new NotFoundError(
|
2022-11-22 04:05:51 -08:00
|
|
|
'Credential to be updated not found. You can only update credentials owned by you',
|
2022-09-21 01:20:29 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-12-19 04:52:42 -08:00
|
|
|
if (sharing.role.name !== 'owner' && !req.user.hasGlobalScope('credential:update')) {
|
2023-12-07 02:35:40 -08:00
|
|
|
Container.get(Logger).info(
|
|
|
|
'Attempt to update credential blocked due to lack of permissions',
|
|
|
|
{
|
|
|
|
credentialId,
|
|
|
|
userId: req.user.id,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
throw new UnauthorizedError('You can only update credentials owned by you');
|
|
|
|
}
|
|
|
|
|
2022-09-21 01:20:29 -07:00
|
|
|
const { credentials: credential } = sharing;
|
|
|
|
|
2023-10-23 04:39:35 -07:00
|
|
|
const decryptedData = CredentialsService.decrypt(credential);
|
2022-09-21 01:20:29 -07:00
|
|
|
const preparedCredentialData = await CredentialsService.prepareUpdateData(
|
|
|
|
req.body,
|
|
|
|
decryptedData,
|
|
|
|
);
|
|
|
|
const newCredentialData = CredentialsService.createEncryptedData(
|
|
|
|
credentialId,
|
|
|
|
preparedCredentialData,
|
|
|
|
);
|
|
|
|
|
|
|
|
const responseData = await CredentialsService.update(credentialId, newCredentialData);
|
|
|
|
|
2023-01-13 09:12:22 -08:00
|
|
|
if (responseData === null) {
|
2023-11-28 01:19:27 -08:00
|
|
|
throw new NotFoundError(`Credential ID "${credentialId}" could not be found to be updated.`);
|
2022-09-21 01:20:29 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Remove the encrypted data as it is not needed in the frontend
|
2023-01-02 08:42:32 -08:00
|
|
|
const { data: _, ...rest } = responseData;
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2023-10-25 07:35:22 -07:00
|
|
|
Container.get(Logger).verbose('Credential updated', { credentialId });
|
2022-09-21 01:20:29 -07:00
|
|
|
|
2023-01-02 08:42:32 -08:00
|
|
|
return { ...rest };
|
2022-09-21 01:20:29 -07:00
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* DELETE /credentials/:id
|
|
|
|
*/
|
|
|
|
credentialsController.delete(
|
2023-06-20 10:13:18 -07:00
|
|
|
'/:id(\\w+)',
|
2022-09-21 01:20:29 -07:00
|
|
|
ResponseHelper.send(async (req: CredentialRequest.Delete) => {
|
|
|
|
const { id: credentialId } = req.params;
|
|
|
|
|
2023-12-07 02:35:40 -08:00
|
|
|
const sharing = await CredentialsService.getSharing(
|
|
|
|
req.user,
|
|
|
|
credentialId,
|
|
|
|
{
|
|
|
|
allowGlobalScope: true,
|
|
|
|
globalScope: 'credential:delete',
|
|
|
|
},
|
|
|
|
['credentials', 'role'],
|
|
|
|
);
|
2022-09-21 01:20:29 -07:00
|
|
|
|
|
|
|
if (!sharing) {
|
2023-10-25 07:35:22 -07:00
|
|
|
Container.get(Logger).info(
|
|
|
|
'Attempt to delete credential blocked due to lack of permissions',
|
|
|
|
{
|
|
|
|
credentialId,
|
|
|
|
userId: req.user.id,
|
|
|
|
},
|
|
|
|
);
|
2023-11-28 01:19:27 -08:00
|
|
|
throw new NotFoundError(
|
2022-11-22 04:05:51 -08:00
|
|
|
'Credential to be deleted not found. You can only removed credentials owned by you',
|
2022-09-21 01:20:29 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-12-19 04:52:42 -08:00
|
|
|
if (sharing.role.name !== 'owner' && !req.user.hasGlobalScope('credential:delete')) {
|
2023-12-07 02:35:40 -08:00
|
|
|
Container.get(Logger).info(
|
|
|
|
'Attempt to delete credential blocked due to lack of permissions',
|
|
|
|
{
|
|
|
|
credentialId,
|
|
|
|
userId: req.user.id,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
throw new UnauthorizedError('You can only remove credentials owned by you');
|
|
|
|
}
|
|
|
|
|
2022-09-21 01:20:29 -07:00
|
|
|
const { credentials: credential } = sharing;
|
|
|
|
|
|
|
|
await CredentialsService.delete(credential);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}),
|
|
|
|
);
|