refactor(core): Enforce authorization by default on all routes (no-changelog) (#8762)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2024-02-28 17:02:18 +01:00
committed by GitHub
parent 2811f77798
commit db4a419c8d
46 changed files with 126 additions and 299 deletions

View File

@@ -1,8 +1,7 @@
import { Authorized, Get, RestController } from '@/decorators';
import { Get, RestController } from '@/decorators';
import { ActiveWorkflowRequest } from '@/requests';
import { ActiveWorkflowsService } from '@/services/activeWorkflows.service';
@Authorized()
@RestController('/active-workflows')
export class ActiveWorkflowsController {
constructor(private readonly activeWorkflowsService: ActiveWorkflowsService) {}

View File

@@ -1,7 +1,7 @@
import validator from 'validator';
import { AuthService } from '@/auth/auth.service';
import { Authorized, Get, Post, RestController } from '@/decorators';
import { Get, Post, RestController } from '@/decorators';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { Request, Response } from 'express';
import type { User } from '@db/entities/User';
@@ -38,10 +38,8 @@ export class AuthController {
private readonly postHog?: PostHogClient,
) {}
/**
* Log in a user.
*/
@Post('/login')
/** Log in a user */
@Post('/login', { skipAuth: true })
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
const { email, password, mfaToken, mfaRecoveryCode } = req.body;
if (!email) throw new ApplicationError('Email is required to log in');
@@ -113,7 +111,6 @@ export class AuthController {
}
/** Check if the user is already logged in */
@Authorized()
@Get('/login')
async currentUser(req: AuthenticatedRequest): Promise<PublicUser> {
return await this.userService.toPublic(req.user, {
@@ -122,10 +119,8 @@ export class AuthController {
});
}
/**
* Validate invite token to enable invitee to set up their account.
*/
@Get('/resolve-signup-token')
/** Validate invite token to enable invitee to set up their account */
@Get('/resolve-signup-token', { skipAuth: true })
async resolveSignupToken(req: UserRequest.ResolveSignUp) {
const { inviterId, inviteeId } = req.query;
const isWithinUsersLimit = this.license.isWithinUsersLimit();
@@ -192,10 +187,7 @@ export class AuthController {
return { inviter: { firstName, lastName } };
}
/**
* Log out a user.
*/
@Authorized()
/** Log out a user */
@Post('/logout')
logout(_: Request, res: Response) {
this.authService.clearCookie(res);

View File

@@ -5,16 +5,7 @@ import {
STARTER_TEMPLATE_NAME,
UNKNOWN_FAILURE_REASON,
} from '@/constants';
import {
Authorized,
Delete,
Get,
Middleware,
Patch,
Post,
RestController,
GlobalScope,
} from '@/decorators';
import { Delete, Get, Middleware, Patch, Post, RestController, GlobalScope } from '@/decorators';
import { NodeRequest } from '@/requests';
import type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { CommunityPackages } from '@/Interfaces';
@@ -41,7 +32,6 @@ export function isNpmError(error: unknown): error is { code: number; stdout: str
return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
}
@Authorized()
@RestController('/community-packages')
export class CommunityPackagesController {
constructor(

View File

@@ -1,5 +1,5 @@
import express from 'express';
import { Authorized, Get, RestController } from '@/decorators';
import { Get, RestController } from '@/decorators';
import { AuthenticatedRequest } from '@/requests';
import { CtaService } from '@/services/cta.service';
@@ -7,7 +7,6 @@ import { CtaService } from '@/services/cta.service';
* Controller for Call to Action (CTA) endpoints. CTAs are certain
* messages that are shown to users in the UI.
*/
@Authorized()
@RestController('/cta')
export class CtaController {
constructor(private readonly ctaService: CtaService) {}

View File

@@ -11,7 +11,7 @@ export class DebugController {
private readonly workflowRepository: WorkflowRepository,
) {}
@Get('/multi-main-setup')
@Get('/multi-main-setup', { skipAuth: true })
async getMultiMainSetupDetails() {
const leaderKey = await this.orchestrationService.multiMainSetup.fetchLeaderKey();

View File

@@ -7,7 +7,7 @@ import type {
} from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';
import { Authorized, Get, Middleware, RestController } from '@/decorators';
import { Get, Middleware, RestController } from '@/decorators';
import { getBase } from '@/WorkflowExecuteAdditionalData';
import { DynamicNodeParametersService } from '@/services/dynamicNodeParameters.service';
import { DynamicNodeParametersRequest } from '@/requests';
@@ -21,17 +21,12 @@ const assertMethodName: RequestHandler = (req, res, next) => {
next();
};
@Authorized()
@RestController('/dynamic-node-parameters')
export class DynamicNodeParametersController {
constructor(private readonly service: DynamicNodeParametersService) {}
@Middleware()
parseQueryParams(
req: DynamicNodeParametersRequest.BaseRequest,
res: Response,
next: NextFunction,
) {
parseQueryParams(req: DynamicNodeParametersRequest.BaseRequest, _: Response, next: NextFunction) {
const { credentials, currentNodeParameters, nodeTypeAndVersion } = req.query;
if (!nodeTypeAndVersion) {
throw new BadRequestError('Parameter nodeTypeAndVersion is required.');

View File

@@ -7,7 +7,7 @@ import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
import { License } from '@/License';
import { LICENSE_FEATURES, inE2ETests } from '@/constants';
import { NoAuthRequired, Patch, Post, RestController } from '@/decorators';
import { Patch, Post, RestController } from '@/decorators';
import type { UserSetupPayload } from '@/requests';
import type { BooleanLicenseFeature, IPushDataType } from '@/Interfaces';
import { MfaService } from '@/Mfa/mfa.service';
@@ -60,7 +60,6 @@ type PushRequest = Request<
}
>;
@NoAuthRequired()
@RestController('/e2e')
export class E2EController {
private enabledFeatures: Record<BooleanLicenseFeature, boolean> = {
@@ -97,7 +96,7 @@ export class E2EController {
this.enabledFeatures[feature] ?? false;
}
@Post('/reset')
@Post('/reset', { skipAuth: true })
async reset(req: ResetRequest) {
this.resetFeatures();
await this.resetLogStreaming();
@@ -107,18 +106,18 @@ export class E2EController {
await this.setupUserManagement(req.body.owner, req.body.members, req.body.admin);
}
@Post('/push')
@Post('/push', { skipAuth: true })
async pushSend(req: PushRequest) {
this.push.broadcast(req.body.type, req.body.data);
}
@Patch('/feature')
@Patch('/feature', { skipAuth: true })
setFeature(req: Request<{}, {}, { feature: BooleanLicenseFeature; enabled: boolean }>) {
const { enabled, feature } = req.body;
this.enabledFeatures[feature] = enabled;
}
@Patch('/queue-mode')
@Patch('/queue-mode', { skipAuth: true })
async setQueueMode(req: Request<{}, {}, { enabled: boolean }>) {
const { enabled } = req.body;
config.set('executions.mode', enabled ? 'queue' : 'regular');

View File

@@ -3,7 +3,7 @@ import validator from 'validator';
import { AuthService } from '@/auth/auth.service';
import config from '@/config';
import { Authorized, NoAuthRequired, Post, GlobalScope, RestController } from '@/decorators';
import { Post, GlobalScope, RestController } from '@/decorators';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { UserRequest } from '@/requests';
import { License } from '@/License';
@@ -19,7 +19,6 @@ import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { InternalHooks } from '@/InternalHooks';
import { ExternalHooks } from '@/ExternalHooks';
@Authorized()
@RestController('/invitations')
export class InvitationController {
constructor(
@@ -120,8 +119,7 @@ export class InvitationController {
/**
* Fill out user shell with first name, last name, and password.
*/
@NoAuthRequired()
@Post('/:id/accept')
@Post('/:id/accept', { skipAuth: true })
async acceptInvitation(req: UserRequest.Update, res: Response) {
const { id: inviteeId } = req.params;

View File

@@ -4,7 +4,7 @@ import { Response } from 'express';
import { randomBytes } from 'crypto';
import { AuthService } from '@/auth/auth.service';
import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators';
import { Delete, Get, Patch, Post, RestController } from '@/decorators';
import { PasswordUtility } from '@/services/password.utility';
import { validateEntity } from '@/GenericHelpers';
import type { User } from '@db/entities/User';
@@ -23,7 +23,6 @@ import { InternalHooks } from '@/InternalHooks';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UserRepository } from '@/databases/repositories/user.repository';
@Authorized()
@RestController('/me')
export class MeController {
constructor(

View File

@@ -1,9 +1,8 @@
import { Authorized, Delete, Get, Post, RestController } from '@/decorators';
import { Delete, Get, Post, RestController } from '@/decorators';
import { AuthenticatedRequest, MFA } from '@/requests';
import { MfaService } from '@/Mfa/mfa.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Authorized()
@RestController('/mfa')
export class MFAController {
constructor(private mfaService: MfaService) {}

View File

@@ -2,11 +2,10 @@ import { readFile } from 'fs/promises';
import get from 'lodash/get';
import { Request } from 'express';
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
import { Authorized, Post, RestController } from '@/decorators';
import { Post, RestController } from '@/decorators';
import config from '@/config';
import { NodeTypes } from '@/NodeTypes';
@Authorized()
@RestController('/node-types')
export class NodeTypesController {
constructor(private readonly nodeTypes: NodeTypes) {}

View File

@@ -5,7 +5,7 @@ import type { RequestOptions } from 'oauth-1.0a';
import clientOAuth1 from 'oauth-1.0a';
import { createHmac } from 'crypto';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { Authorized, Get, RestController } from '@/decorators';
import { Get, RestController } from '@/decorators';
import { OAuthRequest } from '@/requests';
import { sendErrorResponse } from '@/ResponseHelper';
import { AbstractOAuthController } from './abstractOAuth.controller';
@@ -29,7 +29,6 @@ const algorithmMap = {
/* eslint-enable */
} as const;
@Authorized()
@RestController('/oauth1-credential')
export class OAuth1CredentialController extends AbstractOAuthController {
override oauthVersion = 1;

View File

@@ -8,7 +8,7 @@ import omit from 'lodash/omit';
import set from 'lodash/set';
import split from 'lodash/split';
import { ApplicationError, jsonParse, jsonStringify } from 'n8n-workflow';
import { Authorized, Get, RestController } from '@/decorators';
import { Get, RestController } from '@/decorators';
import { OAuthRequest } from '@/requests';
import { AbstractOAuthController } from './abstractOAuth.controller';
@@ -17,7 +17,6 @@ interface CsrfStateParam {
token: string;
}
@Authorized()
@RestController('/oauth2-credential')
export class OAuth2CredentialController extends AbstractOAuthController {
override oauthVersion = 2;

View File

@@ -1,9 +1,8 @@
import { Authorized, Post, RestController, GlobalScope } from '@/decorators';
import { Post, RestController, GlobalScope } from '@/decorators';
import { OrchestrationRequest } from '@/requests';
import { OrchestrationService } from '@/services/orchestration.service';
import { License } from '@/License';
@Authorized()
@RestController('/orchestration')
export class OrchestrationController {
constructor(
@@ -12,7 +11,7 @@ export class OrchestrationController {
) {}
/**
* These endpoints do not return anything, they just trigger the messsage to
* These endpoints do not return anything, they just trigger the message to
* the workers to respond on Redis with their status.
*/
@GlobalScope('orchestration:read')

View File

@@ -4,7 +4,7 @@ import { Response } from 'express';
import { AuthService } from '@/auth/auth.service';
import config from '@/config';
import { validateEntity } from '@/GenericHelpers';
import { Authorized, Post, RestController } from '@/decorators';
import { GlobalScope, Post, RestController } from '@/decorators';
import { PasswordUtility } from '@/services/password.utility';
import { OwnerRequest } from '@/requests';
import { SettingsRepository } from '@db/repositories/settings.repository';
@@ -15,7 +15,6 @@ import { Logger } from '@/Logger';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { InternalHooks } from '@/InternalHooks';
@Authorized('global:owner')
@RestController('/owner')
export class OwnerController {
constructor(
@@ -33,24 +32,19 @@ export class OwnerController {
* Promote a shell into the owner of the n8n instance,
* and enable `isInstanceOwnerSetUp` setting.
*/
@Post('/setup')
@Post('/setup', { skipAuth: true })
async setupOwner(req: OwnerRequest.Post, res: Response) {
const { email, firstName, lastName, password } = req.body;
const { id: userId } = req.user;
if (config.getEnv('userManagement.isInstanceOwnerSetUp')) {
this.logger.debug(
'Request to claim instance ownership failed because instance owner already exists',
{
userId,
},
);
throw new BadRequestError('Instance owner already setup');
}
if (!email || !validator.isEmail(email)) {
this.logger.debug('Request to claim instance ownership failed because of invalid email', {
userId,
invalidEmail: email,
});
throw new BadRequestError('Invalid email address');
@@ -61,25 +55,24 @@ export class OwnerController {
if (!firstName || !lastName) {
this.logger.debug(
'Request to claim instance ownership failed because of missing first name or last name in payload',
{ userId, payload: req.body },
{ payload: req.body },
);
throw new BadRequestError('First and last names are mandatory');
}
let owner = req.user;
Object.assign(owner, {
email,
firstName,
lastName,
password: await this.passwordUtility.hash(validPassword),
let owner = await this.userRepository.findOneOrFail({
where: { role: 'global:owner' },
});
owner.email = email;
owner.firstName = firstName;
owner.lastName = lastName;
owner.password = await this.passwordUtility.hash(validPassword);
await validateEntity(owner);
owner = await this.userRepository.save(owner, { transaction: false });
this.logger.info('Owner was set up successfully', { userId });
this.logger.info('Owner was set up successfully');
await this.settingsRepository.update(
{ key: 'userManagement.isInstanceOwnerSetUp' },
@@ -88,19 +81,19 @@ export class OwnerController {
config.set('userManagement.isInstanceOwnerSetUp', true);
this.logger.debug('Setting isInstanceOwnerSetUp updated successfully', { userId });
this.logger.debug('Setting isInstanceOwnerSetUp updated successfully');
this.authService.issueCookie(res, owner);
void this.internalHooks.onInstanceOwnerSetup({ user_id: userId });
void this.internalHooks.onInstanceOwnerSetup({ user_id: owner.id });
return await this.userService.toPublic(owner, { posthog: this.postHog, withScopes: true });
}
@Post('/dismiss-banner')
@GlobalScope('banner:dismiss')
async dismissBanner(req: OwnerRequest.DismissBanner) {
const bannerName = 'banner' in req.body ? (req.body.banner as string) : '';
const response = await this.settingsRepository.dismissBanner({ bannerName });
return response;
return await this.settingsRepository.dismissBanner({ bannerName });
}
}

View File

@@ -50,6 +50,7 @@ export class PasswordResetController {
*/
@Post('/forgot-password', {
middlewares: !inTest ? [throttle] : [],
skipAuth: true,
})
async forgotPassword(req: PasswordResetRequest.Email) {
if (!this.mailer.isEmailSetUp) {
@@ -150,7 +151,7 @@ export class PasswordResetController {
/**
* Verify password reset token and user ID.
*/
@Get('/resolve-password-token')
@Get('/resolve-password-token', { skipAuth: true })
async resolvePasswordToken(req: PasswordResetRequest.Credentials) {
const { token } = req.query;
@@ -182,7 +183,7 @@ export class PasswordResetController {
/**
* Verify password reset token and update password.
*/
@Post('/change-password')
@Post('/change-password', { skipAuth: true })
async changePassword(req: PasswordResetRequest.NewPassword, res: Response) {
const { token, password, mfaToken } = req.body;

View File

@@ -1,20 +1,10 @@
import { Request, Response, NextFunction } from 'express';
import config from '@/config';
import {
Authorized,
Delete,
Get,
Middleware,
Patch,
Post,
RestController,
GlobalScope,
} from '@/decorators';
import { Delete, Get, Middleware, Patch, Post, RestController, GlobalScope } from '@/decorators';
import { TagService } from '@/services/tag.service';
import { TagsRequest } from '@/requests';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Authorized()
@RestController('/tags')
export class TagsController {
private config = config;

View File

@@ -1,7 +1,7 @@
import type { Request } from 'express';
import { join } from 'path';
import { access } from 'fs/promises';
import { Authorized, Get, RestController } from '@/decorators';
import { Get, RestController } from '@/decorators';
import config from '@/config';
import { NODES_BASE_DIR } from '@/constants';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@@ -15,7 +15,6 @@ export declare namespace TranslationRequest {
export type Credential = Request<{}, {}, {}, { credentialType: string }>;
}
@Authorized()
@RestController('/')
export class TranslationController {
constructor(private readonly credentialTypes: CredentialTypes) {}

View File

@@ -4,15 +4,7 @@ import { AuthService } from '@/auth/auth.service';
import { User } from '@db/entities/User';
import { SharedCredentials } from '@db/entities/SharedCredentials';
import { SharedWorkflow } from '@db/entities/SharedWorkflow';
import {
GlobalScope,
Authorized,
Delete,
Get,
RestController,
Patch,
Licensed,
} from '@/decorators';
import { GlobalScope, Delete, Get, RestController, Patch, Licensed } from '@/decorators';
import {
ListQuery,
UserRequest,
@@ -35,7 +27,6 @@ import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooks } from '@/InternalHooks';
import { validateEntity } from '@/GenericHelpers';
@Authorized()
@RestController('/users')
export class UsersController {
constructor(