From 2eda8580a6df30a8f8713ddf7ebd197d18d8f46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Tue, 21 Mar 2023 14:49:35 +0100 Subject: [PATCH] fix(core): Force-upgrade `http-cache-semantics` to address CVE-2022-25881 (#5733) [GitHub Advisory](https://github.com/advisories/GHSA-rc47-6667-2j5j) --- .../UserManagement/UserManagementHelper.ts | 24 ----- packages/cli/src/commands/start.ts | 4 +- .../cli/src/controllers/auth.controller.ts | 6 +- .../cli/src/controllers/ldap.controller.ts | 3 +- packages/cli/src/controllers/me.controller.ts | 3 +- .../cli/src/controllers/nodes.controller.ts | 12 +-- .../cli/src/controllers/owner.controller.ts | 3 +- .../controllers/passwordReset.controller.ts | 3 - .../cli/src/controllers/tags.controller.ts | 5 +- .../src/controllers/translation.controller.ts | 3 +- .../cli/src/controllers/users.controller.ts | 5 +- packages/cli/src/databases/entities/Role.ts | 5 + packages/cli/src/decorators/Authorized.ts | 16 +++ packages/cli/src/decorators/RestController.ts | 6 +- packages/cli/src/decorators/constants.ts | 1 + packages/cli/src/decorators/index.ts | 1 + .../cli/src/decorators/registerController.ts | 23 +++- packages/cli/src/decorators/types.ts | 3 + packages/cli/src/middlewares/auth.ts | 102 ++++++------------ .../src/middlewares/createAuthMiddleware.ts | 15 +++ .../cli/test/integration/shared/constants.ts | 2 +- packages/cli/test/integration/shared/utils.ts | 6 +- .../cli/test/integration/users.api.test.ts | 1 + 23 files changed, 126 insertions(+), 126 deletions(-) create mode 100644 packages/cli/src/decorators/Authorized.ts create mode 100644 packages/cli/src/middlewares/createAuthMiddleware.ts diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 4c34a57b31e265..c2ad8ca5eb12c7 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -204,30 +204,6 @@ export async function getUserById(userId: string): Promise { return user; } -/** - * Check if a URL contains an auth-excluded endpoint. - */ -export function isAuthExcluded(url: string, ignoredEndpoints: Readonly): boolean { - return !!ignoredEndpoints - .filter(Boolean) // skip empty paths - .find((ignoredEndpoint) => url.startsWith(`/${ignoredEndpoint}`)); -} - -/** - * Check if the endpoint is `POST /users/:id`. - */ -export function isPostUsersId(req: express.Request, restEndpoint: string): boolean { - return ( - req.method === 'POST' && - new RegExp(`/${restEndpoint}/users/[\\w\\d-]*`).test(req.url) && - !req.url.includes('reinvite') - ); -} - -export function isAuthenticatedRequest(request: express.Request): request is AuthenticatedRequest { - return request.user !== undefined; -} - // ---------------------------------- // hashing // ---------------------------------- diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 6abd162ad8ab8b..9434a33e152eaf 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -268,10 +268,10 @@ export class Start extends BaseCommand { // eslint-disable-next-line no-restricted-syntax for (const missingPackage of missingPackages) { // eslint-disable-next-line no-await-in-loop - void (await this.loadNodesAndCredentials.loadNpmModule( + await this.loadNodesAndCredentials.loadNpmModule( missingPackage.packageName, missingPackage.version, - )); + ); missingPackages.delete(missingPackage); } LoggerProxy.info('Packages reinstalled successfully. Resuming regular initialization.'); diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index 5092d1ea66b152..25a909ceddb89c 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -1,5 +1,5 @@ import validator from 'validator'; -import { Get, Post, RestController } from '@/decorators'; +import { Authorized, Get, Post, RestController } from '@/decorators'; import { AuthError, BadRequestError, InternalServerError } from '@/ResponseHelper'; import { sanitizeUser, withFeatureFlags } from '@/UserManagement/UserManagementHelper'; import { issueCookie, resolveJwt } from '@/auth/jwt'; @@ -56,7 +56,6 @@ export class AuthController { /** * Log in a user. - * Authless endpoint. */ @Post('/login') async login(req: LoginRequest, res: Response): Promise { @@ -140,7 +139,6 @@ export class AuthController { /** * Validate invite token to enable invitee to set up their account. - * Authless endpoint. */ @Get('/resolve-signup-token') async resolveSignupToken(req: UserRequest.ResolveSignUp) { @@ -201,8 +199,8 @@ export class AuthController { /** * Log out a user. - * Authless endpoint. */ + @Authorized() @Post('/logout') logout(req: Request, res: Response) { res.clearCookie(AUTH_COOKIE_NAME); diff --git a/packages/cli/src/controllers/ldap.controller.ts b/packages/cli/src/controllers/ldap.controller.ts index 619a70db286b2b..b354ef660021c8 100644 --- a/packages/cli/src/controllers/ldap.controller.ts +++ b/packages/cli/src/controllers/ldap.controller.ts @@ -1,5 +1,5 @@ import pick from 'lodash.pick'; -import { Get, Post, Put, RestController } from '@/decorators'; +import { Authorized, Get, Post, Put, RestController } from '@/decorators'; import { getLdapConfig, getLdapSynchronizations, updateLdapConfig } from '@/Ldap/helpers'; import { LdapService } from '@/Ldap/LdapService.ee'; import { LdapSync } from '@/Ldap/LdapSync.ee'; @@ -8,6 +8,7 @@ import { BadRequestError } from '@/ResponseHelper'; import { NON_SENSIBLE_LDAP_CONFIG_PROPERTIES } from '@/Ldap/constants'; import { InternalHooks } from '@/InternalHooks'; +@Authorized(['global', 'owner']) @RestController('/ldap') export class LdapController { constructor( diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index e56c820155a5d0..99ebbda63ffcde 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -1,6 +1,6 @@ import validator from 'validator'; import { plainToInstance } from 'class-transformer'; -import { Delete, Get, Patch, Post, RestController } from '@/decorators'; +import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators'; import { compareHash, hashPassword, @@ -24,6 +24,7 @@ import type { import { randomBytes } from 'crypto'; import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers'; +@Authorized() @RestController('/me') export class MeController { private readonly logger: ILogger; diff --git a/packages/cli/src/controllers/nodes.controller.ts b/packages/cli/src/controllers/nodes.controller.ts index 63116bb761f2be..bf6646bbc402a8 100644 --- a/packages/cli/src/controllers/nodes.controller.ts +++ b/packages/cli/src/controllers/nodes.controller.ts @@ -4,7 +4,7 @@ import { STARTER_TEMPLATE_NAME, UNKNOWN_FAILURE_REASON, } from '@/constants'; -import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators'; +import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators'; import { NodeRequest } from '@/requests'; import { BadRequestError, InternalServerError } from '@/ResponseHelper'; import { @@ -30,10 +30,10 @@ import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { InternalHooks } from '@/InternalHooks'; import { Push } from '@/push'; import { Config } from '@/config'; -import { isAuthenticatedRequest } from '@/UserManagement/UserManagementHelper'; const { PACKAGE_NOT_INSTALLED, PACKAGE_NAME_NOT_PROVIDED } = RESPONSE_ERROR_MESSAGES; +@Authorized(['global', 'owner']) @RestController('/nodes') export class NodesController { constructor( @@ -43,14 +43,6 @@ export class NodesController { private internalHooks: InternalHooks, ) {} - // TODO: move this into a new decorator `@Authorized` - @Middleware() - checkIfOwner(req: Request, res: Response, next: NextFunction) { - if (!isAuthenticatedRequest(req) || req.user.globalRole.name !== 'owner') - res.status(403).json({ status: 'error', message: 'Unauthorized' }); - else next(); - } - // TODO: move this into a new decorator `@IfConfig('executions.mode', 'queue')` @Middleware() checkIfCommunityNodesEnabled(req: Request, res: Response, next: NextFunction) { diff --git a/packages/cli/src/controllers/owner.controller.ts b/packages/cli/src/controllers/owner.controller.ts index 8040c5aaac5314..b179eb189bb987 100644 --- a/packages/cli/src/controllers/owner.controller.ts +++ b/packages/cli/src/controllers/owner.controller.ts @@ -1,6 +1,6 @@ import validator from 'validator'; import { validateEntity } from '@/GenericHelpers'; -import { Get, Post, RestController } from '@/decorators'; +import { Authorized, Get, Post, RestController } from '@/decorators'; import { BadRequestError } from '@/ResponseHelper'; import { hashPassword, @@ -18,6 +18,7 @@ import type { Settings } from '@db/entities/Settings'; import type { User } from '@db/entities/User'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; +@Authorized(['global', 'owner']) @RestController('/owner') export class OwnerController { private readonly config: Config; diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index 4b0ec6c9dfcaab..1b9961affa5d67 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -64,7 +64,6 @@ export class PasswordResetController { /** * Send a password reset email. - * Authless endpoint. */ @Post('/forgot-password') async forgotPassword(req: PasswordResetRequest.Email) { @@ -161,7 +160,6 @@ export class PasswordResetController { /** * Verify password reset token and user ID. - * Authless endpoint. */ @Get('/resolve-password-token') async resolvePasswordToken(req: PasswordResetRequest.Credentials) { @@ -203,7 +201,6 @@ export class PasswordResetController { /** * Verify password reset token and user ID and update password. - * Authless endpoint. */ @Post('/change-password') async changePassword(req: PasswordResetRequest.NewPassword, res: Response) { diff --git a/packages/cli/src/controllers/tags.controller.ts b/packages/cli/src/controllers/tags.controller.ts index 3c73235c853b30..85d81eb9f1e45f 100644 --- a/packages/cli/src/controllers/tags.controller.ts +++ b/packages/cli/src/controllers/tags.controller.ts @@ -1,7 +1,7 @@ -import { Request, Response, NextFunction } from 'express'; +import { Request, NextFunction } from 'express'; import type { Repository } from 'typeorm'; import type { Config } from '@/config'; -import { Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators'; +import { Authorized, Delete, Get, Middleware, Patch, Post, RestController } from '@/decorators'; import type { IDatabaseCollections, IExternalHooksClass, ITagWithCountDb } from '@/Interfaces'; import { TagEntity } from '@db/entities/TagEntity'; import { getTagsWithCountDb } from '@/TagHelpers'; @@ -9,6 +9,7 @@ import { validateEntity } from '@/GenericHelpers'; import { BadRequestError, UnauthorizedError } from '@/ResponseHelper'; import { TagsRequest } from '@/requests'; +@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 8240a376a7e641..6d36d650d2e2c5 100644 --- a/packages/cli/src/controllers/translation.controller.ts +++ b/packages/cli/src/controllers/translation.controller.ts @@ -2,7 +2,7 @@ import type { Request } from 'express'; import { ICredentialTypes } from 'n8n-workflow'; import { join } from 'path'; import { access } from 'fs/promises'; -import { Get, RestController } from '@/decorators'; +import { Authorized, Get, RestController } from '@/decorators'; import { BadRequestError, InternalServerError } from '@/ResponseHelper'; import { Config } from '@/config'; import { NODES_BASE_DIR } from '@/constants'; @@ -14,6 +14,7 @@ export declare namespace TranslationRequest { export type Credential = Request<{}, {}, {}, { credentialType: string }>; } +@Authorized() @RestController('/') export class TranslationController { constructor(private config: Config, private credentialTypes: ICredentialTypes) {} diff --git a/packages/cli/src/controllers/users.controller.ts b/packages/cli/src/controllers/users.controller.ts index ddb8a5c6cceeab..00bb4a53adf5b6 100644 --- a/packages/cli/src/controllers/users.controller.ts +++ b/packages/cli/src/controllers/users.controller.ts @@ -6,7 +6,7 @@ import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; import { User } from '@db/entities/User'; import { SharedCredentials } from '@db/entities/SharedCredentials'; import { SharedWorkflow } from '@db/entities/SharedWorkflow'; -import { Delete, Get, Post, RestController } from '@/decorators'; +import { Authorized, Delete, Get, Post, RestController } from '@/decorators'; import { addInviteLinkToUser, generateUserInviteUrl, @@ -35,7 +35,9 @@ import type { import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { AuthIdentity } from '@db/entities/AuthIdentity'; import type { PostHogClient } from '@/posthog'; +import { NoAuthRequired } from '@/decorators/Authorized'; +@Authorized(['global', 'owner']) @RestController('/users') export class UsersController { private config: Config; @@ -276,6 +278,7 @@ export class UsersController { /** * Fill out user shell with first name, last name, and password. */ + @NoAuthRequired() @Post('/:id') async updateUser(req: UserRequest.Update, res: Response) { const { id: inviteeId } = req.params; diff --git a/packages/cli/src/databases/entities/Role.ts b/packages/cli/src/databases/entities/Role.ts index b1943866d48d89..a924f40fe8944f 100644 --- a/packages/cli/src/databases/entities/Role.ts +++ b/packages/cli/src/databases/entities/Role.ts @@ -9,6 +9,7 @@ import { idStringifier } from '../utils/transformers'; export type RoleNames = 'owner' | 'member' | 'user' | 'editor'; export type RoleScopes = 'global' | 'workflow' | 'credential'; +export type AuthRole = [RoleScopes, RoleNames] | 'any' | 'none'; @Entity() @Unique(['scope', 'name']) @@ -32,4 +33,8 @@ export class Role extends AbstractEntity { @OneToMany('SharedCredentials', 'role') sharedCredentials: SharedCredentials[]; + + matches([scope, name]: [RoleScopes, RoleNames]) { + return this.name === name && this.scope === scope; + } } diff --git a/packages/cli/src/decorators/Authorized.ts b/packages/cli/src/decorators/Authorized.ts new file mode 100644 index 00000000000000..880938ae5ce724 --- /dev/null +++ b/packages/cli/src/decorators/Authorized.ts @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* 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/RestController.ts b/packages/cli/src/decorators/RestController.ts index 11c2d55665b39b..09ec49d0185b1c 100644 --- a/packages/cli/src/decorators/RestController.ts +++ b/packages/cli/src/decorators/RestController.ts @@ -1,8 +1,10 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { CONTROLLER_BASE_PATH } from './constants'; +import type { RequestHandler } from 'express'; +import { CONTROLLER_BASE_PATH, CONTROLLER_MIDDLEWARES } from './constants'; export const RestController = - (basePath: `/${string}` = '/'): ClassDecorator => + (basePath: `/${string}` = '/', ...middlewares: RequestHandler[]): ClassDecorator => (target: object) => { Reflect.defineMetadata(CONTROLLER_BASE_PATH, basePath, target); + Reflect.defineMetadata(CONTROLLER_MIDDLEWARES, middlewares, target); }; diff --git a/packages/cli/src/decorators/constants.ts b/packages/cli/src/decorators/constants.ts index 6bff0e4a6cb906..5a5efc938db6c7 100644 --- a/packages/cli/src/decorators/constants.ts +++ b/packages/cli/src/decorators/constants.ts @@ -1,3 +1,4 @@ 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'; diff --git a/packages/cli/src/decorators/index.ts b/packages/cli/src/decorators/index.ts index 71b82b5b69c30d..69657686bdb790 100644 --- a/packages/cli/src/decorators/index.ts +++ b/packages/cli/src/decorators/index.ts @@ -1,3 +1,4 @@ +export { Authorized } 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 e20293ae21c714..fee5e0bd340fe1 100644 --- a/packages/cli/src/decorators/registerController.ts +++ b/packages/cli/src/decorators/registerController.ts @@ -1,10 +1,16 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Router } from 'express'; import type { Config } from '@/config'; -import { CONTROLLER_BASE_PATH, CONTROLLER_MIDDLEWARES, CONTROLLER_ROUTES } from './constants'; +import { + CONTROLLER_AUTH_ROLES, + CONTROLLER_BASE_PATH, + CONTROLLER_MIDDLEWARES, + CONTROLLER_ROUTES, +} from './constants'; import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file import type { Application, Request, Response, RequestHandler } from 'express'; -import type { Controller, MiddlewareMetadata, RouteMetadata } from './types'; +import type { AuthRoleMetadata, Controller, MiddlewareMetadata, RouteMetadata } from './types'; +import { createAuthMiddleware } from '@/middlewares/createAuthMiddleware'; export const registerController = (app: Application, config: Config, controller: object) => { const controllerClass = controller.constructor; @@ -14,11 +20,20 @@ export const registerController = (app: Application, config: Config, controller: if (!controllerBasePath) throw new Error(`${controllerClass.name} is missing the RestController decorator`); + const middlewares = Reflect.getMetadata( + CONTROLLER_MIDDLEWARES, + controllerClass, + ) as RequestHandler[]; + const authRoles = Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) as + | AuthRoleMetadata + | undefined; const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[]; if (routes.length > 0) { const router = Router({ mergeParams: true }); const restBasePath = config.getEnv('endpoints.rest'); - const prefix = `/${[restBasePath, controllerBasePath].join('/')}`.replace(/\/+/g, '/'); + const prefix = `/${[restBasePath, controllerBasePath].join('/')}` + .replace(/\/+/g, '/') + .replace(/\/$/, ''); const controllerMiddlewares = ( (Reflect.getMetadata(CONTROLLER_MIDDLEWARES, controllerClass) ?? []) as MiddlewareMetadata[] @@ -28,8 +43,10 @@ export const registerController = (app: Application, config: Config, controller: ); routes.forEach(({ method, path, middlewares: routeMiddlewares, handlerName }) => { + const authRole = authRoles && (authRoles[handlerName] ?? authRoles['*']); router[method]( path, + ...(authRole ? [createAuthMiddleware(authRole)] : []), ...controllerMiddlewares, ...routeMiddlewares, send(async (req: Request, res: Response) => diff --git a/packages/cli/src/decorators/types.ts b/packages/cli/src/decorators/types.ts index 250abf0d27c959..4e9ee16b511518 100644 --- a/packages/cli/src/decorators/types.ts +++ b/packages/cli/src/decorators/types.ts @@ -1,7 +1,10 @@ import type { Request, Response, RequestHandler } from 'express'; +import type { AuthRole } from '@db/entities/Role'; export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'; +export type AuthRoleMetadata = Record; + export interface MiddlewareMetadata { handlerName: string; } diff --git a/packages/cli/src/middlewares/auth.ts b/packages/cli/src/middlewares/auth.ts index 1299f474084b6f..cf0aeb7ac572c7 100644 --- a/packages/cli/src/middlewares/auth.ts +++ b/packages/cli/src/middlewares/auth.ts @@ -10,12 +10,7 @@ import type { AuthenticatedRequest } from '@/requests'; import config from '@/config'; import { AUTH_COOKIE_NAME, EDITOR_UI_DIST_DIR } from '@/constants'; import { issueCookie, resolveJwtContent } from '@/auth/jwt'; -import { - isAuthenticatedRequest, - isAuthExcluded, - isPostUsersId, - isUserManagementEnabled, -} from '@/UserManagement/UserManagementHelper'; +import { isUserManagementEnabled } from '@/UserManagement/UserManagementHelper'; import type { Repository } from 'typeorm'; import type { User } from '@db/entities/User'; import { SamlUrls } from '@/sso/saml/constants'; @@ -61,12 +56,43 @@ const refreshExpiringCookie: RequestHandler = async (req: AuthenticatedRequest, next(); }; -const passportMiddleware = passport.authenticate('jwt', { session: false }) as RequestHandler; - const staticAssets = globSync(['**/*.html', '**/*.svg', '**/*.png', '**/*.ico'], { cwd: EDITOR_UI_DIST_DIR, }); +const canSkipAuth = ( + { method, url }: Request, + ignoredEndpoints: Readonly, + restEndpoint: string, +) => + method === 'OPTIONS' || + staticAssets.includes(url.slice(1)) || + url.startsWith(`/${restEndpoint}/settings`) || + url.startsWith(`/${restEndpoint}/login`) || + url.startsWith(`/${restEndpoint}/resolve-signup-token`) || + url.startsWith(`/${restEndpoint}/forgot-password`) || + url.startsWith(`/${restEndpoint}/resolve-password-token`) || + url.startsWith(`/${restEndpoint}/change-password`) || + url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) || + url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) || + url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.metadata}`) || + url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.initSSO}`) || + url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.acs}`) || + !!ignoredEndpoints + .filter(Boolean) // skip empty paths + .find((ignoredEndpoint) => url.startsWith(`/${ignoredEndpoint}`)); + +const passportMiddleware = passport.authenticate('jwt', { session: false }) as RequestHandler; + +// TODO: delete this +export function isPostUsersId(req: Request, restEndpoint: string): boolean { + return ( + req.method === 'POST' && + new RegExp(`/${restEndpoint}/users/[\\w\\d-]*`).test(req.url) && + !req.url.includes('reinvite') + ); +} + /** * This sets up the auth middlewares in the correct order */ @@ -81,28 +107,8 @@ export const setupAuthMiddlewares = ( app.use(jwtAuth()); app.use(async (req: Request, res: Response, next: NextFunction) => { - if ( - // TODO: refactor me!!! - // skip authentication for preflight requests - req.method === 'OPTIONS' || - staticAssets.includes(req.url.slice(1)) || - req.url.startsWith(`/${restEndpoint}/settings`) || - req.url.startsWith(`/${restEndpoint}/login`) || - req.url.startsWith(`/${restEndpoint}/logout`) || - req.url.startsWith(`/${restEndpoint}/resolve-signup-token`) || - isPostUsersId(req, restEndpoint) || - req.url.startsWith(`/${restEndpoint}/forgot-password`) || - req.url.startsWith(`/${restEndpoint}/resolve-password-token`) || - req.url.startsWith(`/${restEndpoint}/change-password`) || - req.url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) || - req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) || - req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.metadata}`) || - req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.initSSO}`) || - req.url.startsWith(`/${restEndpoint}/sso/saml${SamlUrls.acs}`) || - isAuthExcluded(req.url, ignoredEndpoints) - ) { + if (canSkipAuth(req, ignoredEndpoints, restEndpoint) || isPostUsersId(req, restEndpoint)) return next(); - } // skip authentication if user management is disabled if (!isUserManagementEnabled()) { @@ -116,43 +122,5 @@ export const setupAuthMiddlewares = ( return passportMiddleware(req, res, next); }); - app.use((req: Request | AuthenticatedRequest, res: Response, next: NextFunction) => { - // req.user is empty for public routes, so just proceed - // owner can do anything, so proceed as well - if (!req.user || (isAuthenticatedRequest(req) && req.user.globalRole.name === 'owner')) { - next(); - return; - } - // Not owner and user exists. We now protect restricted urls. - const postRestrictedUrls = [ - `/${restEndpoint}/users`, - `/${restEndpoint}/owner`, - `/${restEndpoint}/ldap/sync`, - `/${restEndpoint}/ldap/test-connection`, - ]; - const getRestrictedUrls = [`/${restEndpoint}/ldap/sync`, `/${restEndpoint}/ldap/config`]; - const putRestrictedUrls = [`/${restEndpoint}/ldap/config`]; - const trimmedUrl = req.url.endsWith('/') ? req.url.slice(0, -1) : req.url; - if ( - (req.method === 'POST' && postRestrictedUrls.includes(trimmedUrl)) || - (req.method === 'GET' && getRestrictedUrls.includes(trimmedUrl)) || - (req.method === 'PUT' && putRestrictedUrls.includes(trimmedUrl)) || - (req.method === 'DELETE' && - new RegExp(`/${restEndpoint}/users/[^/]+`, 'gm').test(trimmedUrl)) || - (req.method === 'POST' && - new RegExp(`/${restEndpoint}/users/[^/]+/reinvite`, 'gm').test(trimmedUrl)) || - new RegExp(`/${restEndpoint}/owner/[^/]+`, 'gm').test(trimmedUrl) - ) { - Logger.verbose('User attempted to access endpoint without authorization', { - endpoint: `${req.method} ${trimmedUrl}`, - userId: isAuthenticatedRequest(req) ? req.user.id : 'unknown', - }); - res.status(403).json({ status: 'error', message: 'Unauthorized' }); - return; - } - - next(); - }); - app.use(refreshExpiringCookie); }; diff --git a/packages/cli/src/middlewares/createAuthMiddleware.ts b/packages/cli/src/middlewares/createAuthMiddleware.ts new file mode 100644 index 00000000000000..7d805945af23e0 --- /dev/null +++ b/packages/cli/src/middlewares/createAuthMiddleware.ts @@ -0,0 +1,15 @@ +import type { RequestHandler } from 'express'; +import type { AuthenticatedRequest } from '@/requests'; +import type { AuthRole } from '@db/entities/Role'; + +export const createAuthMiddleware = + (authRole: AuthRole): RequestHandler => + ({ user }: AuthenticatedRequest, res, next) => { + if (authRole === 'none') return next(); + + if (!user) return res.status(401).json({ status: 'error', message: 'Unauthorized' }); + + if (authRole === 'any' || user.globalRole.matches(authRole)) return next(); + + res.status(403).json({ status: 'error', message: 'Unauthorized' }); + }; diff --git a/packages/cli/test/integration/shared/constants.ts b/packages/cli/test/integration/shared/constants.ts index 3608225eaa1e1b..37798aa4d0f83e 100644 --- a/packages/cli/test/integration/shared/constants.ts +++ b/packages/cli/test/integration/shared/constants.ts @@ -42,7 +42,7 @@ export const ROUTES_REQUIRING_AUTHORIZATION: Readonly = [ 'POST /users', 'DELETE /users/123', 'POST /users/123/reinvite', - 'POST /owner/pre-setup', + 'GET /owner/pre-setup', 'POST /owner/setup', 'POST /owner/skip-setup', ]; diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index d827f1562d1aa9..74ad36bc3effa8 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -75,9 +75,9 @@ import { InternalHooks } from '@/InternalHooks'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { PostHogClient } from '@/posthog'; import { LdapManager } from '@/Ldap/LdapManager.ee'; -import { LDAP_ENABLED } from '@/Ldap/constants'; -import { handleLdapInit } from '@/Ldap/helpers'; import { Push } from '@/push'; +import { LDAP_DEFAULT_CONFIGURATION, LDAP_ENABLED } from '@/Ldap/constants'; +import { handleLdapInit, encryptPassword } from '@/Ldap/helpers'; export const mockInstance = ( ctor: new (...args: any[]) => T, @@ -258,7 +258,7 @@ const classifyEndpointGroups = (endpointGroups: EndpointGroup[]) => { const routerEndpoints: EndpointGroup[] = []; const functionEndpoints: EndpointGroup[] = []; - const ROUTER_GROUP = ['credentials', 'workflows', 'publicApi', 'eventBus', 'license']; + const ROUTER_GROUP = ['credentials', 'nodes', 'workflows', 'publicApi', 'eventBus', 'license']; endpointGroups.forEach((group) => (ROUTER_GROUP.includes(group) ? routerEndpoints : functionEndpoints).push(group), diff --git a/packages/cli/test/integration/users.api.test.ts b/packages/cli/test/integration/users.api.test.ts index 759d12b8cb42ab..98a3392c7eb8d4 100644 --- a/packages/cli/test/integration/users.api.test.ts +++ b/packages/cli/test/integration/users.api.test.ts @@ -244,6 +244,7 @@ describe('POST /users/:id', () => { }; const response = await authlessAgent.post(`/users/${memberShell.id}`).send(memberData); + expect(response.statusCode).toBe(200); const { id,