From 7877e371bdd1f47820af7c640280507841777735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Thu, 14 Aug 2025 15:07:38 +0200 Subject: [PATCH 01/24] chore: make ServiceAuthenticator properties optional --- src/server/model/server-types.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/server/model/server-types.ts b/src/server/model/server-types.ts index 4f11084..a56486a 100644 --- a/src/server/model/server-types.ts +++ b/src/server/model/server-types.ts @@ -43,18 +43,22 @@ export class ServiceContext { * The resolved language to be used in the current request handling. */ public language: string; + /** * The preferred media type to be used in the current request handling. */ public accept: string; + /** * The request object. */ public request: express.Request; + /** * The response object */ public response: express.Response; + /** * The next function. It can be used to delegate to the next middleware * registered the processing of the current request. @@ -107,14 +111,16 @@ export interface ServiceAuthenticator { * Get the user list of roles. */ getRoles: (req: express.Request, res: express.Response) => Array; + /** * Initialize the authenticator */ - initialize(router: express.Router): void; + initialize?: (router: express.Router) => void; + /** * Retrieve the middleware used to authenticate users. */ - getMiddleware(): express.RequestHandler; + getMiddleware?: () => express.RequestHandler; } export type ServiceProcessor = (req: express.Request, res?: express.Response) => void | Promise; From fb893ae0dcc476fc5bff8904a5fa6e5d34c5ba06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 10:22:22 +0200 Subject: [PATCH 02/24] feat: add middleware --- src/middlewares/routeRequiresRoles.ts | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/middlewares/routeRequiresRoles.ts diff --git a/src/middlewares/routeRequiresRoles.ts b/src/middlewares/routeRequiresRoles.ts new file mode 100644 index 0000000..55aabce --- /dev/null +++ b/src/middlewares/routeRequiresRoles.ts @@ -0,0 +1,52 @@ +import * as debug from 'debug'; +import { NextFunction, Request, Response } from 'express'; +import * as Errors from '../server/model/errors'; + +const debuggerInstance = debug('typescript-rest:middlewares:routeRequiresRoles'); + +export function routeRequiresRoles( + authenticator: { getRoles: (req: Request, res: Response) => Array }, + requiredRoles: Array +) { + if (requiredRoles.length === 0) throw new Error('At least one role must be specified.'); + + const roleRegex = /^[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+){0,}$/; + const nonMatchingRoles = requiredRoles.filter((role) => !roleRegex.test(role)); + if (nonMatchingRoles.length > 0) { + throw new Error( + `Invalid required role(s) specified: ${nonMatchingRoles.join(', ')}. Roles must match the pattern: ${roleRegex}.` + ); + } + + return (req: Request, res: Response, next: NextFunction) => { + const requestRoles = authenticator.getRoles(req, res); + if (debuggerInstance.enabled) debuggerInstance('Validating authentication roles: <%j>.', requestRoles); + + const transformedRoles = requestRoles.map(transformRole); + const isAuthorized = requiredRoles.some((requiredRole: string) => + isRoleMatched(requiredRole, transformedRoles) + ); + if (!isAuthorized) { + next(new Errors.ForbiddenError('You are not allowed to access this endpoint.')); + return; + } + + next(); + }; +} + +function isRoleMatched(requiredRole: string, userRoles: Array): boolean { + for (const userRole of userRoles) { + const isMatch = userRole.test(requiredRole); + if (isMatch) return true; + } + + return false; +} + +function transformRole(role: string): RegExp { + if (!role.includes('*')) return new RegExp(`^${role}$`); + + const regexString = role.replaceAll('**', '[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+){0,}').replaceAll('*', '[a-zA-Z0-9_-]+'); + return new RegExp(`^${regexString}$`); +} From 99fb62975267e568b5ab0cff5cfb035f6ba9156a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 10:22:56 +0200 Subject: [PATCH 03/24] feat: use middleware --- src/server/server-container.ts | 28 ++++++++-------------------- src/typescript-rest.ts | 1 + 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/src/server/server-container.ts b/src/server/server-container.ts index 8ef2747..aa66a11 100644 --- a/src/server/server-container.ts +++ b/src/server/server-container.ts @@ -5,9 +5,9 @@ import * as bodyParser from 'body-parser'; import * as cookieParser from 'cookie-parser'; import * as debug from 'debug'; import * as express from 'express'; -import { NextFunction, Request, Response } from 'express'; import * as _ from 'lodash'; import * as multer from 'multer'; +import { routeRequiresRoles } from '../middlewares/routeRequiresRoles'; import * as Errors from './model/errors'; import { ServiceClass, ServiceMethod } from './model/metadata'; import { @@ -114,7 +114,7 @@ export class ServerContainer { if (this.authenticator) { this.authenticator.forEach((auth, name) => { this.debugger.build('Initializing authenticator: %s', name); - auth.initialize(this.router); + if (auth.initialize) auth.initialize(this.router); }); } this.serverClasses.forEach((classData) => { @@ -337,15 +337,16 @@ export class ServerContainer { if (this.authenticator && authenticatorMap) { const authenticatorNames: Array = Object.keys(authenticatorMap); for (const authenticatorName of authenticatorNames) { - let roles: Array = authenticatorMap[authenticatorName]; + const roles: Array = authenticatorMap[authenticatorName]; this.debugger.build( 'Registering an authenticator middleware <%s> for method <%s>.', authenticatorName, serviceMethod.name ); + const authenticator = this.getAuthenticator(authenticatorName); - result.push(authenticator.getMiddleware()); - roles = roles.filter((role) => role !== '*'); + if (authenticator.getMiddleware) result.push(authenticator.getMiddleware()); + if (roles.length) { this.debugger.build( 'Registering a role validator middleware <%s> for method <%s>.', @@ -353,7 +354,7 @@ export class ServerContainer { serviceMethod.name ); this.debugger.build('Roles: <%j>.', roles); - result.push(this.buildAuthMiddleware(authenticator, roles)); + result.push(routeRequiresRoles(authenticator, roles)); } } } @@ -365,21 +366,8 @@ export class ServerContainer { if (!this.authenticator.has(authenticatorName)) { throw new Error(`Invalid authenticator name ${authenticatorName}`); } - return this.authenticator.get(authenticatorName); - } - private buildAuthMiddleware(authenticator: ServiceAuthenticator, roles: Array): express.RequestHandler { - return (req: Request, res: Response, next: NextFunction) => { - const requestRoles = authenticator.getRoles(req, res); - if (this.debugger.runtime.enabled) { - this.debugger.runtime('Validating authentication roles: <%j>.', requestRoles); - } - if (requestRoles.some((role: string) => roles.indexOf(role) >= 0)) { - next(); - } else { - throw new Errors.ForbiddenError(); - } - }; + return this.authenticator.get(authenticatorName); } private buildParserMiddlewares( diff --git a/src/typescript-rest.ts b/src/typescript-rest.ts index 6246161..4d4ba9e 100644 --- a/src/typescript-rest.ts +++ b/src/typescript-rest.ts @@ -7,6 +7,7 @@ import * as Return from './server/model/return-types'; export * from './decorators/methods'; export * from './decorators/parameters'; export * from './decorators/services'; +export * from './middlewares/routeRequiresRoles'; export * from './server/model/server-types'; export * from './server/server'; From 41cb0ce8d25716932fe5fa78c20bda30088ebc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 10:23:10 +0200 Subject: [PATCH 04/24] chore: higher es version --- test/tsconfig.json | 4 ++-- tsconfig.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/tsconfig.json b/test/tsconfig.json index 59b2879..22b068b 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -3,7 +3,7 @@ "declaration": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "lib": ["es6", "dom"], + "lib": ["es2021", "dom"], "module": "commonjs", "newLine": "LF", "noFallthroughCasesInSwitch": true, @@ -14,7 +14,7 @@ "noUnusedLocals": true, "sourceMap": true, "strictNullChecks": false, - "target": "es5" + "target": "es2021" }, "include": ["**/*.ts"] } diff --git a/tsconfig.json b/tsconfig.json index b8c05b1..f07505b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "declaration": true, "experimentalDecorators": true, "emitDecoratorMetadata": true, - "lib": ["es6", "dom"], + "lib": ["es2021", "dom"], "module": "commonjs", "newLine": "LF", "noFallthroughCasesInSwitch": true, @@ -15,7 +15,7 @@ "outDir": "dist", "sourceMap": true, "strictNullChecks": false, - "target": "es6" + "target": "es2021" }, "include": ["src/**/*.ts"] } From 5d6facc24db837826becc79379de306d8c68940b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 10:24:01 +0200 Subject: [PATCH 05/24] test: add suite for middleware --- .../middlewares/routeRequiresRoles.spec.ts | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 test/unit/middlewares/routeRequiresRoles.spec.ts diff --git a/test/unit/middlewares/routeRequiresRoles.spec.ts b/test/unit/middlewares/routeRequiresRoles.spec.ts new file mode 100644 index 0000000..58c0bfc --- /dev/null +++ b/test/unit/middlewares/routeRequiresRoles.spec.ts @@ -0,0 +1,94 @@ +import * as express from 'express'; +import { Errors, routeRequiresRoles } from '../../../src/typescript-rest'; + +describe('routeRequiresRoles middleware', () => { + const res = {} as any as express.Response; + + test.each([ + [['admin'], ['admin']], + [['admin', 'core:messages'], ['core:messages']], + [['admin', 'core:*'], ['core:messages']], + [ + ['admin', 'core:*'], + ['core:messages', 'admin'] + ], + [ + ['admin', 'core:*'], + ['core:messages', 'aRandomRole'] + ], + [ + ['admin', 'core:*'], + ['core:messages', 'admin', 'aRandomRole'] + ], + [ + ['admin', 'core:*'], + ['core:messages', 'admin', 'aRandomRole', 'anotherRole'] + ], + [ + ['admin', 'core:*'], + ['core:messages', 'admin', 'anotherRole'] + ], + [ + ['admin', 'core:*'], + ['core:messages', 'admin', 'anotherRole', 'aRandomRole'] + ], + [['*:*'], ['core:messages']], + [['**'], ['core:messages']], + [['core:*'], ['core:messages']], + [['core:**'], ['core:messages']], + [['core:**'], ['core:messages:whatever']], + [['core:*:*:*'], ['core:messages:whatever:whatever']], + [['core:*:*:whatever'], ['core:messages:whatever:whatever']], + [['core:**:whatever'], ['core:messages:whatever']], + [['core:**:whatever'], ['core:messages:whatever:whatever']], + [['**:whatever'], ['core:messages:whatever']], + [['**:whatever'], ['core:messages:whatever:whatever']] + ])('should accept the given (%s) and required (%s) roles', (userRoles, requiredRoles) => { + const next = jest.fn(); + + const authenticator = { getRoles: () => userRoles }; + const fn = routeRequiresRoles(authenticator, requiredRoles); + fn({ userRoles: userRoles } as any as express.Request, res, next); + + expect(next).toHaveBeenCalledTimes(1); + + expect(next).toHaveBeenCalledWith(); + }); + + test.each([ + [[], ['admin']], + [['admin'], ['aRandomRole']], + [['admin', 'core:*'], ['aRandomRole']], + [ + ['admin', 'core:*'], + ['aRandomRole', 'anotherRole'] + ], + [['core:*'], ['core:messages:whatever']], + [['core:*:*:*'], ['core:messages:whatever']], + [['core:*:*:whatever'], ['core:messages:whatever']], + [['core:*'], ['core:messages:whatever']] + ])('should reject the given (%s) and required (%s) roles', (userRoles, requiredRoles) => { + const next = jest.fn(); + + const authenticator = { getRoles: () => userRoles }; + const fn = routeRequiresRoles(authenticator, requiredRoles); + fn({ userRoles: userRoles } as any as express.Request, res, next); + + expect(next).toHaveBeenCalledTimes(1); + + expect(next).toHaveBeenCalledWith(new Errors.ForbiddenError('You are not allowed to access this endpoint.')); + }); + + test('should throw an error if no roles are specified', () => { + expect(() => routeRequiresRoles({ getRoles: () => [] }, [])).toThrow('At least one role must be specified.'); + }); + + test.each(['admin:', 'admin::core', 'admin::', '*'])( + 'should throw an error because the required role (%s) does not match the pattern', + (role) => { + expect(() => routeRequiresRoles({ getRoles: () => [] }, [role])).toThrow( + 'Invalid required role(s) specified' + ); + } + ); +}); From 4c03075e42f64bed91ceaf50e6e743308f94f513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 10:24:28 +0200 Subject: [PATCH 06/24] chore: version bump --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ee496ae..6209431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@nmshd/typescript-rest", - "version": "3.1.6", + "version": "3.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@nmshd/typescript-rest", - "version": "3.1.6", + "version": "3.2.0", "license": "MIT", "dependencies": { "@types/body-parser": "1.19.5", diff --git a/package.json b/package.json index 88fb35e..1e08e07 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/typescript-rest", - "version": "3.1.6", + "version": "3.2.0", "description": "A Library to create RESTFul APIs with Typescript", "keywords": [ "API", From de478f6fdce143e1f240cedea0760ee4a9b1c0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 10:26:02 +0200 Subject: [PATCH 07/24] chore: remove unused debugger instance --- src/server/server-container.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/server/server-container.ts b/src/server/server-container.ts index aa66a11..82974aa 100644 --- a/src/server/server-container.ts +++ b/src/server/server-container.ts @@ -53,8 +53,7 @@ export class ServerContainer { public router: express.Router; private debugger = { - build: debug('typescript-rest:server-container:build'), - runtime: debug('typescript-rest:server-container:runtime') + build: debug('typescript-rest:server-container:build') }; private upload: multer.Multer; private serverClasses: Map = new Map(); From 2121cf650c83f50581665a41e014339450417ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 10:30:07 +0200 Subject: [PATCH 08/24] chore: audit fix --- package-lock.json | 387 +++++++++------------------------------------- 1 file changed, 70 insertions(+), 317 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6209431..dce3a9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -586,34 +586,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@es-joy/jsdoccomment": { "version": "0.46.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz", @@ -1351,42 +1323,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2003,21 +1939,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2105,15 +2026,6 @@ "node": ">=14" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2807,15 +2719,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -2961,18 +2864,6 @@ "wrappy": "1" } }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -3143,6 +3034,22 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3943,14 +3850,18 @@ } }, "node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "dev": true, + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" }, "engines": { "node": ">= 0.12" @@ -4251,6 +4162,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -7475,52 +7402,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, "node_modules/tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", @@ -7703,15 +7584,6 @@ "uuid": "bin/uuid" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -7930,18 +7802,6 @@ "node": ">=12" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -8318,31 +8178,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "dependencies": { - "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - } - } - }, "@es-joy/jsdoccomment": { "version": "0.46.0", "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.46.0.tgz", @@ -8863,38 +8698,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "@tsconfig/node10": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", - "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", - "dev": true, - "optional": true, - "peer": true - }, - "@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "optional": true, - "peer": true - }, - "@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "optional": true, - "peer": true - }, - "@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "optional": true, - "peer": true - }, "@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -9351,17 +9154,6 @@ "dev": true, "requires": {} }, - "acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "acorn": "^8.11.0" - } - }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -9419,14 +9211,6 @@ "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", "dev": true }, - "arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "optional": true, - "peer": true - }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -9907,14 +9691,6 @@ "prompts": "^2.0.1" } }, - "create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "optional": true, - "peer": true - }, "cross-env": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", @@ -10004,14 +9780,6 @@ "wrappy": "1" } }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true, - "optional": true, - "peer": true - }, "diff-sequences": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", @@ -10131,6 +9899,18 @@ "es-errors": "^1.3.0" } }, + "es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "requires": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + } + }, "escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -10682,14 +10462,17 @@ "dev": true }, "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.5.tgz", + "integrity": "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==", "dev": true, "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" } }, "forwarded": { @@ -10880,6 +10663,15 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==" }, + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "requires": { + "has-symbols": "^1.0.3" + } + }, "hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -13122,29 +12914,6 @@ } } }, - "ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "optional": true, - "peer": true, - "requires": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - } - }, "tslib": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", @@ -13258,14 +13027,6 @@ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true }, - "v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "optional": true, - "peer": true - }, "v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -13423,14 +13184,6 @@ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true }, - "yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "optional": true, - "peer": true - }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", From 614822abb2f91bdc67bbb4c7b966f43a39f8a764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 10:31:19 +0200 Subject: [PATCH 09/24] chore: exclude one more vuln --- .ci/runChecks.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/runChecks.sh b/.ci/runChecks.sh index 25eb320..5c334d2 100755 --- a/.ci/runChecks.sh +++ b/.ci/runChecks.sh @@ -4,4 +4,4 @@ npm ci npm run build npm run lint npx license-check -npx better-npm-audit audit --exclude=1096727,1097682 +npx better-npm-audit audit --exclude=1106509,1096727,1097682 From 4bc764843ece9e0e773df366592c37bd6140c122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 10:42:21 +0200 Subject: [PATCH 10/24] refactor: simplfy routeRequiresRoles for only one possible role --- src/middlewares/routeRequiresRoles.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/middlewares/routeRequiresRoles.ts b/src/middlewares/routeRequiresRoles.ts index 55aabce..0f0f4a1 100644 --- a/src/middlewares/routeRequiresRoles.ts +++ b/src/middlewares/routeRequiresRoles.ts @@ -6,8 +6,9 @@ const debuggerInstance = debug('typescript-rest:middlewares:routeRequiresRoles') export function routeRequiresRoles( authenticator: { getRoles: (req: Request, res: Response) => Array }, - requiredRoles: Array + requiredRoles: Array | string ) { + if (typeof requiredRoles === 'string') requiredRoles = [requiredRoles]; if (requiredRoles.length === 0) throw new Error('At least one role must be specified.'); const roleRegex = /^[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+){0,}$/; From 38beff5ef4027128285c3829b0aa7ef04ff9f2ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 14:05:52 +0200 Subject: [PATCH 11/24] chore: add jsdoc --- src/middlewares/routeRequiresRoles.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/middlewares/routeRequiresRoles.ts b/src/middlewares/routeRequiresRoles.ts index 0f0f4a1..7848af3 100644 --- a/src/middlewares/routeRequiresRoles.ts +++ b/src/middlewares/routeRequiresRoles.ts @@ -4,6 +4,13 @@ import * as Errors from '../server/model/errors'; const debuggerInstance = debug('typescript-rest:middlewares:routeRequiresRoles'); +/** + * Middleware to check if the user has the required roles to access a route. + * + * @param authenticator extracts roles from the request. + * @param requiredRoles can be a single role or an array of roles. At least one role must be specified. If at least one of the roles matches the user's roles, access is granted. + * @returns the middleware function that checks if the user has the required roles. + */ export function routeRequiresRoles( authenticator: { getRoles: (req: Request, res: Response) => Array }, requiredRoles: Array | string From 7a1eabc1d7e2be92fce70fcfec1b0bfe597003a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 14:21:52 +0200 Subject: [PATCH 12/24] refactor: better dx, better naming --- ...Roles.ts => routeRequiresAuthorization.ts} | 16 +++++++-------- src/server/server-container.ts | 20 +++++++++---------- src/typescript-rest.ts | 2 +- .../middlewares/routeRequiresRoles.spec.ts | 13 +++++++----- 4 files changed, 26 insertions(+), 25 deletions(-) rename src/middlewares/{routeRequiresRoles.ts => routeRequiresAuthorization.ts} (73%) diff --git a/src/middlewares/routeRequiresRoles.ts b/src/middlewares/routeRequiresAuthorization.ts similarity index 73% rename from src/middlewares/routeRequiresRoles.ts rename to src/middlewares/routeRequiresAuthorization.ts index 7848af3..8051cc1 100644 --- a/src/middlewares/routeRequiresRoles.ts +++ b/src/middlewares/routeRequiresAuthorization.ts @@ -8,18 +8,18 @@ const debuggerInstance = debug('typescript-rest:middlewares:routeRequiresRoles') * Middleware to check if the user has the required roles to access a route. * * @param authenticator extracts roles from the request. - * @param requiredRoles can be a single role or an array of roles. At least one role must be specified. If at least one of the roles matches the user's roles, access is granted. + * @param roles can be a single role or an array of roles. At least one role must be specified. If at least one of the roles matches the user's roles, access is granted. * @returns the middleware function that checks if the user has the required roles. */ -export function routeRequiresRoles( +export function routeRequiresAuthorization( authenticator: { getRoles: (req: Request, res: Response) => Array }, - requiredRoles: Array | string + ...roles: [string, ...Array] ) { - if (typeof requiredRoles === 'string') requiredRoles = [requiredRoles]; - if (requiredRoles.length === 0) throw new Error('At least one role must be specified.'); + if (typeof roles === 'string') roles = [roles]; + if (roles.length === 0) throw new Error('At least one role must be specified.'); const roleRegex = /^[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+){0,}$/; - const nonMatchingRoles = requiredRoles.filter((role) => !roleRegex.test(role)); + const nonMatchingRoles = roles.filter((role) => !roleRegex.test(role)); if (nonMatchingRoles.length > 0) { throw new Error( `Invalid required role(s) specified: ${nonMatchingRoles.join(', ')}. Roles must match the pattern: ${roleRegex}.` @@ -31,9 +31,7 @@ export function routeRequiresRoles( if (debuggerInstance.enabled) debuggerInstance('Validating authentication roles: <%j>.', requestRoles); const transformedRoles = requestRoles.map(transformRole); - const isAuthorized = requiredRoles.some((requiredRole: string) => - isRoleMatched(requiredRole, transformedRoles) - ); + const isAuthorized = roles.some((requiredRole: string) => isRoleMatched(requiredRole, transformedRoles)); if (!isAuthorized) { next(new Errors.ForbiddenError('You are not allowed to access this endpoint.')); return; diff --git a/src/server/server-container.ts b/src/server/server-container.ts index 82974aa..e3b7b41 100644 --- a/src/server/server-container.ts +++ b/src/server/server-container.ts @@ -7,7 +7,7 @@ import * as debug from 'debug'; import * as express from 'express'; import * as _ from 'lodash'; import * as multer from 'multer'; -import { routeRequiresRoles } from '../middlewares/routeRequiresRoles'; +import { routeRequiresAuthorization } from '../middlewares/routeRequiresAuthorization'; import * as Errors from './model/errors'; import { ServiceClass, ServiceMethod } from './model/metadata'; import { @@ -337,6 +337,8 @@ export class ServerContainer { const authenticatorNames: Array = Object.keys(authenticatorMap); for (const authenticatorName of authenticatorNames) { const roles: Array = authenticatorMap[authenticatorName]; + if (roles.length === 0) continue; + this.debugger.build( 'Registering an authenticator middleware <%s> for method <%s>.', authenticatorName, @@ -346,15 +348,13 @@ export class ServerContainer { const authenticator = this.getAuthenticator(authenticatorName); if (authenticator.getMiddleware) result.push(authenticator.getMiddleware()); - if (roles.length) { - this.debugger.build( - 'Registering a role validator middleware <%s> for method <%s>.', - authenticatorName, - serviceMethod.name - ); - this.debugger.build('Roles: <%j>.', roles); - result.push(routeRequiresRoles(authenticator, roles)); - } + this.debugger.build( + 'Registering a role validator middleware <%s> for method <%s>.', + authenticatorName, + serviceMethod.name + ); + this.debugger.build('Roles: <%j>.', roles); + result.push(routeRequiresAuthorization(authenticator, roles[0], ...roles.slice(1))); } } diff --git a/src/typescript-rest.ts b/src/typescript-rest.ts index 4d4ba9e..a95aef6 100644 --- a/src/typescript-rest.ts +++ b/src/typescript-rest.ts @@ -7,7 +7,7 @@ import * as Return from './server/model/return-types'; export * from './decorators/methods'; export * from './decorators/parameters'; export * from './decorators/services'; -export * from './middlewares/routeRequiresRoles'; +export * from './middlewares/routeRequiresAuthorization'; export * from './server/model/server-types'; export * from './server/server'; diff --git a/test/unit/middlewares/routeRequiresRoles.spec.ts b/test/unit/middlewares/routeRequiresRoles.spec.ts index 58c0bfc..6d0f112 100644 --- a/test/unit/middlewares/routeRequiresRoles.spec.ts +++ b/test/unit/middlewares/routeRequiresRoles.spec.ts @@ -1,5 +1,5 @@ import * as express from 'express'; -import { Errors, routeRequiresRoles } from '../../../src/typescript-rest'; +import { Errors, routeRequiresAuthorization } from '../../../src/typescript-rest'; describe('routeRequiresRoles middleware', () => { const res = {} as any as express.Response; @@ -47,7 +47,7 @@ describe('routeRequiresRoles middleware', () => { const next = jest.fn(); const authenticator = { getRoles: () => userRoles }; - const fn = routeRequiresRoles(authenticator, requiredRoles); + const fn = routeRequiresAuthorization(authenticator, requiredRoles[0], ...requiredRoles.slice(1)); fn({ userRoles: userRoles } as any as express.Request, res, next); expect(next).toHaveBeenCalledTimes(1); @@ -71,7 +71,7 @@ describe('routeRequiresRoles middleware', () => { const next = jest.fn(); const authenticator = { getRoles: () => userRoles }; - const fn = routeRequiresRoles(authenticator, requiredRoles); + const fn = routeRequiresAuthorization(authenticator, requiredRoles[0], ...requiredRoles.slice(1)); fn({ userRoles: userRoles } as any as express.Request, res, next); expect(next).toHaveBeenCalledTimes(1); @@ -80,13 +80,16 @@ describe('routeRequiresRoles middleware', () => { }); test('should throw an error if no roles are specified', () => { - expect(() => routeRequiresRoles({ getRoles: () => [] }, [])).toThrow('At least one role must be specified.'); + expect( + // @ts-expect-error: Testing error throwing + () => routeRequiresAuthorization({ getRoles: () => [] }) + ).toThrow('At least one role must be specified.'); }); test.each(['admin:', 'admin::core', 'admin::', '*'])( 'should throw an error because the required role (%s) does not match the pattern', (role) => { - expect(() => routeRequiresRoles({ getRoles: () => [] }, [role])).toThrow( + expect(() => routeRequiresAuthorization({ getRoles: () => [] }, role)).toThrow( 'Invalid required role(s) specified' ); } From cb9117a5aebb170a33a72d5ac488322677eb03c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 14:23:51 +0200 Subject: [PATCH 13/24] chore: remove typecheck --- src/middlewares/routeRequiresAuthorization.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/middlewares/routeRequiresAuthorization.ts b/src/middlewares/routeRequiresAuthorization.ts index 8051cc1..430d8b3 100644 --- a/src/middlewares/routeRequiresAuthorization.ts +++ b/src/middlewares/routeRequiresAuthorization.ts @@ -15,7 +15,6 @@ export function routeRequiresAuthorization( authenticator: { getRoles: (req: Request, res: Response) => Array }, ...roles: [string, ...Array] ) { - if (typeof roles === 'string') roles = [roles]; if (roles.length === 0) throw new Error('At least one role must be specified.'); const roleRegex = /^[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+){0,}$/; From 6c1e4f16369dc11183ef3fd65619a5e778d3067f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 14:32:05 +0200 Subject: [PATCH 14/24] chore: wording --- src/middlewares/routeRequiresAuthorization.ts | 14 ++++++++------ test/unit/middlewares/routeRequiresRoles.spec.ts | 12 ++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/middlewares/routeRequiresAuthorization.ts b/src/middlewares/routeRequiresAuthorization.ts index 430d8b3..390c074 100644 --- a/src/middlewares/routeRequiresAuthorization.ts +++ b/src/middlewares/routeRequiresAuthorization.ts @@ -8,20 +8,20 @@ const debuggerInstance = debug('typescript-rest:middlewares:routeRequiresRoles') * Middleware to check if the user has the required roles to access a route. * * @param authenticator extracts roles from the request. - * @param roles can be a single role or an array of roles. At least one role must be specified. If at least one of the roles matches the user's roles, access is granted. + * @param permittedRoles can be a single role or an array of roles. At least one role must be specified. If at least one of the roles matches the user's roles, access is granted. * @returns the middleware function that checks if the user has the required roles. */ export function routeRequiresAuthorization( authenticator: { getRoles: (req: Request, res: Response) => Array }, - ...roles: [string, ...Array] + ...permittedRoles: [string, ...Array] ) { - if (roles.length === 0) throw new Error('At least one role must be specified.'); + if (permittedRoles.length === 0) throw new Error('At least one role must be specified.'); const roleRegex = /^[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+){0,}$/; - const nonMatchingRoles = roles.filter((role) => !roleRegex.test(role)); + const nonMatchingRoles = permittedRoles.filter((role) => !roleRegex.test(role)); if (nonMatchingRoles.length > 0) { throw new Error( - `Invalid required role(s) specified: ${nonMatchingRoles.join(', ')}. Roles must match the pattern: ${roleRegex}.` + `Invalid permitted role(s) specified: ${nonMatchingRoles.join(', ')}. Roles must match the pattern: ${roleRegex}.` ); } @@ -30,7 +30,9 @@ export function routeRequiresAuthorization( if (debuggerInstance.enabled) debuggerInstance('Validating authentication roles: <%j>.', requestRoles); const transformedRoles = requestRoles.map(transformRole); - const isAuthorized = roles.some((requiredRole: string) => isRoleMatched(requiredRole, transformedRoles)); + const isAuthorized = permittedRoles.some((requiredRole: string) => + isRoleMatched(requiredRole, transformedRoles) + ); if (!isAuthorized) { next(new Errors.ForbiddenError('You are not allowed to access this endpoint.')); return; diff --git a/test/unit/middlewares/routeRequiresRoles.spec.ts b/test/unit/middlewares/routeRequiresRoles.spec.ts index 6d0f112..5bceb44 100644 --- a/test/unit/middlewares/routeRequiresRoles.spec.ts +++ b/test/unit/middlewares/routeRequiresRoles.spec.ts @@ -43,11 +43,11 @@ describe('routeRequiresRoles middleware', () => { [['core:**:whatever'], ['core:messages:whatever:whatever']], [['**:whatever'], ['core:messages:whatever']], [['**:whatever'], ['core:messages:whatever:whatever']] - ])('should accept the given (%s) and required (%s) roles', (userRoles, requiredRoles) => { + ])('should accept the given (%s) and permitted (%s) roles', (userRoles, permittedRoles) => { const next = jest.fn(); const authenticator = { getRoles: () => userRoles }; - const fn = routeRequiresAuthorization(authenticator, requiredRoles[0], ...requiredRoles.slice(1)); + const fn = routeRequiresAuthorization(authenticator, permittedRoles[0], ...permittedRoles.slice(1)); fn({ userRoles: userRoles } as any as express.Request, res, next); expect(next).toHaveBeenCalledTimes(1); @@ -67,11 +67,11 @@ describe('routeRequiresRoles middleware', () => { [['core:*:*:*'], ['core:messages:whatever']], [['core:*:*:whatever'], ['core:messages:whatever']], [['core:*'], ['core:messages:whatever']] - ])('should reject the given (%s) and required (%s) roles', (userRoles, requiredRoles) => { + ])('should reject the given (%s) and permitted (%s) roles', (userRoles, permittedRoles) => { const next = jest.fn(); const authenticator = { getRoles: () => userRoles }; - const fn = routeRequiresAuthorization(authenticator, requiredRoles[0], ...requiredRoles.slice(1)); + const fn = routeRequiresAuthorization(authenticator, permittedRoles[0], ...permittedRoles.slice(1)); fn({ userRoles: userRoles } as any as express.Request, res, next); expect(next).toHaveBeenCalledTimes(1); @@ -87,10 +87,10 @@ describe('routeRequiresRoles middleware', () => { }); test.each(['admin:', 'admin::core', 'admin::', '*'])( - 'should throw an error because the required role (%s) does not match the pattern', + 'should throw an error because the permitted role (%s) does not match the pattern', (role) => { expect(() => routeRequiresAuthorization({ getRoles: () => [] }, role)).toThrow( - 'Invalid required role(s) specified' + 'Invalid permitted role(s) specified' ); } ); From 2e855824b5912132b04280a13b048cccfd43ab19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 14:34:45 +0200 Subject: [PATCH 15/24] fix: require role for security decorator --- src/decorators/services.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/decorators/services.ts b/src/decorators/services.ts index 6eed48b..4c32f59 100644 --- a/src/decorators/services.ts +++ b/src/decorators/services.ts @@ -73,8 +73,8 @@ export function Path(path: string) { * GET http://mydomain/people/123 (For all authorized users) * ``` */ -export function Security(roles?: string | Array, name?: string) { - roles = _.castArray(roles || '*'); +export function Security(roles: string | Array, name?: string) { + roles = _.castArray(roles); return new SecurityServiceDecorator('Security') .withObjectProperty('authenticator', name || 'default', roles) .createDecorator(); From 1218cf1d30de639a5115241a73d1e520f99bb77d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 14:40:11 +0200 Subject: [PATCH 16/24] fix: allow no empty array --- src/decorators/services.ts | 4 ++-- test/unit/decorators.spec.ts | 20 +++----------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/decorators/services.ts b/src/decorators/services.ts index 4c32f59..b59f5fa 100644 --- a/src/decorators/services.ts +++ b/src/decorators/services.ts @@ -73,8 +73,8 @@ export function Path(path: string) { * GET http://mydomain/people/123 (For all authorized users) * ``` */ -export function Security(roles: string | Array, name?: string) { - roles = _.castArray(roles); +export function Security(roles: string | [string, ...Array], name?: string) { + roles = Array.isArray(roles) ? roles : [roles]; return new SecurityServiceDecorator('Security') .withObjectProperty('authenticator', name || 'default', roles) .createDecorator(); diff --git a/test/unit/decorators.spec.ts b/test/unit/decorators.spec.ts index f629887..7183c9f 100644 --- a/test/unit/decorators.spec.ts +++ b/test/unit/decorators.spec.ts @@ -127,8 +127,7 @@ describe('Decorators', () => { }); it('should add a security set of roles to methods of a class', () => { - const roles = ['test-role', 'tes-role2']; - serviceDecorators.Security(roles)( + serviceDecorators.Security(['test-role', 'tes-role2'])( TestService.prototype, 'test', Object.getOwnPropertyDescriptor(TestService.prototype, 'test') @@ -136,21 +135,8 @@ describe('Decorators', () => { expect(serverContainer.registerServiceMethod).toHaveBeenCalledTimes(1); expect(serviceMethod.authenticator.default).toHaveLength(2); - expect(serviceMethod.authenticator.default).toContain(roles[0]); - expect(serviceMethod.authenticator.default).toContain(roles[1]); - }); - - it('should add a security validation to accept any role when empty is received', () => { - const role = ''; - serviceDecorators.Security(role)( - TestService.prototype, - 'test', - Object.getOwnPropertyDescriptor(TestService.prototype, 'test') - ); - - expect(serverContainer.registerServiceMethod).toHaveBeenCalledTimes(1); - expect(serviceMethod.authenticator.default).toHaveLength(1); - expect(serviceMethod.authenticator.default).toContain('*'); + expect(serviceMethod.authenticator.default).toContain('test-role'); + expect(serviceMethod.authenticator.default).toContain('test-role2'); }); it('should add a security validation to accept any role when undefined is received', () => { From 867a54d171e7f4e4dc98ccd188db611a689f34ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 14:40:40 +0200 Subject: [PATCH 17/24] fix: throw early if empty --- src/decorators/services.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/decorators/services.ts b/src/decorators/services.ts index b59f5fa..1e6fac5 100644 --- a/src/decorators/services.ts +++ b/src/decorators/services.ts @@ -75,6 +75,9 @@ export function Path(path: string) { */ export function Security(roles: string | [string, ...Array], name?: string) { roles = Array.isArray(roles) ? roles : [roles]; + + if (roles.length === 0) throw new Error('At least one role must be specified.'); + return new SecurityServiceDecorator('Security') .withObjectProperty('authenticator', name || 'default', roles) .createDecorator(); From 23b133be417240b9246e7e57b62c080d2f28fc03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 14:41:55 +0200 Subject: [PATCH 18/24] chore: better error --- src/middlewares/routeRequiresAuthorization.ts | 4 ++-- test/unit/middlewares/routeRequiresRoles.spec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/middlewares/routeRequiresAuthorization.ts b/src/middlewares/routeRequiresAuthorization.ts index 390c074..62dc7fd 100644 --- a/src/middlewares/routeRequiresAuthorization.ts +++ b/src/middlewares/routeRequiresAuthorization.ts @@ -8,14 +8,14 @@ const debuggerInstance = debug('typescript-rest:middlewares:routeRequiresRoles') * Middleware to check if the user has the required roles to access a route. * * @param authenticator extracts roles from the request. - * @param permittedRoles can be a single role or an array of roles. At least one role must be specified. If at least one of the roles matches the user's roles, access is granted. + * @param permittedRoles can be a single role or an array of roles. If at least one of the roles matches the user's roles, access is granted. * @returns the middleware function that checks if the user has the required roles. */ export function routeRequiresAuthorization( authenticator: { getRoles: (req: Request, res: Response) => Array }, ...permittedRoles: [string, ...Array] ) { - if (permittedRoles.length === 0) throw new Error('At least one role must be specified.'); + if (permittedRoles.length === 0) throw new Error('At least one permitted role must be specified.'); const roleRegex = /^[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+){0,}$/; const nonMatchingRoles = permittedRoles.filter((role) => !roleRegex.test(role)); diff --git a/test/unit/middlewares/routeRequiresRoles.spec.ts b/test/unit/middlewares/routeRequiresRoles.spec.ts index 5bceb44..55243fc 100644 --- a/test/unit/middlewares/routeRequiresRoles.spec.ts +++ b/test/unit/middlewares/routeRequiresRoles.spec.ts @@ -83,7 +83,7 @@ describe('routeRequiresRoles middleware', () => { expect( // @ts-expect-error: Testing error throwing () => routeRequiresAuthorization({ getRoles: () => [] }) - ).toThrow('At least one role must be specified.'); + ).toThrow('At least one permitted role must be specified.'); }); test.each(['admin:', 'admin::core', 'admin::', '*'])( From fc8a3ccf850a3a7b24516d4488c79fb3b2726388 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 14:42:22 +0200 Subject: [PATCH 19/24] test: more negative test cases --- test/unit/middlewares/routeRequiresRoles.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/middlewares/routeRequiresRoles.spec.ts b/test/unit/middlewares/routeRequiresRoles.spec.ts index 55243fc..3ec117a 100644 --- a/test/unit/middlewares/routeRequiresRoles.spec.ts +++ b/test/unit/middlewares/routeRequiresRoles.spec.ts @@ -86,7 +86,7 @@ describe('routeRequiresRoles middleware', () => { ).toThrow('At least one permitted role must be specified.'); }); - test.each(['admin:', 'admin::core', 'admin::', '*'])( + test.each(['admin:', ':admin', 'admin::core', 'admin::', '::admin', '*', '**'])( 'should throw an error because the permitted role (%s) does not match the pattern', (role) => { expect(() => routeRequiresAuthorization({ getRoles: () => [] }, role)).toThrow( From e727fe856617d3f83129d7202fdcd13ca7424fd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 14:45:13 +0200 Subject: [PATCH 20/24] chore: pr comments --- src/middlewares/routeRequiresAuthorization.ts | 29 +++++++------------ ....ts => routeRequiresAuthorization.spec.ts} | 2 +- 2 files changed, 12 insertions(+), 19 deletions(-) rename test/unit/middlewares/{routeRequiresRoles.spec.ts => routeRequiresAuthorization.spec.ts} (98%) diff --git a/src/middlewares/routeRequiresAuthorization.ts b/src/middlewares/routeRequiresAuthorization.ts index 62dc7fd..9197e85 100644 --- a/src/middlewares/routeRequiresAuthorization.ts +++ b/src/middlewares/routeRequiresAuthorization.ts @@ -2,15 +2,8 @@ import * as debug from 'debug'; import { NextFunction, Request, Response } from 'express'; import * as Errors from '../server/model/errors'; -const debuggerInstance = debug('typescript-rest:middlewares:routeRequiresRoles'); - -/** - * Middleware to check if the user has the required roles to access a route. - * - * @param authenticator extracts roles from the request. - * @param permittedRoles can be a single role or an array of roles. If at least one of the roles matches the user's roles, access is granted. - * @returns the middleware function that checks if the user has the required roles. - */ +const debuggerInstance = debug('typescript-rest:middlewares:routeRequiresAuthorization'); + export function routeRequiresAuthorization( authenticator: { getRoles: (req: Request, res: Response) => Array }, ...permittedRoles: [string, ...Array] @@ -29,10 +22,8 @@ export function routeRequiresAuthorization( const requestRoles = authenticator.getRoles(req, res); if (debuggerInstance.enabled) debuggerInstance('Validating authentication roles: <%j>.', requestRoles); - const transformedRoles = requestRoles.map(transformRole); - const isAuthorized = permittedRoles.some((requiredRole: string) => - isRoleMatched(requiredRole, transformedRoles) - ); + const transformedRoles = requestRoles.map(transformUserRole); + const isAuthorized = permittedRoles.some((permittedRole) => isRoleMatched(permittedRole, transformedRoles)); if (!isAuthorized) { next(new Errors.ForbiddenError('You are not allowed to access this endpoint.')); return; @@ -42,18 +33,20 @@ export function routeRequiresAuthorization( }; } -function isRoleMatched(requiredRole: string, userRoles: Array): boolean { +function isRoleMatched(permittedRole: string, userRoles: Array): boolean { for (const userRole of userRoles) { - const isMatch = userRole.test(requiredRole); + const isMatch = userRole.test(permittedRole); if (isMatch) return true; } return false; } -function transformRole(role: string): RegExp { - if (!role.includes('*')) return new RegExp(`^${role}$`); +function transformUserRole(userRole: string): RegExp { + if (!userRole.includes('*')) return new RegExp(`^${userRole}$`); - const regexString = role.replaceAll('**', '[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+){0,}').replaceAll('*', '[a-zA-Z0-9_-]+'); + const regexString = userRole + .replaceAll('**', '[a-zA-Z0-9_-]+(:[a-zA-Z0-9_-]+){0,}') + .replaceAll('*', '[a-zA-Z0-9_-]+'); return new RegExp(`^${regexString}$`); } diff --git a/test/unit/middlewares/routeRequiresRoles.spec.ts b/test/unit/middlewares/routeRequiresAuthorization.spec.ts similarity index 98% rename from test/unit/middlewares/routeRequiresRoles.spec.ts rename to test/unit/middlewares/routeRequiresAuthorization.spec.ts index 3ec117a..b2e0bb3 100644 --- a/test/unit/middlewares/routeRequiresRoles.spec.ts +++ b/test/unit/middlewares/routeRequiresAuthorization.spec.ts @@ -1,7 +1,7 @@ import * as express from 'express'; import { Errors, routeRequiresAuthorization } from '../../../src/typescript-rest'; -describe('routeRequiresRoles middleware', () => { +describe('routeRequiresAuthorization middleware', () => { const res = {} as any as express.Response; test.each([ From f7df90fa7c629ae37d9c24bde3fb03bff5678e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 14:46:25 +0200 Subject: [PATCH 21/24] test: update more tests --- test/unit/decorators.spec.ts | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/test/unit/decorators.spec.ts b/test/unit/decorators.spec.ts index 7183c9f..4503baf 100644 --- a/test/unit/decorators.spec.ts +++ b/test/unit/decorators.spec.ts @@ -127,7 +127,7 @@ describe('Decorators', () => { }); it('should add a security set of roles to methods of a class', () => { - serviceDecorators.Security(['test-role', 'tes-role2'])( + serviceDecorators.Security(['test-role', 'test-role2'])( TestService.prototype, 'test', Object.getOwnPropertyDescriptor(TestService.prototype, 'test') @@ -139,19 +139,6 @@ describe('Decorators', () => { expect(serviceMethod.authenticator.default).toContain('test-role2'); }); - it('should add a security validation to accept any role when undefined is received', () => { - const role: string = undefined; - serviceDecorators.Security(role)( - TestService.prototype, - 'test', - Object.getOwnPropertyDescriptor(TestService.prototype, 'test') - ); - - expect(serverContainer.registerServiceMethod).toHaveBeenCalledTimes(1); - expect(serviceMethod.authenticator.default).toHaveLength(1); - expect(serviceMethod.authenticator.default).toContain('*'); - }); - it('should set the default authenticator if no name is provided', () => { const role: string = 'test-role'; serviceDecorators.Security(role)( From ccace6aa2f7728673039b5cbcf7c666184b3c2d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 15:17:54 +0200 Subject: [PATCH 22/24] chore: test --- test/unit/middlewares/routeRequiresAuthorization.spec.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/unit/middlewares/routeRequiresAuthorization.spec.ts b/test/unit/middlewares/routeRequiresAuthorization.spec.ts index b2e0bb3..c1d8cd4 100644 --- a/test/unit/middlewares/routeRequiresAuthorization.spec.ts +++ b/test/unit/middlewares/routeRequiresAuthorization.spec.ts @@ -65,8 +65,7 @@ describe('routeRequiresAuthorization middleware', () => { ], [['core:*'], ['core:messages:whatever']], [['core:*:*:*'], ['core:messages:whatever']], - [['core:*:*:whatever'], ['core:messages:whatever']], - [['core:*'], ['core:messages:whatever']] + [['core:*:*:whatever'], ['core:messages:whatever']] ])('should reject the given (%s) and permitted (%s) roles', (userRoles, permittedRoles) => { const next = jest.fn(); @@ -79,7 +78,7 @@ describe('routeRequiresAuthorization middleware', () => { expect(next).toHaveBeenCalledWith(new Errors.ForbiddenError('You are not allowed to access this endpoint.')); }); - test('should throw an error if no roles are specified', () => { + test('should throw an error if no permitted roles are specified', () => { expect( // @ts-expect-error: Testing error throwing () => routeRequiresAuthorization({ getRoles: () => [] }) From 87beef3c32f6f8cfbc9721e26560e5bc3ceb66a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 15:21:05 +0200 Subject: [PATCH 23/24] test: add test --- test/unit/decorators.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/unit/decorators.spec.ts b/test/unit/decorators.spec.ts index 4503baf..db106ca 100644 --- a/test/unit/decorators.spec.ts +++ b/test/unit/decorators.spec.ts @@ -139,6 +139,17 @@ describe('Decorators', () => { expect(serviceMethod.authenticator.default).toContain('test-role2'); }); + it('should add a security validation to accept any role when undefined is received', () => { + expect(() => + // @ts-expect-error: Testing error throwing + serviceDecorators.Security([])( + TestService.prototype, + 'test', + Object.getOwnPropertyDescriptor(TestService.prototype, 'test') + ) + ).toThrow('At least one role must be specified.'); + }); + it('should set the default authenticator if no name is provided', () => { const role: string = 'test-role'; serviceDecorators.Security(role)( From 043b306738bd8a644563a5eaefbcce5a0f1c3e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Fri, 15 Aug 2025 15:22:10 +0200 Subject: [PATCH 24/24] chore: test name --- test/unit/decorators.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/decorators.spec.ts b/test/unit/decorators.spec.ts index db106ca..4160eee 100644 --- a/test/unit/decorators.spec.ts +++ b/test/unit/decorators.spec.ts @@ -139,7 +139,7 @@ describe('Decorators', () => { expect(serviceMethod.authenticator.default).toContain('test-role2'); }); - it('should add a security validation to accept any role when undefined is received', () => { + it('should throw an error when no roles are specified', () => { expect(() => // @ts-expect-error: Testing error throwing serviceDecorators.Security([])(