diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 080f93ea562b6..6806dd02943a9 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -35,6 +35,7 @@ import { Workflow, WorkflowExecuteMode, ITaskDataConnections, + LoggerProxy as Logger, } from 'n8n-workflow'; // eslint-disable-next-line import/no-cycle @@ -668,7 +669,7 @@ export class CredentialsHelper extends ICredentialsHelper { }; } } - + Logger.debug('Credential test failed', error); return { status: 'Error', message: error.message.toString(), diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index eb1a261a0c52e..ac9cea886bff8 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -777,6 +777,7 @@ class App { }); if (!savedWorkflow) { + LoggerProxy.error('Failed to create workflow', { userId: req.user.id }); throw new ResponseHelper.ResponseError('Failed to save workflow'); } @@ -938,6 +939,10 @@ class App { }); if (!shared) { + LoggerProxy.info('User attempted to access a workflow without permissions', { + workflowId, + userId: req.user.id, + }); throw new ResponseHelper.ResponseError( `Workflow with ID "${workflowId}" could not be found.`, undefined, @@ -976,6 +981,10 @@ class App { }); if (!shared) { + LoggerProxy.info('User attempted to update a workflow without permissions', { + workflowId, + userId: req.user.id, + }); throw new ResponseHelper.ResponseError( `Workflow with ID "${workflowId}" could not be found to be updated.`, undefined, @@ -1114,6 +1123,10 @@ class App { }); if (!shared) { + LoggerProxy.info('User attempted to delete a workflow without permissions', { + workflowId, + userId: req.user.id, + }); throw new ResponseHelper.ResponseError( `Workflow with ID "${workflowId}" could not be found to be deleted.`, undefined, @@ -1573,6 +1586,11 @@ class App { }); if (!shared) { + LoggerProxy.info('User attempted to access workflow errors without permissions', { + workflowId, + userId: req.user.id, + }); + throw new ResponseHelper.ResponseError( `Workflow with ID "${workflowId}" could not be found.`, undefined, @@ -1653,6 +1671,7 @@ class App { const { id: credentialId } = req.query; if (!credentialId) { + LoggerProxy.error('OAuth1 credential authorization failed due to missing credential ID'); throw new ResponseHelper.ResponseError( 'Required credential ID is missing', undefined, @@ -1663,6 +1682,10 @@ class App { const credential = await getCredentialForUser(credentialId, req.user); if (!credential) { + LoggerProxy.error( + 'OAuth1 credential authorization failed because the current user does not have the correct permissions', + { userId: req.user.id }, + ); throw new ResponseHelper.ResponseError( RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL, undefined, @@ -1757,6 +1780,11 @@ class App { // Update the credentials in DB await Db.collections.Credentials!.update(credentialId, newCredentialsData); + LoggerProxy.verbose('OAuth1 authorization successful for new credential', { + userId: req.user.id, + credentialId, + }); + return returnUri; }), ); @@ -1776,12 +1804,23 @@ class App { undefined, 503, ); + LoggerProxy.error( + 'OAuth1 callback failed because of insufficient parameters received', + { + userId: req.user.id, + credentialId, + }, + ); return ResponseHelper.sendErrorResponse(res, errorResponse); } const credential = await getCredentialForUser(credentialId, req.user); if (!credential) { + LoggerProxy.error('OAuth1 callback failed because of insufficient user permissions', { + userId: req.user.id, + credentialId, + }); const errorResponse = new ResponseHelper.ResponseError( RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL, undefined, @@ -1829,6 +1868,10 @@ class App { try { oauthToken = await requestPromise(options); } catch (error) { + LoggerProxy.error('Unable to fetch tokens for OAuth1 callback', { + userId: req.user.id, + credentialId, + }); const errorResponse = new ResponseHelper.ResponseError( 'Unable to get access tokens!', undefined, @@ -1855,8 +1898,16 @@ class App { // Save the credentials in DB await Db.collections.Credentials!.update(credentialId, newCredentialsData); + LoggerProxy.verbose('OAuth1 callback successful for new credential', { + userId: req.user.id, + credentialId, + }); res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html')); } catch (error) { + LoggerProxy.error('OAuth1 callback failed because of insufficient user permissions', { + userId: req.user.id, + credentialId: req.query.cid, + }); // Error response return ResponseHelper.sendErrorResponse(res, error); } @@ -1884,6 +1935,10 @@ class App { const credential = await getCredentialForUser(credentialId, req.user); if (!credential) { + LoggerProxy.error('Failed to authorize OAuth2 due to lack of permissions', { + userId: req.user.id, + credentialId, + }); throw new ResponseHelper.ResponseError( RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL, undefined, @@ -1971,6 +2026,10 @@ class App { returnUri += `&${authQueryParameters}`; } + LoggerProxy.verbose('OAuth2 authentication successful for new credential', { + userId: req.user.id, + credentialId, + }); return returnUri; }), ); @@ -2013,6 +2072,10 @@ class App { const credential = await getCredentialForUser(state.cid, req.user); if (!credential) { + LoggerProxy.error('OAuth2 callback failed because of insufficient permissions', { + userId: req.user.id, + credentialId: state.cid, + }); const errorResponse = new ResponseHelper.ResponseError( RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL, undefined, @@ -2051,6 +2114,10 @@ class App { decryptedDataOriginal.csrfSecret === undefined || !token.verify(decryptedDataOriginal.csrfSecret as string, state.token) ) { + LoggerProxy.debug('OAuth2 callback state is invalid', { + userId: req.user.id, + credentialId: state.cid, + }); const errorResponse = new ResponseHelper.ResponseError( 'The OAuth2 callback state is invalid!', undefined, @@ -2098,6 +2165,10 @@ class App { } if (oauthToken === undefined) { + LoggerProxy.error('OAuth2 callback failed: unable to get access tokens', { + userId: req.user.id, + credentialId: state.cid, + }); const errorResponse = new ResponseHelper.ResponseError( 'Unable to get access tokens!', undefined, @@ -2128,6 +2199,10 @@ class App { newCredentialsData.updatedAt = this.getCurrentDate(); // Save the credentials in DB await Db.collections.Credentials!.update(state.cid, newCredentialsData); + LoggerProxy.verbose('OAuth2 callback successful for new credential', { + userId: req.user.id, + credentialId: state.cid, + }); res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html')); } catch (error) { @@ -2277,7 +2352,16 @@ class App { }, }); - if (!execution) return undefined; + if (!execution) { + LoggerProxy.info( + 'Attempt to read execution was blocked due to insufficient permissions', + { + userId: req.user.id, + executionId, + }, + ); + return undefined; + } if (req.query.unflattedResponse === 'true') { return ResponseHelper.unflattenExecutionData(execution); @@ -2312,6 +2396,13 @@ class App { }); if (!execution) { + LoggerProxy.info( + 'Attempt to retry an execution was blocked due to insufficient permissions', + { + userId: req.user.id, + executionId, + }, + ); throw new ResponseHelper.ResponseError( `The execution with the ID "${executionId}" does not exist.`, 404, @@ -2387,6 +2478,11 @@ class App { // Find the data of the last executed node in the new workflow const node = workflowInstance.getNode(stack.node.name); if (node === null) { + LoggerProxy.error('Failed to retry an execution because a node could not be found', { + userId: req.user.id, + executionId, + nodeName: stack.node.name, + }); throw new Error( `Could not find the node "${stack.node.name}" in workflow. It probably got deleted or renamed. Without it the workflow can sadly not be retried.`, ); @@ -2468,7 +2564,13 @@ class App { }, }); - if (!executions.length) return; + if (!executions.length) { + LoggerProxy.error('Failed to delete an execution due to insufficient permissions', { + userId: req.user.id, + executionIds: ids, + }); + return; + } const idsToDelete = executions.map(({ id }) => id.toString()); diff --git a/packages/cli/src/api/credentials.api.ts b/packages/cli/src/api/credentials.api.ts index bce869c6d9d03..e1e672711952d 100644 --- a/packages/cli/src/api/credentials.api.ts +++ b/packages/cli/src/api/credentials.api.ts @@ -6,7 +6,8 @@ import express = require('express'); import { In } from 'typeorm'; import { UserSettings, Credentials } from 'n8n-core'; -import { INodeCredentialTestResult } from 'n8n-workflow'; +import { INodeCredentialTestResult, LoggerProxy } from 'n8n-workflow'; +import { getLogger } from '../Logger'; import { CredentialsHelper, @@ -28,6 +29,18 @@ import { externalHooks } from '../Server'; export const credentialsController = express.Router(); +/** + * Initialize Logger if needed + */ +credentialsController.use((req, res, next) => { + try { + LoggerProxy.getInstance(); + } catch (error) { + LoggerProxy.init(getLogger()); + } + next(); +}); + /** * GET /credentials */ @@ -38,28 +51,33 @@ credentialsController.get( const filter = req.query.filter ? (JSON.parse(req.query.filter) as Record) : {}; - 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({ - 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(({ credentialId }) => credentialId)), - ...filter, - }, - }); + try { + 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({ + 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(({ credentialId }) => credentialId)), + ...filter, + }, + }); + } + } catch (error) { + LoggerProxy.error('Request to list credentials failed', error); + throw error; } return credentials.map((credential) => { @@ -179,7 +197,10 @@ credentialsController.post( return savedCredential; }); - + LoggerProxy.verbose('New credential created', { + credentialId: newCredential.id, + ownerId: req.user.id, + }); return { id: id.toString(), ...rest }; }), ); @@ -202,6 +223,10 @@ credentialsController.delete( }); if (!shared) { + LoggerProxy.info('Attempt to delete credential blocked due to lack of permissions', { + credentialId, + userId: req.user.id, + }); throw new ResponseHelper.ResponseError( `Credential with ID "${credentialId}" could not be found to be deleted.`, undefined, @@ -240,6 +265,10 @@ credentialsController.patch( }); if (!shared) { + LoggerProxy.info('Attempt to update credential blocked due to lack of permissions', { + credentialId, + userId: req.user.id, + }); throw new ResponseHelper.ResponseError( `Credential with ID "${credentialId}" could not be found to be updated.`, undefined, @@ -317,6 +346,8 @@ credentialsController.patch( // Remove the encrypted data as it is not needed in the frontend const { id, data, ...rest } = responseData; + LoggerProxy.verbose('Credential updated', { credentialId }); + return { id: id.toString(), ...rest,