diff --git a/packages/@n8n/permissions/src/types.ts b/packages/@n8n/permissions/src/types.ts index 6dce8947d6..1707d1c35e 100644 --- a/packages/@n8n/permissions/src/types.ts +++ b/packages/@n8n/permissions/src/types.ts @@ -1,6 +1,7 @@ export type DefaultOperations = 'create' | 'read' | 'update' | 'delete' | 'list'; export type Resource = | 'auditLogs' + | 'banner' | 'communityPackage' | 'credential' | 'externalSecretsProvider' @@ -27,6 +28,7 @@ export type ResourceScope< export type WildcardScope = `${Resource}:*` | '*'; export type AuditLogsScope = ResourceScope<'auditLogs', 'manage'>; +export type BannerScope = ResourceScope<'banner', 'dismiss'>; export type CommunityPackageScope = ResourceScope< 'communityPackage', 'install' | 'uninstall' | 'update' | 'list' | 'manage' @@ -56,6 +58,7 @@ export type WorkflowScope = ResourceScope<'workflow', DefaultOperations | 'share export type Scope = | AuditLogsScope + | BannerScope | CommunityPackageScope | CredentialScope | ExternalSecretProviderScope diff --git a/packages/cli/src/ExternalSecrets/ExternalSecrets.controller.ee.ts b/packages/cli/src/ExternalSecrets/ExternalSecrets.controller.ee.ts index 1be5bc7a42..86a61b75a0 100644 --- a/packages/cli/src/ExternalSecrets/ExternalSecrets.controller.ee.ts +++ b/packages/cli/src/ExternalSecrets/ExternalSecrets.controller.ee.ts @@ -1,11 +1,10 @@ -import { Authorized, Get, Post, RestController, GlobalScope } from '@/decorators'; +import { Get, Post, RestController, GlobalScope } from '@/decorators'; import { ExternalSecretsRequest } from '@/requests'; import { Response } from 'express'; import { ExternalSecretsService } from './ExternalSecrets.service.ee'; import { ExternalSecretsProviderNotFoundError } from '@/errors/external-secrets-provider-not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; -@Authorized() @RestController('/external-secrets') export class ExternalSecretsController { constructor(private readonly secretsService: ExternalSecretsService) {} diff --git a/packages/cli/src/Ldap/ldap.controller.ts b/packages/cli/src/Ldap/ldap.controller.ts index cb408cf1c9..e7715630bf 100644 --- a/packages/cli/src/Ldap/ldap.controller.ts +++ b/packages/cli/src/Ldap/ldap.controller.ts @@ -1,5 +1,5 @@ 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 { BadRequestError } from '@/errors/response-errors/bad-request.error'; @@ -8,7 +8,6 @@ import { getLdapSynchronizations } from './helpers'; import { LdapConfiguration } from './types'; import { LdapService } from './ldap.service'; -@Authorized() @RestController('/ldap') export class LdapController { constructor( diff --git a/packages/cli/src/auth/auth.service.ts b/packages/cli/src/auth/auth.service.ts index a78360ddc6..6e5ddd4be5 100644 --- a/packages/cli/src/auth/auth.service.ts +++ b/packages/cli/src/auth/auth.service.ts @@ -1,5 +1,5 @@ import { Service } from 'typedi'; -import type { Response } from 'express'; +import type { NextFunction, Response } from 'express'; import { createHash } from 'crypto'; 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 type { User } from '@db/entities/User'; import { UserRepository } from '@db/repositories/user.repository'; -import type { AuthRole } from '@/decorators/types'; import { AuthError } from '@/errors/response-errors/auth.error'; import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; import { License } from '@/License'; import { Logger } from '@/Logger'; -import type { AuthenticatedRequest, AsyncRequestHandler } from '@/requests'; +import type { AuthenticatedRequest } from '@/requests'; import { JwtService } from '@/services/jwt.service'; import { UrlService } from '@/services/url.service'; @@ -31,54 +30,33 @@ interface IssuedJWT extends AuthJwtPayload { @Service() export class AuthService { - private middlewareCache = new Map(); - constructor( private readonly logger: Logger, private readonly license: License, private readonly jwtService: JwtService, private readonly urlService: UrlService, private readonly userRepository: UserRepository, - ) {} + ) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.authMiddleware = this.authMiddleware.bind(this); + } - createAuthMiddleware(authRole: AuthRole): AsyncRequestHandler { - const { middlewareCache: cache } = this; - let authMiddleware = cache.get(authRole); - if (authMiddleware) return authMiddleware; - - authMiddleware = async (req: AuthenticatedRequest, res, next) => { - if (authRole === 'none') { - next(); - return; - } - - 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; - } + async authMiddleware(req: AuthenticatedRequest, res: Response, next: NextFunction) { + 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) { - 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; + if (req.user) next(); + else res.status(401).json({ status: 'error', message: 'Unauthorized' }); } clearCookie(res: Response) { diff --git a/packages/cli/src/controllers/activeWorkflows.controller.ts b/packages/cli/src/controllers/activeWorkflows.controller.ts index b86c2c4bad..20e75208c8 100644 --- a/packages/cli/src/controllers/activeWorkflows.controller.ts +++ b/packages/cli/src/controllers/activeWorkflows.controller.ts @@ -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) {} diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index eda3914b36..7dd4e0b630 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -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 { 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 { 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); diff --git a/packages/cli/src/controllers/communityPackages.controller.ts b/packages/cli/src/controllers/communityPackages.controller.ts index 918a3e3dca..681acd9d23 100644 --- a/packages/cli/src/controllers/communityPackages.controller.ts +++ b/packages/cli/src/controllers/communityPackages.controller.ts @@ -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( diff --git a/packages/cli/src/controllers/cta.controller.ts b/packages/cli/src/controllers/cta.controller.ts index 9d06ff0812..5cd41a1dcd 100644 --- a/packages/cli/src/controllers/cta.controller.ts +++ b/packages/cli/src/controllers/cta.controller.ts @@ -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) {} diff --git a/packages/cli/src/controllers/debug.controller.ts b/packages/cli/src/controllers/debug.controller.ts index b74ccd5840..d689be18f8 100644 --- a/packages/cli/src/controllers/debug.controller.ts +++ b/packages/cli/src/controllers/debug.controller.ts @@ -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(); diff --git a/packages/cli/src/controllers/dynamicNodeParameters.controller.ts b/packages/cli/src/controllers/dynamicNodeParameters.controller.ts index 8a3fba26bb..0c70862956 100644 --- a/packages/cli/src/controllers/dynamicNodeParameters.controller.ts +++ b/packages/cli/src/controllers/dynamicNodeParameters.controller.ts @@ -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.'); diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts index 1435c9064f..0656e6f181 100644 --- a/packages/cli/src/controllers/e2e.controller.ts +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -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 = { @@ -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'); diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index 1e7ee6a799..39a13792cd 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -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; diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index 5c5d8a9657..ab09935225 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -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( diff --git a/packages/cli/src/controllers/mfa.controller.ts b/packages/cli/src/controllers/mfa.controller.ts index 9d4f370cd0..3c58b944d5 100644 --- a/packages/cli/src/controllers/mfa.controller.ts +++ b/packages/cli/src/controllers/mfa.controller.ts @@ -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) {} diff --git a/packages/cli/src/controllers/nodeTypes.controller.ts b/packages/cli/src/controllers/nodeTypes.controller.ts index ad0b20efbc..fddefb7e10 100644 --- a/packages/cli/src/controllers/nodeTypes.controller.ts +++ b/packages/cli/src/controllers/nodeTypes.controller.ts @@ -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) {} diff --git a/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts b/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts index 761e6b15b3..0c3f0fb204 100644 --- a/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts +++ b/packages/cli/src/controllers/oauth/oAuth1Credential.controller.ts @@ -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; diff --git a/packages/cli/src/controllers/oauth/oAuth2Credential.controller.ts b/packages/cli/src/controllers/oauth/oAuth2Credential.controller.ts index a24f105f2b..66aec58e99 100644 --- a/packages/cli/src/controllers/oauth/oAuth2Credential.controller.ts +++ b/packages/cli/src/controllers/oauth/oAuth2Credential.controller.ts @@ -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; diff --git a/packages/cli/src/controllers/orchestration.controller.ts b/packages/cli/src/controllers/orchestration.controller.ts index 6ef467effd..74a9665e1e 100644 --- a/packages/cli/src/controllers/orchestration.controller.ts +++ b/packages/cli/src/controllers/orchestration.controller.ts @@ -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') diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index f1b24a2853..d0b910f780 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -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 }); } } diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index 6eb073be13..041d667dac 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -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; diff --git a/packages/cli/src/controllers/tags.controller.ts b/packages/cli/src/controllers/tags.controller.ts index d7c7c6fced..f097002eac 100644 --- a/packages/cli/src/controllers/tags.controller.ts +++ b/packages/cli/src/controllers/tags.controller.ts @@ -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; diff --git a/packages/cli/src/controllers/translation.controller.ts b/packages/cli/src/controllers/translation.controller.ts index 7b5f6bef57..f359ec2b3a 100644 --- a/packages/cli/src/controllers/translation.controller.ts +++ b/packages/cli/src/controllers/translation.controller.ts @@ -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) {} diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index 65dbe40010..baa441fe36 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -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( diff --git a/packages/cli/src/credentials/credentials.controller.ts b/packages/cli/src/credentials/credentials.controller.ts index 6dc62814f0..884f8ee56b 100644 --- a/packages/cli/src/credentials/credentials.controller.ts +++ b/packages/cli/src/credentials/credentials.controller.ts @@ -11,14 +11,13 @@ import { License } from '@/License'; import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; import { OwnershipService } from '@/services/ownership.service'; 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 { UserManagementMailer } from '@/UserManagement/email'; import * as Db from '@/Db'; import * as utils from '@/utils'; import { listQueryMiddleware } from '@/middlewares'; -@Authorized() @RestController('/credentials') export class CredentialsController { constructor( diff --git a/packages/cli/src/decorators/Authorized.ts b/packages/cli/src/decorators/Authorized.ts deleted file mode 100644 index 4e97972a89..0000000000 --- a/packages/cli/src/decorators/Authorized.ts +++ /dev/null @@ -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'); diff --git a/packages/cli/src/decorators/Route.ts b/packages/cli/src/decorators/Route.ts index 64dd96d293..420a168b00 100644 --- a/packages/cli/src/decorators/Route.ts +++ b/packages/cli/src/decorators/Route.ts @@ -5,6 +5,8 @@ import type { Method, RouteMetadata } from './types'; interface RouteOptions { middlewares?: RequestHandler[]; 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 = @@ -20,6 +22,7 @@ const RouteFactory = middlewares: options.middlewares ?? [], handlerName: String(handlerName), usesTemplates: options.usesTemplates ?? false, + skipAuth: options.skipAuth ?? false, }); Reflect.defineMetadata(CONTROLLER_ROUTES, routes, controllerClass); }; diff --git a/packages/cli/src/decorators/constants.ts b/packages/cli/src/decorators/constants.ts index ba3d8a3147..1487f91a0f 100644 --- a/packages/cli/src/decorators/constants.ts +++ b/packages/cli/src/decorators/constants.ts @@ -1,6 +1,5 @@ export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES'; export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH'; 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_REQUIRED_SCOPES = 'CONTROLLER_REQUIRED_SCOPES'; diff --git a/packages/cli/src/decorators/index.ts b/packages/cli/src/decorators/index.ts index 934e333fe7..94c94ef184 100644 --- a/packages/cli/src/decorators/index.ts +++ b/packages/cli/src/decorators/index.ts @@ -1,4 +1,3 @@ -export { Authorized, NoAuthRequired } from './Authorized'; export { RestController } from './RestController'; export { Get, Post, Put, Patch, Delete } from './Route'; export { Middleware } from './Middleware'; diff --git a/packages/cli/src/decorators/registerController.ts b/packages/cli/src/decorators/registerController.ts index 3bb04b7d77..d17799c00c 100644 --- a/packages/cli/src/decorators/registerController.ts +++ b/packages/cli/src/decorators/registerController.ts @@ -12,7 +12,6 @@ import { License } from '@/License'; import type { AuthenticatedRequest } from '@/requests'; import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file import { - CONTROLLER_AUTH_ROLES, CONTROLLER_BASE_PATH, CONTROLLER_LICENSE_FEATURES, CONTROLLER_MIDDLEWARES, @@ -20,7 +19,6 @@ import { CONTROLLER_ROUTES, } from './constants'; import type { - AuthRoleMetadata, Controller, LicenseMetadata, MiddlewareMetadata, @@ -74,9 +72,6 @@ export const registerController = (app: Application, controllerClass: Class { - const authRole = authRoles?.[handlerName] ?? authRoles?.['*']; + ({ method, path, middlewares: routeMiddlewares, handlerName, usesTemplates, skipAuth }) => { const features = licenseFeatures?.[handlerName] ?? licenseFeatures?.['*']; const scopes = requiredScopes?.[handlerName] ?? requiredScopes?.['*']; const handler = async (req: Request, res: Response) => await controller[handlerName](req, res); router[method]( path, - ...(authRole ? [authService.createAuthMiddleware(authRole)] : []), + // eslint-disable-next-line @typescript-eslint/unbound-method + ...(skipAuth ? [] : [authService.authMiddleware]), ...(features ? [createLicenseMiddleware(features)] : []), ...(scopes ? [createGlobalScopeMiddleware(scopes)] : []), ...controllerMiddlewares, diff --git a/packages/cli/src/decorators/types.ts b/packages/cli/src/decorators/types.ts index bbaccf39ab..6a8b14e0fa 100644 --- a/packages/cli/src/decorators/types.ts +++ b/packages/cli/src/decorators/types.ts @@ -1,13 +1,9 @@ import type { Request, Response, RequestHandler } from 'express'; -import type { GlobalRole } from '@db/entities/User'; import type { BooleanLicenseFeature } from '@/Interfaces'; import type { Scope } from '@n8n/permissions'; export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'; -export type AuthRole = GlobalRole | 'any' | 'none'; -export type AuthRoleMetadata = Record; - export type LicenseMetadata = Record; export type ScopeMetadata = Record; @@ -22,6 +18,7 @@ export interface RouteMetadata { handlerName: string; middlewares: RequestHandler[]; usesTemplates: boolean; + skipAuth: boolean; } export type Controller = Record< diff --git a/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts b/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts index 5fdf0e73c3..0cd0dde9a9 100644 --- a/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControl.controller.ee.ts @@ -1,6 +1,6 @@ import type { PullResult } from 'simple-git'; import express from 'express'; -import { Authorized, Get, Post, Patch, RestController, GlobalScope } from '@/decorators'; +import { Get, Post, Patch, RestController, GlobalScope } from '@/decorators'; import { sourceControlLicensedMiddleware, sourceControlLicensedAndEnabledMiddleware, @@ -17,7 +17,6 @@ import { getRepoType } from './sourceControlHelper.ee'; import { SourceControlGetStatus } from './types/sourceControlGetStatus'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; -@Authorized() @RestController('/source-control') export class SourceControlController { constructor( @@ -26,8 +25,7 @@ export class SourceControlController { private readonly internalHooks: InternalHooks, ) {} - @Authorized('none') - @Get('/preferences', { middlewares: [sourceControlLicensedMiddleware] }) + @Get('/preferences', { middlewares: [sourceControlLicensedMiddleware], skipAuth: true }) async getPreferences(): Promise { // returns the settings with the privateKey property redacted return this.sourceControlPreferencesService.getPreferences(); @@ -151,7 +149,6 @@ export class SourceControlController { } } - @Authorized('any') @Get('/get-branches', { middlewares: [sourceControlLicensedMiddleware] }) async getBranches() { try { @@ -212,7 +209,6 @@ export class SourceControlController { } } - @Authorized('any') @Get('/get-status', { middlewares: [sourceControlLicensedAndEnabledMiddleware] }) async getStatus(req: SourceControlRequest.GetStatus) { try { @@ -225,7 +221,6 @@ export class SourceControlController { } } - @Authorized('any') @Get('/status', { middlewares: [sourceControlLicensedMiddleware] }) async status(req: SourceControlRequest.GetStatus) { try { diff --git a/packages/cli/src/environments/variables/variables.controller.ee.ts b/packages/cli/src/environments/variables/variables.controller.ee.ts index f080b3d0b8..16a59f26d8 100644 --- a/packages/cli/src/environments/variables/variables.controller.ee.ts +++ b/packages/cli/src/environments/variables/variables.controller.ee.ts @@ -1,21 +1,11 @@ import { VariablesRequest } from '@/requests'; -import { - Authorized, - Delete, - Get, - Licensed, - Patch, - Post, - GlobalScope, - RestController, -} from '@/decorators'; +import { Delete, Get, Licensed, Patch, Post, GlobalScope, RestController } from '@/decorators'; import { VariablesService } from './variables.service.ee'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { VariableValidationError } from '@/errors/variable-validation.error'; import { VariableCountLimitReachedError } from '@/errors/variable-count-limit-reached.error'; -@Authorized() @RestController('/variables') export class VariablesController { constructor(private readonly variablesService: VariablesService) {} diff --git a/packages/cli/src/eventbus/eventBus.controller.ee.ts b/packages/cli/src/eventbus/eventBus.controller.ee.ts index c8b649cbc4..95433a359b 100644 --- a/packages/cli/src/eventbus/eventBus.controller.ee.ts +++ b/packages/cli/src/eventbus/eventBus.controller.ee.ts @@ -5,7 +5,7 @@ import type { } 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 { BadRequestError } from '@/errors/response-errors/bad-request.error'; @@ -52,7 +52,6 @@ const isMessageEventBusDestinationOptions = ( // Controller // ---------------------------------------- -@Authorized() @RestController('/eventbus') export class EventBusControllerEE { constructor(private readonly eventBus: MessageEventBus) {} diff --git a/packages/cli/src/eventbus/eventBus.controller.ts b/packages/cli/src/eventbus/eventBus.controller.ts index 84d2feb131..179d44da41 100644 --- a/packages/cli/src/eventbus/eventBus.controller.ts +++ b/packages/cli/src/eventbus/eventBus.controller.ts @@ -2,7 +2,7 @@ import express from 'express'; import type { IRunExecutionData } 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 { isEventMessageOptions } from './EventMessageClasses/AbstractEventMessage'; @@ -33,7 +33,6 @@ const isWithQueryString = (candidate: unknown): candidate is { query: string } = // Controller // ---------------------------------------- -@Authorized() @RestController('/eventbus') export class EventBusController { constructor( diff --git a/packages/cli/src/executions/executions.controller.ts b/packages/cli/src/executions/executions.controller.ts index aa9c214800..3d778b2bc7 100644 --- a/packages/cli/src/executions/executions.controller.ts +++ b/packages/cli/src/executions/executions.controller.ts @@ -1,7 +1,7 @@ import type { GetManyActiveFilter } from './execution.types'; import { ExecutionRequest } from './execution.types'; 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 { License } from '@/License'; 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 { ActiveExecutionService } from './active-execution.service'; -@Authorized() @RestController('/executions') export class ExecutionsController { private readonly isQueueMode = config.getEnv('executions.mode') === 'queue'; diff --git a/packages/cli/src/license/license.controller.ts b/packages/cli/src/license/license.controller.ts index c2b1bde830..086ab3d4f8 100644 --- a/packages/cli/src/license/license.controller.ts +++ b/packages/cli/src/license/license.controller.ts @@ -1,8 +1,7 @@ -import { Authorized, Get, Post, GlobalScope, RestController } from '@/decorators'; +import { Get, Post, GlobalScope, RestController } from '@/decorators'; import { LicenseRequest } from '@/requests'; import { LicenseService } from './license.service'; -@Authorized() @RestController('/license') export class LicenseController { constructor(private readonly licenseService: LicenseService) {} diff --git a/packages/cli/src/permissions/roles.ts b/packages/cli/src/permissions/roles.ts index 39d9c649f7..68d61af0b2 100644 --- a/packages/cli/src/permissions/roles.ts +++ b/packages/cli/src/permissions/roles.ts @@ -2,6 +2,7 @@ import type { Scope } from '@n8n/permissions'; export const ownerPermissions: Scope[] = [ 'auditLogs:manage', + 'banner:dismiss', 'credential:create', 'credential:read', 'credential:update', diff --git a/packages/cli/src/push/index.ts b/packages/cli/src/push/index.ts index 14cff7887e..51b0ad05d8 100644 --- a/packages/cli/src/push/index.ts +++ b/packages/cli/src/push/index.ts @@ -104,7 +104,7 @@ export const setupPushHandler = (restEndpoint: string, app: Application) => { const pushValidationMiddleware: RequestHandler = async ( req: SSEPushRequest | WebSocketPushRequest, - res, + _, next, ) => { const ws = req.ws; @@ -127,7 +127,8 @@ export const setupPushHandler = (restEndpoint: string, app: Application) => { const authService = Container.get(AuthService); app.use( endpoint, - authService.createAuthMiddleware('any'), + // eslint-disable-next-line @typescript-eslint/unbound-method + authService.authMiddleware, pushValidationMiddleware, (req: SSEPushRequest | WebSocketPushRequest, res: PushResponse) => push.handleRequest(req, res), ); diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 02436390b5..d7a6911c09 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -1,4 +1,3 @@ -import type { ParsedQs } from 'qs'; import type express from 'express'; import type { BannerName, @@ -21,17 +20,6 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { WorkflowHistory } from '@db/entities/WorkflowHistory'; -export type AsyncRequestHandler< - Params = Record, - ResBody = unknown, - ReqBody = unknown, - ReqQuery = ParsedQs, -> = ( - req: express.Request, - res: express.Response, - next: () => void, -) => Promise; - export class UserUpdatePayload implements Pick { @IsEmail() email: string; diff --git a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts index c7fb89ac1f..597fdfb93c 100644 --- a/packages/cli/src/sso/saml/routes/saml.controller.ee.ts +++ b/packages/cli/src/sso/saml/routes/saml.controller.ee.ts @@ -3,8 +3,7 @@ import { validate } from 'class-validator'; import type { PostBindingContext } from 'samlify/types/src/entity'; 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 { AuthenticatedRequest } from '@/requests'; import { InternalHooks } from '@/InternalHooks'; @@ -31,7 +30,6 @@ import { SamlService } from '../saml.service.ee'; import { SamlConfiguration } from '../types/requests'; import { getInitSSOFormView } from '../views/initSsoPost'; -@Authorized() @RestController('/sso/saml') export class SamlController { constructor( @@ -41,8 +39,7 @@ export class SamlController { private readonly internalHooks: InternalHooks, ) {} - @NoAuthRequired() - @Get(SamlUrls.metadata) + @Get(SamlUrls.metadata, { skipAuth: true }) async getServiceProviderMetadata(_: express.Request, res: express.Response) { return res .header('Content-Type', 'text/xml') @@ -53,7 +50,6 @@ export class SamlController { * GET /sso/saml/config * Return SAML config */ - @Authorized('any') @Get(SamlUrls.config, { middlewares: [samlLicensedMiddleware] }) async configGet() { const prefs = this.samlService.samlPreferences; @@ -101,8 +97,7 @@ export class SamlController { * GET /sso/saml/acs * Assertion Consumer Service endpoint */ - @NoAuthRequired() - @Get(SamlUrls.acs, { middlewares: [samlLicensedMiddleware] }) + @Get(SamlUrls.acs, { middlewares: [samlLicensedMiddleware], skipAuth: true }) async acsGet(req: SamlConfiguration.AcsRequest, res: express.Response) { return await this.acsHandler(req, res, 'redirect'); } @@ -111,8 +106,7 @@ export class SamlController { * POST /sso/saml/acs * Assertion Consumer Service endpoint */ - @NoAuthRequired() - @Post(SamlUrls.acs, { middlewares: [samlLicensedMiddleware] }) + @Post(SamlUrls.acs, { middlewares: [samlLicensedMiddleware], skipAuth: true }) async acsPost(req: SamlConfiguration.AcsRequest, res: express.Response) { return await this.acsHandler(req, res, 'post'); } @@ -177,8 +171,7 @@ export class SamlController { * Access URL for implementing SP-init SSO * This endpoint is available if SAML is licensed and enabled */ - @NoAuthRequired() - @Get(SamlUrls.initSSO, { middlewares: [samlLicensedAndEnabledMiddleware] }) + @Get(SamlUrls.initSSO, { middlewares: [samlLicensedAndEnabledMiddleware], skipAuth: true }) async initSsoGet(req: express.Request, res: express.Response) { let redirectUrl = ''; try { diff --git a/packages/cli/src/workflows/workflowHistory/workflowHistory.controller.ee.ts b/packages/cli/src/workflows/workflowHistory/workflowHistory.controller.ee.ts index 2ac2cf4233..c57e3cb0b5 100644 --- a/packages/cli/src/workflows/workflowHistory/workflowHistory.controller.ee.ts +++ b/packages/cli/src/workflows/workflowHistory/workflowHistory.controller.ee.ts @@ -1,4 +1,4 @@ -import { Authorized, RestController, Get, Middleware } from '@/decorators'; +import { RestController, Get, Middleware } from '@/decorators'; import { WorkflowHistoryRequest } from '@/requests'; import { WorkflowHistoryService } from './workflowHistory.service.ee'; import { Request, Response, NextFunction } from 'express'; @@ -11,7 +11,6 @@ import { WorkflowHistoryVersionNotFoundError } from '@/errors/workflow-history-v const DEFAULT_TAKE = 20; -@Authorized() @RestController('/workflow-history') export class WorkflowHistoryController { constructor(private readonly historyService: WorkflowHistoryService) {} diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 807b892cc3..d740667db7 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -8,7 +8,7 @@ import * as ResponseHelper from '@/ResponseHelper'; import * as WorkflowHelpers from '@/WorkflowHelpers'; import type { IWorkflowResponse } from '@/Interfaces'; 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 { WorkflowEntity } from '@db/entities/WorkflowEntity'; import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository'; @@ -39,7 +39,6 @@ import { WorkflowExecutionService } from './workflowExecution.service'; import { WorkflowSharingService } from './workflowSharing.service'; import { UserManagementMailer } from '@/UserManagement/email'; -@Authorized() @RestController('/workflows') export class WorkflowsController { constructor( diff --git a/packages/cli/test/integration/auth.mw.test.ts b/packages/cli/test/integration/auth.mw.test.ts index e4b1b974bb..8f40759f96 100644 --- a/packages/cli/test/integration/auth.mw.test.ts +++ b/packages/cli/test/integration/auth.mw.test.ts @@ -17,14 +17,12 @@ describe('Auth Middleware', () => { ['PATCH', '/me'], ['PATCH', '/me/password'], ['POST', '/me/survey'], - ['POST', '/owner/setup'], ]; /** Routes requiring a valid `n8n-auth` cookie for an owner. */ const ROUTES_REQUIRING_AUTHORIZATION: Readonly> = [ ['POST', '/invitations'], ['DELETE', '/users/123'], - ['POST', '/owner/setup'], ]; describe('Routes requiring Authentication', () => { diff --git a/packages/cli/test/integration/owner.api.test.ts b/packages/cli/test/integration/owner.api.test.ts index cbe8a84c91..ff5dffe854 100644 --- a/packages/cli/test/integration/owner.api.test.ts +++ b/packages/cli/test/integration/owner.api.test.ts @@ -1,5 +1,4 @@ import validator from 'validator'; -import type { SuperAgentTest } from 'supertest'; import config from '@/config'; import type { User } from '@db/entities/User'; @@ -18,11 +17,9 @@ import Container from 'typedi'; const testServer = utils.setupTestServer({ endpointGroups: ['owner'] }); let ownerShell: User; -let authOwnerShellAgent: SuperAgentTest; beforeEach(async () => { ownerShell = await createUserShell('global:owner'); - authOwnerShellAgent = testServer.authAgentFor(ownerShell); config.set('userManagement.isInstanceOwnerSetUp', false); }); @@ -39,7 +36,7 @@ describe('POST /owner/setup', () => { 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); @@ -88,7 +85,7 @@ describe('POST /owner/setup', () => { 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); @@ -150,7 +147,7 @@ describe('POST /owner/setup', () => { test('should fail with invalid inputs', async () => { 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); } }); diff --git a/packages/cli/test/unit/auth/auth.service.test.ts b/packages/cli/test/unit/auth/auth.service.test.ts index e5d873a543..3d432ebb64 100644 --- a/packages/cli/test/unit/auth/auth.service.test.ts +++ b/packages/cli/test/unit/auth/auth.service.test.ts @@ -39,7 +39,7 @@ describe('AuthService', () => { config.set('userManagement.jwtRefreshTimeoutHours', 0); }); - describe('createAuthMiddleware', () => { + describe('authMiddleware', () => { const req = mock({ cookies: {}, user: undefined }); const res = mock(); const next = jest.fn() as NextFunction; @@ -48,64 +48,34 @@ describe('AuthService', () => { res.status.mockReturnThis(); }); - describe('authRole = none', () => { - const authMiddleware = authService.createAuthMiddleware('none'); - - it('should just skips auth checks', async () => { - await authMiddleware(req, res, next); - expect(next).toHaveBeenCalled(); - expect(res.status).not.toHaveBeenCalled(); - }); + it('should 401 if no cookie is set', async () => { + req.cookies[AUTH_COOKIE_NAME] = undefined; + await authService.authMiddleware(req, res, next); + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(401); }); - describe('authRole = any', () => { - const authMiddleware = authService.createAuthMiddleware('any'); + 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); - it('should 401 if no cookie is set', async () => { - req.cookies[AUTH_COOKIE_NAME] = undefined; - await authMiddleware(req, res, next); - expect(next).not.toHaveBeenCalled(); - 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', - }); - }); + await authService.authMiddleware(req, res, next); + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(401); + expect(res.clearCookie).toHaveBeenCalledWith(AUTH_COOKIE_NAME); }); - describe('authRole = global:owner', () => { - const authMiddleware = authService.createAuthMiddleware('global:owner'); + 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); - it('should 403 if the user does not have the correct role', async () => { - req.cookies[AUTH_COOKIE_NAME] = validToken; - jest.advanceTimersByTime(6 * Time.days.toMilliseconds); - userRepository.findOne.mockResolvedValue( - mock({ ...userData, role: 'global:member' }), - ); - - await authMiddleware(req, res, next); - expect(next).not.toHaveBeenCalled(); - expect(res.status).toHaveBeenCalledWith(403); + await authService.authMiddleware(req, res, next); + expect(next).toHaveBeenCalled(); + expect(res.cookie).toHaveBeenCalledWith('n8n-auth', expect.any(String), { + httpOnly: true, + maxAge: 604800000, + sameSite: 'lax', }); }); }); diff --git a/packages/cli/test/unit/controllers/owner.controller.test.ts b/packages/cli/test/unit/controllers/owner.controller.test.ts index ffa6f1aa48..50917a2107 100644 --- a/packages/cli/test/unit/controllers/owner.controller.test.ts +++ b/packages/cli/test/unit/controllers/owner.controller.test.ts @@ -93,11 +93,15 @@ describe('OwnerController', () => { }); const res = mock(); configGetSpy.mockReturnValue(false); + userRepository.findOneOrFail.calledWith(anyObject()).mockResolvedValue(user); userRepository.save.calledWith(anyObject()).mockResolvedValue(user); jest.spyOn(jwt, 'sign').mockImplementation(() => 'signed-token'); await controller.setupOwner(req, res); + expect(userRepository.findOneOrFail).toHaveBeenCalledWith({ + where: { role: 'global:owner' }, + }); expect(userRepository.save).toHaveBeenCalledWith(user, { transaction: false }); expect(authService.issueCookie).toHaveBeenCalledWith(res, user); });