mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-12 13:27:31 -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_MIDDLEWARES = 'CONTROLLER_MIDDLEWARES';
|
||||
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 { Middleware } from './Middleware';
|
||||
export { registerController } from './registerController';
|
||||
export { Licensed } from './Licensed';
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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') {
|
||||
|
|
Loading…
Reference in a new issue