1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-09-11 13:15:28 +03:00

feat: Add Licensed decorator (no-changelog) (#7828)

Github issue / Community forum post (link here to close automatically):
This commit is contained in:
Val 2023-11-27 13:46:18 +00:00 committed by GitHub
parent d667bca658
commit 27e048c201
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 54 additions and 13 deletions

View File

@ -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);
};
};

View File

@ -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';

View File

@ -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';

View File

@ -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),

View File

@ -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;
}

View File

@ -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') {