diff --git a/packages/cli/src/decorators/Licensed.ts b/packages/cli/src/decorators/Licensed.ts new file mode 100644 index 0000000000..e19a24f0f0 --- /dev/null +++ b/packages/cli/src/decorators/Licensed.ts @@ -0,0 +1,15 @@ +import type { BooleanLicenseFeature } from '@/Interfaces'; +import type { LicenseMetadata } from './types'; +import { CONTROLLER_LICENSE_FEATURES } from './constants'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const Licensed = (features: BooleanLicenseFeature | BooleanLicenseFeature[]) => { + // eslint-disable-next-line @typescript-eslint/ban-types + return (target: Function | object, handlerName?: string) => { + const controllerClass = handlerName ? target.constructor : target; + const license = (Reflect.getMetadata(CONTROLLER_LICENSE_FEATURES, controllerClass) ?? + {}) as LicenseMetadata; + license[handlerName ?? '*'] = Array.isArray(features) ? features : [features]; + Reflect.defineMetadata(CONTROLLER_LICENSE_FEATURES, license, controllerClass); + }; +}; diff --git a/packages/cli/src/decorators/constants.ts b/packages/cli/src/decorators/constants.ts index 5a5efc938d..44f158ca5a 100644 --- a/packages/cli/src/decorators/constants.ts +++ b/packages/cli/src/decorators/constants.ts @@ -2,3 +2,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'; +export const CONTROLLER_LICENSE_FEATURES = 'CONTROLLER_LICENSE_FEATURES'; diff --git a/packages/cli/src/decorators/index.ts b/packages/cli/src/decorators/index.ts index 0e683d410c..5ce2038f4d 100644 --- a/packages/cli/src/decorators/index.ts +++ b/packages/cli/src/decorators/index.ts @@ -3,3 +3,4 @@ export { RestController } from './RestController'; export { Get, Post, Put, Patch, Delete } from './Route'; export { Middleware } from './Middleware'; export { registerController } from './registerController'; +export { Licensed } from './Licensed'; diff --git a/packages/cli/src/decorators/registerController.ts b/packages/cli/src/decorators/registerController.ts index 8a21580758..0f814f214d 100644 --- a/packages/cli/src/decorators/registerController.ts +++ b/packages/cli/src/decorators/registerController.ts @@ -6,6 +6,7 @@ import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to import { CONTROLLER_AUTH_ROLES, CONTROLLER_BASE_PATH, + CONTROLLER_LICENSE_FEATURES, CONTROLLER_MIDDLEWARES, CONTROLLER_ROUTES, } from './constants'; @@ -13,9 +14,13 @@ import type { AuthRole, AuthRoleMetadata, Controller, + LicenseMetadata, MiddlewareMetadata, RouteMetadata, } from './types'; +import type { BooleanLicenseFeature } from '@/Interfaces'; +import Container from 'typedi'; +import { License } from '@/License'; export const createAuthMiddleware = (authRole: AuthRole): RequestHandler => @@ -31,6 +36,25 @@ export const createAuthMiddleware = res.status(403).json({ status: 'error', message: 'Unauthorized' }); }; +export const createLicenseMiddleware = + (features: BooleanLicenseFeature[]): RequestHandler => + (_req, res, next) => { + if (features.length === 0) { + return next(); + } + + const licenseService = Container.get(License); + + const hasAllFeatures = features.every((feature) => licenseService.isFeatureEnabled(feature)); + if (!hasAllFeatures) { + return res + .status(403) + .json({ status: 'error', message: 'Plan lacks license for this feature' }); + } + + return next(); + }; + const authFreeRoutes: string[] = []; export const canSkipAuth = (method: string, path: string): boolean => @@ -49,6 +73,9 @@ export const registerController = (app: Application, config: Config, cObj: objec | AuthRoleMetadata | undefined; const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[]; + const licenseFeatures = Reflect.getMetadata(CONTROLLER_LICENSE_FEATURES, controllerClass) as + | LicenseMetadata + | undefined; if (routes.length > 0) { const router = Router({ mergeParams: true }); const restBasePath = config.getEnv('endpoints.rest'); @@ -63,10 +90,12 @@ export const registerController = (app: Application, config: Config, cObj: objec routes.forEach( ({ method, path, middlewares: routeMiddlewares, handlerName, usesTemplates }) => { const authRole = authRoles && (authRoles[handlerName] ?? authRoles['*']); + const features = licenseFeatures && (licenseFeatures[handlerName] ?? licenseFeatures['*']); const handler = async (req: Request, res: Response) => controller[handlerName](req, res); router[method]( path, ...(authRole ? [createAuthMiddleware(authRole)] : []), + ...(features ? [createLicenseMiddleware(features)] : []), ...controllerMiddlewares, ...routeMiddlewares, usesTemplates ? handler : send(handler), diff --git a/packages/cli/src/decorators/types.ts b/packages/cli/src/decorators/types.ts index 4d0f7d43b8..2d57cc1fd6 100644 --- a/packages/cli/src/decorators/types.ts +++ b/packages/cli/src/decorators/types.ts @@ -1,11 +1,14 @@ import type { Request, Response, RequestHandler } from 'express'; import type { RoleNames, RoleScopes } from '@db/entities/Role'; +import type { BooleanLicenseFeature } from '@/Interfaces'; export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete'; export type AuthRole = [RoleScopes, RoleNames] | 'any' | 'none'; export type AuthRoleMetadata = Record; +export type LicenseMetadata = Record; + export interface MiddlewareMetadata { handlerName: string; } diff --git a/packages/cli/src/environments/variables/variables.controller.ee.ts b/packages/cli/src/environments/variables/variables.controller.ee.ts index 66eafe6c11..9651b5173e 100644 --- a/packages/cli/src/environments/variables/variables.controller.ee.ts +++ b/packages/cli/src/environments/variables/variables.controller.ee.ts @@ -2,23 +2,13 @@ import { Container, Service } from 'typedi'; import * as ResponseHelper from '@/ResponseHelper'; import { VariablesRequest } from '@/requests'; -import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators'; +import { Authorized, Delete, Get, Licensed, Patch, Post, RestController } from '@/decorators'; import { VariablesService, VariablesLicenseError, VariablesValidationError, } from './variables.service.ee'; -import { isVariablesEnabled } from './enviromentHelpers'; import { Logger } from '@/Logger'; -import type { RequestHandler } from 'express'; - -const variablesLicensedMiddleware: RequestHandler = (req, res, next) => { - if (isVariablesEnabled()) { - next(); - } else { - res.status(403).json({ status: 'error', message: 'Unauthorized' }); - } -}; @Service() @Authorized() @@ -34,7 +24,8 @@ export class VariablesController { return Container.get(VariablesService).getAllCached(); } - @Post('/', { middlewares: [variablesLicensedMiddleware] }) + @Post('/') + @Licensed('feat:variables') async createVariable(req: VariablesRequest.Create) { if (req.user.globalRole.name !== 'owner') { this.logger.info('Attempt to update a variable blocked due to lack of permissions', { @@ -66,7 +57,8 @@ export class VariablesController { return variable; } - @Patch('/:id', { middlewares: [variablesLicensedMiddleware] }) + @Patch('/:id') + @Licensed('feat:variables') async updateVariable(req: VariablesRequest.Update) { const id = req.params.id; if (req.user.globalRole.name !== 'owner') {