mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat: Add Licensed decorator (no-changelog) (#7828)
Github issue / Community forum post (link here to close automatically):
This commit is contained in:
parent
d667bca658
commit
27e048c201
15
packages/cli/src/decorators/Licensed.ts
Normal file
15
packages/cli/src/decorators/Licensed.ts
Normal 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);
|
||||||
|
};
|
||||||
|
};
|
|
@ -2,3 +2,4 @@ export const CONTROLLER_ROUTES = 'CONTROLLER_ROUTES';
|
||||||
export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH';
|
export const CONTROLLER_BASE_PATH = 'CONTROLLER_BASE_PATH';
|
||||||
export const CONTROLLER_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES';
|
export const CONTROLLER_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES';
|
||||||
export const CONTROLLER_AUTH_ROLES = 'CONTROLLER_AUTH_ROLES';
|
export const CONTROLLER_AUTH_ROLES = 'CONTROLLER_AUTH_ROLES';
|
||||||
|
export const CONTROLLER_LICENSE_FEATURES = 'CONTROLLER_LICENSE_FEATURES';
|
||||||
|
|
|
@ -3,3 +3,4 @@ export { RestController } from './RestController';
|
||||||
export { Get, Post, Put, Patch, Delete } from './Route';
|
export { Get, Post, Put, Patch, Delete } from './Route';
|
||||||
export { Middleware } from './Middleware';
|
export { Middleware } from './Middleware';
|
||||||
export { registerController } from './registerController';
|
export { registerController } from './registerController';
|
||||||
|
export { Licensed } from './Licensed';
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to
|
||||||
import {
|
import {
|
||||||
CONTROLLER_AUTH_ROLES,
|
CONTROLLER_AUTH_ROLES,
|
||||||
CONTROLLER_BASE_PATH,
|
CONTROLLER_BASE_PATH,
|
||||||
|
CONTROLLER_LICENSE_FEATURES,
|
||||||
CONTROLLER_MIDDLEWARES,
|
CONTROLLER_MIDDLEWARES,
|
||||||
CONTROLLER_ROUTES,
|
CONTROLLER_ROUTES,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
|
@ -13,9 +14,13 @@ import type {
|
||||||
AuthRole,
|
AuthRole,
|
||||||
AuthRoleMetadata,
|
AuthRoleMetadata,
|
||||||
Controller,
|
Controller,
|
||||||
|
LicenseMetadata,
|
||||||
MiddlewareMetadata,
|
MiddlewareMetadata,
|
||||||
RouteMetadata,
|
RouteMetadata,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import type { BooleanLicenseFeature } from '@/Interfaces';
|
||||||
|
import Container from 'typedi';
|
||||||
|
import { License } from '@/License';
|
||||||
|
|
||||||
export const createAuthMiddleware =
|
export const createAuthMiddleware =
|
||||||
(authRole: AuthRole): RequestHandler =>
|
(authRole: AuthRole): RequestHandler =>
|
||||||
|
@ -31,6 +36,25 @@ export const createAuthMiddleware =
|
||||||
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
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[] = [];
|
const authFreeRoutes: string[] = [];
|
||||||
|
|
||||||
export const canSkipAuth = (method: string, path: string): boolean =>
|
export const canSkipAuth = (method: string, path: string): boolean =>
|
||||||
|
@ -49,6 +73,9 @@ export const registerController = (app: Application, config: Config, cObj: objec
|
||||||
| AuthRoleMetadata
|
| AuthRoleMetadata
|
||||||
| undefined;
|
| undefined;
|
||||||
const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[];
|
const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[];
|
||||||
|
const licenseFeatures = Reflect.getMetadata(CONTROLLER_LICENSE_FEATURES, controllerClass) as
|
||||||
|
| LicenseMetadata
|
||||||
|
| undefined;
|
||||||
if (routes.length > 0) {
|
if (routes.length > 0) {
|
||||||
const router = Router({ mergeParams: true });
|
const router = Router({ mergeParams: true });
|
||||||
const restBasePath = config.getEnv('endpoints.rest');
|
const restBasePath = config.getEnv('endpoints.rest');
|
||||||
|
@ -63,10 +90,12 @@ export const registerController = (app: Application, config: Config, cObj: objec
|
||||||
routes.forEach(
|
routes.forEach(
|
||||||
({ method, path, middlewares: routeMiddlewares, handlerName, usesTemplates }) => {
|
({ method, path, middlewares: routeMiddlewares, handlerName, usesTemplates }) => {
|
||||||
const authRole = authRoles && (authRoles[handlerName] ?? authRoles['*']);
|
const authRole = authRoles && (authRoles[handlerName] ?? authRoles['*']);
|
||||||
|
const features = licenseFeatures && (licenseFeatures[handlerName] ?? licenseFeatures['*']);
|
||||||
const handler = async (req: Request, res: Response) => controller[handlerName](req, res);
|
const handler = async (req: Request, res: Response) => controller[handlerName](req, res);
|
||||||
router[method](
|
router[method](
|
||||||
path,
|
path,
|
||||||
...(authRole ? [createAuthMiddleware(authRole)] : []),
|
...(authRole ? [createAuthMiddleware(authRole)] : []),
|
||||||
|
...(features ? [createLicenseMiddleware(features)] : []),
|
||||||
...controllerMiddlewares,
|
...controllerMiddlewares,
|
||||||
...routeMiddlewares,
|
...routeMiddlewares,
|
||||||
usesTemplates ? handler : send(handler),
|
usesTemplates ? handler : send(handler),
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import type { Request, Response, RequestHandler } from 'express';
|
import type { Request, Response, RequestHandler } from 'express';
|
||||||
import type { RoleNames, RoleScopes } from '@db/entities/Role';
|
import type { RoleNames, RoleScopes } from '@db/entities/Role';
|
||||||
|
import type { BooleanLicenseFeature } from '@/Interfaces';
|
||||||
|
|
||||||
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
||||||
|
|
||||||
export type AuthRole = [RoleScopes, RoleNames] | 'any' | 'none';
|
export type AuthRole = [RoleScopes, RoleNames] | 'any' | 'none';
|
||||||
export type AuthRoleMetadata = Record<string, AuthRole>;
|
export type AuthRoleMetadata = Record<string, AuthRole>;
|
||||||
|
|
||||||
|
export type LicenseMetadata = Record<string, BooleanLicenseFeature[]>;
|
||||||
|
|
||||||
export interface MiddlewareMetadata {
|
export interface MiddlewareMetadata {
|
||||||
handlerName: string;
|
handlerName: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,23 +2,13 @@ import { Container, Service } from 'typedi';
|
||||||
|
|
||||||
import * as ResponseHelper from '@/ResponseHelper';
|
import * as ResponseHelper from '@/ResponseHelper';
|
||||||
import { VariablesRequest } from '@/requests';
|
import { VariablesRequest } from '@/requests';
|
||||||
import { Authorized, Delete, Get, Patch, Post, RestController } from '@/decorators';
|
import { Authorized, Delete, Get, Licensed, Patch, Post, RestController } from '@/decorators';
|
||||||
import {
|
import {
|
||||||
VariablesService,
|
VariablesService,
|
||||||
VariablesLicenseError,
|
VariablesLicenseError,
|
||||||
VariablesValidationError,
|
VariablesValidationError,
|
||||||
} from './variables.service.ee';
|
} from './variables.service.ee';
|
||||||
import { isVariablesEnabled } from './enviromentHelpers';
|
|
||||||
import { Logger } from '@/Logger';
|
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()
|
@Service()
|
||||||
@Authorized()
|
@Authorized()
|
||||||
|
@ -34,7 +24,8 @@ export class VariablesController {
|
||||||
return Container.get(VariablesService).getAllCached();
|
return Container.get(VariablesService).getAllCached();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/', { middlewares: [variablesLicensedMiddleware] })
|
@Post('/')
|
||||||
|
@Licensed('feat:variables')
|
||||||
async createVariable(req: VariablesRequest.Create) {
|
async createVariable(req: VariablesRequest.Create) {
|
||||||
if (req.user.globalRole.name !== 'owner') {
|
if (req.user.globalRole.name !== 'owner') {
|
||||||
this.logger.info('Attempt to update a variable blocked due to lack of permissions', {
|
this.logger.info('Attempt to update a variable blocked due to lack of permissions', {
|
||||||
|
@ -66,7 +57,8 @@ export class VariablesController {
|
||||||
return variable;
|
return variable;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch('/:id', { middlewares: [variablesLicensedMiddleware] })
|
@Patch('/:id')
|
||||||
|
@Licensed('feat:variables')
|
||||||
async updateVariable(req: VariablesRequest.Update) {
|
async updateVariable(req: VariablesRequest.Update) {
|
||||||
const id = req.params.id;
|
const id = req.params.id;
|
||||||
if (req.user.globalRole.name !== 'owner') {
|
if (req.user.globalRole.name !== 'owner') {
|
||||||
|
|
Loading…
Reference in a new issue