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
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 126 additions and 299 deletions

View file

@ -1,6 +1,7 @@
export type DefaultOperations = 'create' | 'read' | 'update' | 'delete' | 'list'; export type DefaultOperations = 'create' | 'read' | 'update' | 'delete' | 'list';
export type Resource = export type Resource =
| 'auditLogs' | 'auditLogs'
| 'banner'
| 'communityPackage' | 'communityPackage'
| 'credential' | 'credential'
| 'externalSecretsProvider' | 'externalSecretsProvider'
@ -27,6 +28,7 @@ export type ResourceScope<
export type WildcardScope = `${Resource}:*` | '*'; export type WildcardScope = `${Resource}:*` | '*';
export type AuditLogsScope = ResourceScope<'auditLogs', 'manage'>; export type AuditLogsScope = ResourceScope<'auditLogs', 'manage'>;
export type BannerScope = ResourceScope<'banner', 'dismiss'>;
export type CommunityPackageScope = ResourceScope< export type CommunityPackageScope = ResourceScope<
'communityPackage', 'communityPackage',
'install' | 'uninstall' | 'update' | 'list' | 'manage' 'install' | 'uninstall' | 'update' | 'list' | 'manage'
@ -56,6 +58,7 @@ export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share
export type Scope = export type Scope =
| AuditLogsScope | AuditLogsScope
| BannerScope
| CommunityPackageScope | CommunityPackageScope
| CredentialScope | CredentialScope
| ExternalSecretProviderScope | ExternalSecretProviderScope

View file

@ -1,11 +1,10 @@
import { Authorized, Get, Post, RestController, GlobalScope } from '@/decorators'; import { Get, Post, RestController, GlobalScope } from '@/decorators';
import { ExternalSecretsRequest } from '@/requests'; import { ExternalSecretsRequest } from '@/requests';
import { Response } from 'express'; import { Response } from 'express';
import { ExternalSecretsService } from './ExternalSecrets.service.ee'; import { ExternalSecretsService } from './ExternalSecrets.service.ee';
import { ExternalSecretsProviderNotFoundError } from '@/errors/external-secrets-provider-not-found.error'; import { ExternalSecretsProviderNotFoundError } from '@/errors/external-secrets-provider-not-found.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
@Authorized()
@RestController('/external-secrets') @RestController('/external-secrets')
export class ExternalSecretsController { export class ExternalSecretsController {
constructor(private readonly secretsService: ExternalSecretsService) {} constructor(private readonly secretsService: ExternalSecretsService) {}

View file

@ -1,5 +1,5 @@
import pick from 'lodash/pick'; import pick from 'lodash/pick';
import { Authorized, Get, Post, Put, RestController, GlobalScope } from '@/decorators'; import { Get, Post, Put, RestController, GlobalScope } from '@/decorators';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@ -8,7 +8,6 @@ import { getLdapSynchronizations } from './helpers';
import { LdapConfiguration } from './types'; import { LdapConfiguration } from './types';
import { LdapService } from './ldap.service'; import { LdapService } from './ldap.service';
@Authorized()
@RestController('/ldap') @RestController('/ldap')
export class LdapController { export class LdapController {
constructor( constructor(

View file

@ -1,5 +1,5 @@
import { Service } from 'typedi'; import { Service } from 'typedi';
import type { Response } from 'express'; import type { NextFunction, Response } from 'express';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { JsonWebTokenError, TokenExpiredError, type JwtPayload } from 'jsonwebtoken'; import { JsonWebTokenError, TokenExpiredError, type JwtPayload } from 'jsonwebtoken';
@ -7,12 +7,11 @@ import config from '@/config';
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES, Time } from '@/constants'; import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES, Time } from '@/constants';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import type { AuthRole } from '@/decorators/types';
import { AuthError } from '@/errors/response-errors/auth.error'; import { AuthError } from '@/errors/response-errors/auth.error';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { License } from '@/License'; import { License } from '@/License';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import type { AuthenticatedRequest, AsyncRequestHandler } from '@/requests'; import type { AuthenticatedRequest } from '@/requests';
import { JwtService } from '@/services/jwt.service'; import { JwtService } from '@/services/jwt.service';
import { UrlService } from '@/services/url.service'; import { UrlService } from '@/services/url.service';
@ -31,54 +30,33 @@ interface IssuedJWT extends AuthJwtPayload {
@Service() @Service()
export class AuthService { export class AuthService {
private middlewareCache = new Map<string, AsyncRequestHandler>();
constructor( constructor(
private readonly logger: Logger, private readonly logger: Logger,
private readonly license: License, private readonly license: License,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly urlService: UrlService, private readonly urlService: UrlService,
private readonly userRepository: UserRepository, private readonly userRepository: UserRepository,
) {} ) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
this.authMiddleware = this.authMiddleware.bind(this);
}
createAuthMiddleware(authRole: AuthRole): AsyncRequestHandler { async authMiddleware(req: AuthenticatedRequest, res: Response, next: NextFunction) {
const { middlewareCache: cache } = this; const token = req.cookies[AUTH_COOKIE_NAME];
let authMiddleware = cache.get(authRole); if (token) {
if (authMiddleware) return authMiddleware; try {
req.user = await this.resolveJwt(token, res);
authMiddleware = async (req: AuthenticatedRequest, res, next) => { } catch (error) {
if (authRole === 'none') { if (error instanceof JsonWebTokenError || error instanceof AuthError) {
next(); this.clearCookie(res);
return; } else {
} throw error;
const token = req.cookies[AUTH_COOKIE_NAME];
if (token) {
try {
req.user = await this.resolveJwt(token, res);
} catch (error) {
if (error instanceof JsonWebTokenError || error instanceof AuthError) {
this.clearCookie(res);
} else {
throw error;
}
} }
} }
}
if (!req.user) { if (req.user) next();
res.status(401).json({ status: 'error', message: 'Unauthorized' }); else res.status(401).json({ status: 'error', message: 'Unauthorized' });
return;
}
if (authRole === 'any' || authRole === req.user.role) {
next();
} else {
res.status(403).json({ status: 'error', message: 'Forbidden' });
}
};
cache.set(authRole, authMiddleware);
return authMiddleware;
} }
clearCookie(res: Response) { clearCookie(res: Response) {

View file

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

View file

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

View file

@ -5,16 +5,7 @@ import {
STARTER_TEMPLATE_NAME, STARTER_TEMPLATE_NAME,
UNKNOWN_FAILURE_REASON, UNKNOWN_FAILURE_REASON,
} from '@/constants'; } from '@/constants';
import { import { Delete, Get, Middleware, Patch, Post, RestController, GlobalScope } from '@/decorators';
Authorized,
Delete,
Get,
Middleware,
Patch,
Post,
RestController,
GlobalScope,
} from '@/decorators';
import { NodeRequest } from '@/requests'; import { NodeRequest } from '@/requests';
import type { InstalledPackages } from '@db/entities/InstalledPackages'; import type { InstalledPackages } from '@db/entities/InstalledPackages';
import type { CommunityPackages } from '@/Interfaces'; 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; return typeof error === 'object' && error !== null && 'code' in error && 'stdout' in error;
} }
@Authorized()
@RestController('/community-packages') @RestController('/community-packages')
export class CommunityPackagesController { export class CommunityPackagesController {
constructor( constructor(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ import { Response } from 'express';
import { randomBytes } from 'crypto'; import { randomBytes } from 'crypto';
import { AuthService } from '@/auth/auth.service'; 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 { PasswordUtility } from '@/services/password.utility';
import { validateEntity } from '@/GenericHelpers'; import { validateEntity } from '@/GenericHelpers';
import type { User } from '@db/entities/User'; 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 { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UserRepository } from '@/databases/repositories/user.repository'; import { UserRepository } from '@/databases/repositories/user.repository';
@Authorized()
@RestController('/me') @RestController('/me')
export class MeController { export class MeController {
constructor( 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 { AuthenticatedRequest, MFA } from '@/requests';
import { MfaService } from '@/Mfa/mfa.service'; import { MfaService } from '@/Mfa/mfa.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Authorized()
@RestController('/mfa') @RestController('/mfa')
export class MFAController { export class MFAController {
constructor(private mfaService: MfaService) {} constructor(private mfaService: MfaService) {}

View file

@ -2,11 +2,10 @@ import { readFile } from 'fs/promises';
import get from 'lodash/get'; import get from 'lodash/get';
import { Request } from 'express'; import { Request } from 'express';
import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow'; import type { INodeTypeDescription, INodeTypeNameVersion } from 'n8n-workflow';
import { Authorized, Post, RestController } from '@/decorators'; import { Post, RestController } from '@/decorators';
import config from '@/config'; import config from '@/config';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
@Authorized()
@RestController('/node-types') @RestController('/node-types')
export class NodeTypesController { export class NodeTypesController {
constructor(private readonly nodeTypes: NodeTypes) {} 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 clientOAuth1 from 'oauth-1.0a';
import { createHmac } from 'crypto'; import { createHmac } from 'crypto';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { Authorized, Get, RestController } from '@/decorators'; import { Get, RestController } from '@/decorators';
import { OAuthRequest } from '@/requests'; import { OAuthRequest } from '@/requests';
import { sendErrorResponse } from '@/ResponseHelper'; import { sendErrorResponse } from '@/ResponseHelper';
import { AbstractOAuthController } from './abstractOAuth.controller'; import { AbstractOAuthController } from './abstractOAuth.controller';
@ -29,7 +29,6 @@ const algorithmMap = {
/* eslint-enable */ /* eslint-enable */
} as const; } as const;
@Authorized()
@RestController('/oauth1-credential') @RestController('/oauth1-credential')
export class OAuth1CredentialController extends AbstractOAuthController { export class OAuth1CredentialController extends AbstractOAuthController {
override oauthVersion = 1; override oauthVersion = 1;

View file

@ -8,7 +8,7 @@ import omit from 'lodash/omit';
import set from 'lodash/set'; import set from 'lodash/set';
import split from 'lodash/split'; import split from 'lodash/split';
import { ApplicationError, jsonParse, jsonStringify } from 'n8n-workflow'; import { ApplicationError, jsonParse, jsonStringify } from 'n8n-workflow';
import { Authorized, Get, RestController } from '@/decorators'; import { Get, RestController } from '@/decorators';
import { OAuthRequest } from '@/requests'; import { OAuthRequest } from '@/requests';
import { AbstractOAuthController } from './abstractOAuth.controller'; import { AbstractOAuthController } from './abstractOAuth.controller';
@ -17,7 +17,6 @@ interface CsrfStateParam {
token: string; token: string;
} }
@Authorized()
@RestController('/oauth2-credential') @RestController('/oauth2-credential')
export class OAuth2CredentialController extends AbstractOAuthController { export class OAuth2CredentialController extends AbstractOAuthController {
override oauthVersion = 2; 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 { OrchestrationRequest } from '@/requests';
import { OrchestrationService } from '@/services/orchestration.service'; import { OrchestrationService } from '@/services/orchestration.service';
import { License } from '@/License'; import { License } from '@/License';
@Authorized()
@RestController('/orchestration') @RestController('/orchestration')
export class OrchestrationController { export class OrchestrationController {
constructor( 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. * the workers to respond on Redis with their status.
*/ */
@GlobalScope('orchestration:read') @GlobalScope('orchestration:read')

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,14 +11,13 @@ import { License } from '@/License';
import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
import { OwnershipService } from '@/services/ownership.service'; import { OwnershipService } from '@/services/ownership.service';
import { EnterpriseCredentialsService } from './credentials.service.ee'; import { EnterpriseCredentialsService } from './credentials.service.ee';
import { Authorized, Delete, Get, Licensed, Patch, Post, Put, RestController } from '@/decorators'; import { Delete, Get, Licensed, Patch, Post, Put, RestController } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UserManagementMailer } from '@/UserManagement/email'; import { UserManagementMailer } from '@/UserManagement/email';
import * as Db from '@/Db'; import * as Db from '@/Db';
import * as utils from '@/utils'; import * as utils from '@/utils';
import { listQueryMiddleware } from '@/middlewares'; import { listQueryMiddleware } from '@/middlewares';
@Authorized()
@RestController('/credentials') @RestController('/credentials')
export class CredentialsController { export class CredentialsController {
constructor( constructor(

View file

@ -1,15 +0,0 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { CONTROLLER_AUTH_ROLES } from './constants';
import type { AuthRoleMetadata } from './types';
export function Authorized(authRole: AuthRoleMetadata[string] = 'any'): Function {
return function (target: Function | object, handlerName?: string) {
const controllerClass = handlerName ? target.constructor : target;
const authRoles = (Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) ??
{}) as AuthRoleMetadata;
authRoles[handlerName ?? '*'] = authRole;
Reflect.defineMetadata(CONTROLLER_AUTH_ROLES, authRoles, controllerClass);
};
}
export const NoAuthRequired = () => Authorized('none');

View file

@ -5,6 +5,8 @@ import type { Method, RouteMetadata } from './types';
interface RouteOptions { interface RouteOptions {
middlewares?: RequestHandler[]; middlewares?: RequestHandler[];
usesTemplates?: boolean; usesTemplates?: boolean;
/** When this flag is set to true, auth cookie isn't validated, and req.user will not be set */
skipAuth?: boolean;
} }
const RouteFactory = const RouteFactory =
@ -20,6 +22,7 @@ const RouteFactory =
middlewares: options.middlewares ?? [], middlewares: options.middlewares ?? [],
handlerName: String(handlerName), handlerName: String(handlerName),
usesTemplates: options.usesTemplates ?? false, usesTemplates: options.usesTemplates ?? false,
skipAuth: options.skipAuth ?? false,
}); });
Reflect.defineMetadata(CONTROLLER_ROUTES, routes, controllerClass); Reflect.defineMetadata(CONTROLLER_ROUTES, routes, controllerClass);
}; };

View file

@ -1,6 +1,5 @@
export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES'; export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES';
export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH'; export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH';
export const CONTROLLER_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES'; export const CONTROLLER_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES';
export const CONTROLLER_AUTH_ROLES = 'CONTROLLER_AUTH_ROLES';
export const CONTROLLER_LICENSE_FEATURES = 'CONTROLLER_LICENSE_FEATURES'; export const CONTROLLER_LICENSE_FEATURES = 'CONTROLLER_LICENSE_FEATURES';
export const CONTROLLER_REQUIRED_SCOPES = 'CONTROLLER_REQUIRED_SCOPES'; export const CONTROLLER_REQUIRED_SCOPES = 'CONTROLLER_REQUIRED_SCOPES';

View file

@ -1,4 +1,3 @@
export { Authorized, NoAuthRequired } from './Authorized';
export { RestController } from './RestController'; export { RestController } from './RestController';
export { Get, Post, Put, Patch, Delete } from './Route'; export { Get, Post, Put, Patch, Delete } from './Route';
export { Middleware } from './Middleware'; export { Middleware } from './Middleware';

View file

@ -12,7 +12,6 @@ import { License } from '@/License';
import type { AuthenticatedRequest } from '@/requests'; import type { AuthenticatedRequest } from '@/requests';
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
import { import {
CONTROLLER_AUTH_ROLES,
CONTROLLER_BASE_PATH, CONTROLLER_BASE_PATH,
CONTROLLER_LICENSE_FEATURES, CONTROLLER_LICENSE_FEATURES,
CONTROLLER_MIDDLEWARES, CONTROLLER_MIDDLEWARES,
@ -20,7 +19,6 @@ import {
CONTROLLER_ROUTES, CONTROLLER_ROUTES,
} from './constants'; } from './constants';
import type { import type {
AuthRoleMetadata,
Controller, Controller,
LicenseMetadata, LicenseMetadata,
MiddlewareMetadata, MiddlewareMetadata,
@ -74,9 +72,6 @@ export const registerController = (app: Application, controllerClass: Class<obje
extra: { controllerName: controllerClass.name }, extra: { controllerName: controllerClass.name },
}); });
const authRoles = Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) as
| AuthRoleMetadata
| undefined;
const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[]; const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[];
const licenseFeatures = Reflect.getMetadata(CONTROLLER_LICENSE_FEATURES, controllerClass) as const licenseFeatures = Reflect.getMetadata(CONTROLLER_LICENSE_FEATURES, controllerClass) as
| LicenseMetadata | LicenseMetadata
@ -99,15 +94,15 @@ export const registerController = (app: Application, controllerClass: Class<obje
const authService = Container.get(AuthService); const authService = Container.get(AuthService);
routes.forEach( routes.forEach(
({ method, path, middlewares: routeMiddlewares, handlerName, usesTemplates }) => { ({ method, path, middlewares: routeMiddlewares, handlerName, usesTemplates, skipAuth }) => {
const authRole = authRoles?.[handlerName] ?? authRoles?.['*'];
const features = licenseFeatures?.[handlerName] ?? licenseFeatures?.['*']; const features = licenseFeatures?.[handlerName] ?? licenseFeatures?.['*'];
const scopes = requiredScopes?.[handlerName] ?? requiredScopes?.['*']; const scopes = requiredScopes?.[handlerName] ?? requiredScopes?.['*'];
const handler = async (req: Request, res: Response) => const handler = async (req: Request, res: Response) =>
await controller[handlerName](req, res); await controller[handlerName](req, res);
router[method]( router[method](
path, path,
...(authRole ? [authService.createAuthMiddleware(authRole)] : []), // eslint-disable-next-line @typescript-eslint/unbound-method
...(skipAuth ? [] : [authService.authMiddleware]),
...(features ? [createLicenseMiddleware(features)] : []), ...(features ? [createLicenseMiddleware(features)] : []),
...(scopes ? [createGlobalScopeMiddleware(scopes)] : []), ...(scopes ? [createGlobalScopeMiddleware(scopes)] : []),
...controllerMiddlewares, ...controllerMiddlewares,

View file

@ -1,13 +1,9 @@
import type { Request, Response, RequestHandler } from 'express'; import type { Request, Response, RequestHandler } from 'express';
import type { GlobalRole } from '@db/entities/User';
import type { BooleanLicenseFeature } from '@/Interfaces'; import type { BooleanLicenseFeature } from '@/Interfaces';
import type { Scope } from '@n8n/permissions'; import type { Scope } from '@n8n/permissions';
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'; export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
export type AuthRole = GlobalRole | 'any' | 'none';
export type AuthRoleMetadata = Record<string, AuthRole>;
export type LicenseMetadata = Record<string, BooleanLicenseFeature[]>; export type LicenseMetadata = Record<string, BooleanLicenseFeature[]>;
export type ScopeMetadata = Record<string, Scope[]>; export type ScopeMetadata = Record<string, Scope[]>;
@ -22,6 +18,7 @@ export interface RouteMetadata {
handlerName: string; handlerName: string;
middlewares: RequestHandler[]; middlewares: RequestHandler[];
usesTemplates: boolean; usesTemplates: boolean;
skipAuth: boolean;
} }
export type Controller = Record< export type Controller = Record<

View file

@ -1,6 +1,6 @@
import type { PullResult } from 'simple-git'; import type { PullResult } from 'simple-git';
import express from 'express'; import express from 'express';
import { Authorized, Get, Post, Patch, RestController, GlobalScope } from '@/decorators'; import { Get, Post, Patch, RestController, GlobalScope } from '@/decorators';
import { import {
sourceControlLicensedMiddleware, sourceControlLicensedMiddleware,
sourceControlLicensedAndEnabledMiddleware, sourceControlLicensedAndEnabledMiddleware,
@ -17,7 +17,6 @@ import { getRepoType } from './sourceControlHelper.ee';
import { SourceControlGetStatus } from './types/sourceControlGetStatus'; import { SourceControlGetStatus } from './types/sourceControlGetStatus';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@Authorized()
@RestController('/source-control') @RestController('/source-control')
export class SourceControlController { export class SourceControlController {
constructor( constructor(
@ -26,8 +25,7 @@ export class SourceControlController {
private readonly internalHooks: InternalHooks, private readonly internalHooks: InternalHooks,
) {} ) {}
@Authorized('none') @Get('/preferences', { middlewares: [sourceControlLicensedMiddleware], skipAuth: true })
@Get('/preferences', { middlewares: [sourceControlLicensedMiddleware] })
async getPreferences(): Promise<SourceControlPreferences> { async getPreferences(): Promise<SourceControlPreferences> {
// returns the settings with the privateKey property redacted // returns the settings with the privateKey property redacted
return this.sourceControlPreferencesService.getPreferences(); return this.sourceControlPreferencesService.getPreferences();
@ -151,7 +149,6 @@ export class SourceControlController {
} }
} }
@Authorized('any')
@Get('/get-branches', { middlewares: [sourceControlLicensedMiddleware] }) @Get('/get-branches', { middlewares: [sourceControlLicensedMiddleware] })
async getBranches() { async getBranches() {
try { try {
@ -212,7 +209,6 @@ export class SourceControlController {
} }
} }
@Authorized('any')
@Get('/get-status', { middlewares: [sourceControlLicensedAndEnabledMiddleware] }) @Get('/get-status', { middlewares: [sourceControlLicensedAndEnabledMiddleware] })
async getStatus(req: SourceControlRequest.GetStatus) { async getStatus(req: SourceControlRequest.GetStatus) {
try { try {
@ -225,7 +221,6 @@ export class SourceControlController {
} }
} }
@Authorized('any')
@Get('/status', { middlewares: [sourceControlLicensedMiddleware] }) @Get('/status', { middlewares: [sourceControlLicensedMiddleware] })
async status(req: SourceControlRequest.GetStatus) { async status(req: SourceControlRequest.GetStatus) {
try { try {

View file

@ -1,21 +1,11 @@
import { VariablesRequest } from '@/requests'; import { VariablesRequest } from '@/requests';
import { import { Delete, Get, Licensed, Patch, Post, GlobalScope, RestController } from '@/decorators';
Authorized,
Delete,
Get,
Licensed,
Patch,
Post,
GlobalScope,
RestController,
} from '@/decorators';
import { VariablesService } from './variables.service.ee'; import { VariablesService } from './variables.service.ee';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { VariableValidationError } from '@/errors/variable-validation.error'; import { VariableValidationError } from '@/errors/variable-validation.error';
import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error'; import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error';
@Authorized()
@RestController('/variables') @RestController('/variables')
export class VariablesController { export class VariablesController {
constructor(private readonly variablesService: VariablesService) {} constructor(private readonly variablesService: VariablesService) {}

View file

@ -5,7 +5,7 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { MessageEventBusDestinationTypeNames } from 'n8n-workflow'; import { MessageEventBusDestinationTypeNames } from 'n8n-workflow';
import { RestController, Get, Post, Delete, Authorized, GlobalScope } from '@/decorators'; import { RestController, Get, Post, Delete, GlobalScope } from '@/decorators';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
@ -52,7 +52,6 @@ const isMessageEventBusDestinationOptions = (
// Controller // Controller
// ---------------------------------------- // ----------------------------------------
@Authorized()
@RestController('/eventbus') @RestController('/eventbus')
export class EventBusControllerEE { export class EventBusControllerEE {
constructor(private readonly eventBus: MessageEventBus) {} constructor(private readonly eventBus: MessageEventBus) {}

View file

@ -2,7 +2,7 @@ import express from 'express';
import type { IRunExecutionData } from 'n8n-workflow'; import type { IRunExecutionData } from 'n8n-workflow';
import { EventMessageTypeNames } from 'n8n-workflow'; import { EventMessageTypeNames } from 'n8n-workflow';
import { RestController, Get, Post, Authorized, GlobalScope } from '@/decorators'; import { RestController, Get, Post, GlobalScope } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { isEventMessageOptions } from './EventMessageClasses/AbstractEventMessage'; import { isEventMessageOptions } from './EventMessageClasses/AbstractEventMessage';
@ -33,7 +33,6 @@ const isWithQueryString = (candidate: unknown): candidate is { query: string } =
// Controller // Controller
// ---------------------------------------- // ----------------------------------------
@Authorized()
@RestController('/eventbus') @RestController('/eventbus')
export class EventBusController { export class EventBusController {
constructor( constructor(

View file

@ -1,7 +1,7 @@
import type { GetManyActiveFilter } from './execution.types'; import type { GetManyActiveFilter } from './execution.types';
import { ExecutionRequest } from './execution.types'; import { ExecutionRequest } from './execution.types';
import { ExecutionService } from './execution.service'; import { ExecutionService } from './execution.service';
import { Authorized, Get, Post, RestController } from '@/decorators'; import { Get, Post, RestController } from '@/decorators';
import { EnterpriseExecutionsService } from './execution.service.ee'; import { EnterpriseExecutionsService } from './execution.service.ee';
import { License } from '@/License'; import { License } from '@/License';
import { WorkflowSharingService } from '@/workflows/workflowSharing.service'; import { WorkflowSharingService } from '@/workflows/workflowSharing.service';
@ -11,7 +11,6 @@ import { jsonParse } from 'n8n-workflow';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { ActiveExecutionService } from './active-execution.service'; import { ActiveExecutionService } from './active-execution.service';
@Authorized()
@RestController('/executions') @RestController('/executions')
export class ExecutionsController { export class ExecutionsController {
private readonly isQueueMode = config.getEnv('executions.mode') === 'queue'; private readonly isQueueMode = config.getEnv('executions.mode') === 'queue';

View file

@ -1,8 +1,7 @@
import { Authorized, Get, Post, GlobalScope, RestController } from '@/decorators'; import { Get, Post, GlobalScope, RestController } from '@/decorators';
import { LicenseRequest } from '@/requests'; import { LicenseRequest } from '@/requests';
import { LicenseService } from './license.service'; import { LicenseService } from './license.service';
@Authorized()
@RestController('/license') @RestController('/license')
export class LicenseController { export class LicenseController {
constructor(private readonly licenseService: LicenseService) {} constructor(private readonly licenseService: LicenseService) {}

View file

@ -2,6 +2,7 @@ import type { Scope } from '@n8n/permissions';
export const ownerPermissions: Scope[] = [ export const ownerPermissions: Scope[] = [
'auditLogs:manage', 'auditLogs:manage',
'banner:dismiss',
'credential:create', 'credential:create',
'credential:read', 'credential:read',
'credential:update', 'credential:update',

View file

@ -104,7 +104,7 @@ export const setupPushHandler = (restEndpoint: string, app: Application) => {
const pushValidationMiddleware: RequestHandler = async ( const pushValidationMiddleware: RequestHandler = async (
req: SSEPushRequest | WebSocketPushRequest, req: SSEPushRequest | WebSocketPushRequest,
res, _,
next, next,
) => { ) => {
const ws = req.ws; const ws = req.ws;
@ -127,7 +127,8 @@ export const setupPushHandler = (restEndpoint: string, app: Application) => {
const authService = Container.get(AuthService); const authService = Container.get(AuthService);
app.use( app.use(
endpoint, endpoint,
authService.createAuthMiddleware('any'), // eslint-disable-next-line @typescript-eslint/unbound-method
authService.authMiddleware,
pushValidationMiddleware, pushValidationMiddleware,
(req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) => push.handleRequest(req, res), (req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) => push.handleRequest(req, res),
); );

View file

@ -1,4 +1,3 @@
import type { ParsedQs } from 'qs';
import type express from 'express'; import type express from 'express';
import type { import type {
BannerName, BannerName,
@ -21,17 +20,6 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { WorkflowHistory } from '@db/entities/WorkflowHistory'; import type { WorkflowHistory } from '@db/entities/WorkflowHistory';
export type AsyncRequestHandler<
Params = Record<string, string>,
ResBody = unknown,
ReqBody = unknown,
ReqQuery = ParsedQs,
> = (
req: express.Request<Params, ResBody, ReqBody, ReqQuery>,
res: express.Response<ResBody>,
next: () => void,
) => Promise<void>;
export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'lastName'> { export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'lastName'> {
@IsEmail() @IsEmail()
email: string; email: string;

View file

@ -3,8 +3,7 @@ import { validate } from 'class-validator';
import type { PostBindingContext } from 'samlify/types/src/entity'; import type { PostBindingContext } from 'samlify/types/src/entity';
import url from 'url'; import url from 'url';
import { Authorized, Get, NoAuthRequired, Post, RestController, GlobalScope } from '@/decorators'; import { Get, Post, RestController, GlobalScope } from '@/decorators';
import { AuthService } from '@/auth/auth.service'; import { AuthService } from '@/auth/auth.service';
import { AuthenticatedRequest } from '@/requests'; import { AuthenticatedRequest } from '@/requests';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
@ -31,7 +30,6 @@ import { SamlService } from '../saml.service.ee';
import { SamlConfiguration } from '../types/requests'; import { SamlConfiguration } from '../types/requests';
import { getInitSSOFormView } from '../views/initSsoPost'; import { getInitSSOFormView } from '../views/initSsoPost';
@Authorized()
@RestController('/sso/saml') @RestController('/sso/saml')
export class SamlController { export class SamlController {
constructor( constructor(
@ -41,8 +39,7 @@ export class SamlController {
private readonly internalHooks: InternalHooks, private readonly internalHooks: InternalHooks,
) {} ) {}
@NoAuthRequired() @Get(SamlUrls.metadata, { skipAuth: true })
@Get(SamlUrls.metadata)
async getServiceProviderMetadata(_: express.Request, res: express.Response) { async getServiceProviderMetadata(_: express.Request, res: express.Response) {
return res return res
.header('Content-Type', 'text/xml') .header('Content-Type', 'text/xml')
@ -53,7 +50,6 @@ export class SamlController {
* GET /sso/saml/config * GET /sso/saml/config
* Return SAML config * Return SAML config
*/ */
@Authorized('any')
@Get(SamlUrls.config, { middlewares: [samlLicensedMiddleware] }) @Get(SamlUrls.config, { middlewares: [samlLicensedMiddleware] })
async configGet() { async configGet() {
const prefs = this.samlService.samlPreferences; const prefs = this.samlService.samlPreferences;
@ -101,8 +97,7 @@ export class SamlController {
* GET /sso/saml/acs * GET /sso/saml/acs
* Assertion Consumer Service endpoint * Assertion Consumer Service endpoint
*/ */
@NoAuthRequired() @Get(SamlUrls.acs, { middlewares: [samlLicensedMiddleware], skipAuth: true })
@Get(SamlUrls.acs, { middlewares: [samlLicensedMiddleware] })
async acsGet(req: SamlConfiguration.AcsRequest, res: express.Response) { async acsGet(req: SamlConfiguration.AcsRequest, res: express.Response) {
return await this.acsHandler(req, res, 'redirect'); return await this.acsHandler(req, res, 'redirect');
} }
@ -111,8 +106,7 @@ export class SamlController {
* POST /sso/saml/acs * POST /sso/saml/acs
* Assertion Consumer Service endpoint * Assertion Consumer Service endpoint
*/ */
@NoAuthRequired() @Post(SamlUrls.acs, { middlewares: [samlLicensedMiddleware], skipAuth: true })
@Post(SamlUrls.acs, { middlewares: [samlLicensedMiddleware] })
async acsPost(req: SamlConfiguration.AcsRequest, res: express.Response) { async acsPost(req: SamlConfiguration.AcsRequest, res: express.Response) {
return await this.acsHandler(req, res, 'post'); return await this.acsHandler(req, res, 'post');
} }
@ -177,8 +171,7 @@ export class SamlController {
* Access URL for implementing SP-init SSO * Access URL for implementing SP-init SSO
* This endpoint is available if SAML is licensed and enabled * This endpoint is available if SAML is licensed and enabled
*/ */
@NoAuthRequired() @Get(SamlUrls.initSSO, { middlewares: [samlLicensedAndEnabledMiddleware], skipAuth: true })
@Get(SamlUrls.initSSO, { middlewares: [samlLicensedAndEnabledMiddleware] })
async initSsoGet(req: express.Request, res: express.Response) { async initSsoGet(req: express.Request, res: express.Response) {
let redirectUrl = ''; let redirectUrl = '';
try { try {

View file

@ -1,4 +1,4 @@
import { Authorized, RestController, Get, Middleware } from '@/decorators'; import { RestController, Get, Middleware } from '@/decorators';
import { WorkflowHistoryRequest } from '@/requests'; import { WorkflowHistoryRequest } from '@/requests';
import { WorkflowHistoryService } from './workflowHistory.service.ee'; import { WorkflowHistoryService } from './workflowHistory.service.ee';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
@ -11,7 +11,6 @@ import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-v
const DEFAULT_TAKE = 20; const DEFAULT_TAKE = 20;
@Authorized()
@RestController('/workflow-history') @RestController('/workflow-history')
export class WorkflowHistoryController { export class WorkflowHistoryController {
constructor(private readonly historyService: WorkflowHistoryService) {} constructor(private readonly historyService: WorkflowHistoryService) {}

View file

@ -8,7 +8,7 @@ import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import type { IWorkflowResponse } from '@/Interfaces'; import type { IWorkflowResponse } from '@/Interfaces';
import config from '@/config'; import config from '@/config';
import { Authorized, Delete, Get, Patch, Post, Put, RestController } from '@/decorators'; import { Delete, Get, Patch, Post, Put, RestController } from '@/decorators';
import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow'; import { SharedWorkflow, type WorkflowSharingRole } from '@db/entities/SharedWorkflow';
import { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
@ -39,7 +39,6 @@ import { WorkflowExecutionService } from './workflowExecution.service';
import { WorkflowSharingService } from './workflowSharing.service'; import { WorkflowSharingService } from './workflowSharing.service';
import { UserManagementMailer } from '@/UserManagement/email'; import { UserManagementMailer } from '@/UserManagement/email';
@Authorized()
@RestController('/workflows') @RestController('/workflows')
export class WorkflowsController { export class WorkflowsController {
constructor( constructor(

View file

@ -17,14 +17,12 @@ describe('Auth Middleware', () => {
['PATCH', '/me'], ['PATCH', '/me'],
['PATCH', '/me/password'], ['PATCH', '/me/password'],
['POST', '/me/survey'], ['POST', '/me/survey'],
['POST', '/owner/setup'],
]; ];
/** Routes requiring a valid `n8n-auth` cookie for an owner. */ /** Routes requiring a valid `n8n-auth` cookie for an owner. */
const ROUTES_REQUIRING_AUTHORIZATION: Readonly<Array<[string, string]>> = [ const ROUTES_REQUIRING_AUTHORIZATION: Readonly<Array<[string, string]>> = [
['POST', '/invitations'], ['POST', '/invitations'],
['DELETE', '/users/123'], ['DELETE', '/users/123'],
['POST', '/owner/setup'],
]; ];
describe('Routes requiring Authentication', () => { describe('Routes requiring Authentication', () => {

View file

@ -1,5 +1,4 @@
import validator from 'validator'; import validator from 'validator';
import type { SuperAgentTest } from 'supertest';
import config from '@/config'; import config from '@/config';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
@ -18,11 +17,9 @@ import Container from 'typedi';
const testServer = utils.setupTestServer({ endpointGroups: ['owner'] }); const testServer = utils.setupTestServer({ endpointGroups: ['owner'] });
let ownerShell: User; let ownerShell: User;
let authOwnerShellAgent: SuperAgentTest;
beforeEach(async () => { beforeEach(async () => {
ownerShell = await createUserShell('global:owner'); ownerShell = await createUserShell('global:owner');
authOwnerShellAgent = testServer.authAgentFor(ownerShell);
config.set('userManagement.isInstanceOwnerSetUp', false); config.set('userManagement.isInstanceOwnerSetUp', false);
}); });
@ -39,7 +36,7 @@ describe('POST /owner/setup', () => {
password: randomValidPassword(), password: randomValidPassword(),
}; };
const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData); const response = await testServer.authlessAgent.post('/owner/setup').send(newOwnerData);
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
@ -88,7 +85,7 @@ describe('POST /owner/setup', () => {
password: randomValidPassword(), password: randomValidPassword(),
}; };
const response = await authOwnerShellAgent.post('/owner/setup').send(newOwnerData); const response = await testServer.authlessAgent.post('/owner/setup').send(newOwnerData);
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
@ -150,7 +147,7 @@ describe('POST /owner/setup', () => {
test('should fail with invalid inputs', async () => { test('should fail with invalid inputs', async () => {
for (const invalidPayload of INVALID_POST_OWNER_PAYLOADS) { for (const invalidPayload of INVALID_POST_OWNER_PAYLOADS) {
const response = await authOwnerShellAgent.post('/owner/setup').send(invalidPayload); const response = await testServer.authlessAgent.post('/owner/setup').send(invalidPayload);
expect(response.statusCode).toBe(400); expect(response.statusCode).toBe(400);
} }
}); });

View file

@ -39,7 +39,7 @@ describe('AuthService', () => {
config.set('userManagement.jwtRefreshTimeoutHours', 0); config.set('userManagement.jwtRefreshTimeoutHours', 0);
}); });
describe('createAuthMiddleware', () => { describe('authMiddleware', () => {
const req = mock<AuthenticatedRequest>({ cookies: {}, user: undefined }); const req = mock<AuthenticatedRequest>({ cookies: {}, user: undefined });
const res = mock<Response>(); const res = mock<Response>();
const next = jest.fn() as NextFunction; const next = jest.fn() as NextFunction;
@ -48,64 +48,34 @@ describe('AuthService', () => {
res.status.mockReturnThis(); res.status.mockReturnThis();
}); });
describe('authRole = none', () => { it('should 401 if no cookie is set', async () => {
const authMiddleware = authService.createAuthMiddleware('none'); req.cookies[AUTH_COOKIE_NAME] = undefined;
await authService.authMiddleware(req, res, next);
it('should just skips auth checks', async () => { expect(next).not.toHaveBeenCalled();
await authMiddleware(req, res, next); expect(res.status).toHaveBeenCalledWith(401);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
}); });
describe('authRole = any', () => { it('should 401 and clear the cookie if the JWT is expired', async () => {
const authMiddleware = authService.createAuthMiddleware('any'); req.cookies[AUTH_COOKIE_NAME] = validToken;
jest.advanceTimersByTime(365 * Time.days.toMilliseconds);
it('should 401 if no cookie is set', async () => { await authService.authMiddleware(req, res, next);
req.cookies[AUTH_COOKIE_NAME] = undefined; expect(next).not.toHaveBeenCalled();
await authMiddleware(req, res, next); expect(res.status).toHaveBeenCalledWith(401);
expect(next).not.toHaveBeenCalled(); expect(res.clearCookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME);
expect(res.status).toHaveBeenCalledWith(401);
});
it('should 401 and clear the cookie if the JWT is expired', async () => {
req.cookies[AUTH_COOKIE_NAME] = validToken;
jest.advanceTimersByTime(365 * Time.days.toMilliseconds);
await authMiddleware(req, res, next);
expect(next).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(401);
expect(res.clearCookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME);
});
it('should refresh the cookie before it expires', async () => {
req.cookies[AUTH_COOKIE_NAME] = validToken;
jest.advanceTimersByTime(6 * Time.days.toMilliseconds);
userRepository.findOne.mockResolvedValue(user);
await authMiddleware(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.cookie).toHaveBeenCalledWith('n8n-auth', expect.any(String), {
httpOnly: true,
maxAge: 604800000,
sameSite: 'lax',
});
});
}); });
describe('authRole = global:owner', () => { it('should refresh the cookie before it expires', async () => {
const authMiddleware = authService.createAuthMiddleware('global:owner'); req.cookies[AUTH_COOKIE_NAME] = validToken;
jest.advanceTimersByTime(6 * Time.days.toMilliseconds);
userRepository.findOne.mockResolvedValue(user);
it('should 403 if the user does not have the correct role', async () => { await authService.authMiddleware(req, res, next);
req.cookies[AUTH_COOKIE_NAME] = validToken; expect(next).toHaveBeenCalled();
jest.advanceTimersByTime(6 * Time.days.toMilliseconds); expect(res.cookie).toHaveBeenCalledWith('n8n-auth', expect.any(String), {
userRepository.findOne.mockResolvedValue( httpOnly: true,
mock<User>({ ...userData, role: 'global:member' }), maxAge: 604800000,
); sameSite: 'lax',
await authMiddleware(req, res, next);
expect(next).not.toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(403);
}); });
}); });
}); });

View file

@ -93,11 +93,15 @@ describe('OwnerController', () => {
}); });
const res = mock<Response>(); const res = mock<Response>();
configGetSpy.mockReturnValue(false); configGetSpy.mockReturnValue(false);
userRepository.findOneOrFail.calledWith(anyObject()).mockResolvedValue(user);
userRepository.save.calledWith(anyObject()).mockResolvedValue(user); userRepository.save.calledWith(anyObject()).mockResolvedValue(user);
jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token');
await controller.setupOwner(req, res); await controller.setupOwner(req, res);
expect(userRepository.findOneOrFail).toHaveBeenCalledWith({
where: { role: 'global:owner' },
});
expect(userRepository.save).toHaveBeenCalledWith(user, { transaction: false }); expect(userRepository.save).toHaveBeenCalledWith(user, { transaction: false });
expect(authService.issueCookie).toHaveBeenCalledWith(res, user); expect(authService.issueCookie).toHaveBeenCalledWith(res, user);
}); });