Skip to content

Commit

Permalink
feat: Add Licensed decorator (no-changelog) (#7828)
Browse files Browse the repository at this point in the history
Github issue / Community forum post (link here to close automatically):
  • Loading branch information
valya committed Nov 27, 2023
1 parent d667bca commit 27e048c
Show file tree
Hide file tree
Showing 6 changed files with 54 additions and 13 deletions.
15 changes: 15 additions & 0 deletions packages/cli/src/decorators/Licensed.ts
Original file line number Diff line number Diff line change
@@ -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);
};
};
1 change: 1 addition & 0 deletions packages/cli/src/decorators/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
1 change: 1 addition & 0 deletions packages/cli/src/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
29 changes: 29 additions & 0 deletions packages/cli/src/decorators/registerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,21 @@ 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';
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 =>
Expand All @@ -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 =>
Expand All @@ -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');
Expand All @@ -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),
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/decorators/types.ts
Original file line number Diff line number Diff line change
@@ -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<string, AuthRole>;

export type LicenseMetadata = Record<string, BooleanLicenseFeature[]>;

export interface MiddlewareMetadata {
handlerName: string;
}
Expand Down
18 changes: 5 additions & 13 deletions packages/cli/src/environments/variables/variables.controller.ee.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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', {
Expand Down Expand Up @@ -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') {
Expand Down

0 comments on commit 27e048c

Please sign in to comment.