refactor(core): Setup decorator based RBAC (no-changelog) (#5787)
This commit is contained in:
committed by
GitHub
parent
feb2ba09b9
commit
1eeadc6114
16
packages/cli/src/decorators/Authorized.ts
Normal file
16
packages/cli/src/decorators/Authorized.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { CONTROLLER_AUTH_ROLES } from './constants';
|
||||
import type { AuthRoleMetadata } from './types';
|
||||
|
||||
export function Authorized(authRole: AuthRoleMetadata[string] = 'any'): Function {
|
||||
return function (target: Function | Object, handlerName?: string) {
|
||||
const controllerClass = handlerName ? target.constructor : target;
|
||||
const authRoles = (Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) ??
|
||||
{}) as AuthRoleMetadata;
|
||||
authRoles[handlerName ?? '*'] = authRole;
|
||||
Reflect.defineMetadata(CONTROLLER_AUTH_ROLES, authRoles, controllerClass);
|
||||
};
|
||||
}
|
||||
|
||||
export const NoAuthRequired = () => Authorized('none');
|
||||
@@ -1,3 +1,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';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { Authorized, NoAuthRequired } from './Authorized';
|
||||
export { RestController } from './RestController';
|
||||
export { Get, Post, Put, Patch, Delete } from './Route';
|
||||
export { Middleware } from './Middleware';
|
||||
|
||||
@@ -1,10 +1,36 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import { Router } from 'express';
|
||||
import type { Config } from '@/config';
|
||||
import { CONTROLLER_BASE_PATH, CONTROLLER_MIDDLEWARES, CONTROLLER_ROUTES } from './constants';
|
||||
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
|
||||
import type { Application, Request, Response, RequestHandler } from 'express';
|
||||
import type { Controller, MiddlewareMetadata, RouteMetadata } from './types';
|
||||
import type { Config } from '@/config';
|
||||
import type { AuthenticatedRequest } from '@/requests';
|
||||
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
|
||||
import {
|
||||
CONTROLLER_AUTH_ROLES,
|
||||
CONTROLLER_BASE_PATH,
|
||||
CONTROLLER_MIDDLEWARES,
|
||||
CONTROLLER_ROUTES,
|
||||
} from './constants';
|
||||
import type {
|
||||
AuthRole,
|
||||
AuthRoleMetadata,
|
||||
Controller,
|
||||
MiddlewareMetadata,
|
||||
RouteMetadata,
|
||||
} from './types';
|
||||
|
||||
export const createAuthMiddleware =
|
||||
(authRole: AuthRole): RequestHandler =>
|
||||
({ user }: AuthenticatedRequest, res, next) => {
|
||||
if (authRole === 'none') return next();
|
||||
|
||||
if (!user) return res.status(401).json({ status: 'error', message: 'Unauthorized' });
|
||||
|
||||
const { globalRole } = user;
|
||||
if (authRole === 'any' || (globalRole.scope === authRole[0] && globalRole.name === authRole[1]))
|
||||
return next();
|
||||
|
||||
res.status(403).json({ status: 'error', message: 'Unauthorized' });
|
||||
};
|
||||
|
||||
export const registerController = (app: Application, config: Config, controller: object) => {
|
||||
const controllerClass = controller.constructor;
|
||||
@@ -14,11 +40,16 @@ export const registerController = (app: Application, config: Config, controller:
|
||||
if (!controllerBasePath)
|
||||
throw new Error(`${controllerClass.name} is missing the RestController decorator`);
|
||||
|
||||
const authRoles = Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) as
|
||||
| AuthRoleMetadata
|
||||
| undefined;
|
||||
const routes = Reflect.getMetadata(CONTROLLER_ROUTES, controllerClass) as RouteMetadata[];
|
||||
if (routes.length > 0) {
|
||||
const router = Router({ mergeParams: true });
|
||||
const restBasePath = config.getEnv('endpoints.rest');
|
||||
const prefix = `/${[restBasePath, controllerBasePath].join('/')}`.replace(/\/+/g, '/');
|
||||
const prefix = `/${[restBasePath, controllerBasePath].join('/')}`
|
||||
.replace(/\/+/g, '/')
|
||||
.replace(/\/$/, '');
|
||||
|
||||
const controllerMiddlewares = (
|
||||
(Reflect.getMetadata(CONTROLLER_MIDDLEWARES, controllerClass) ?? []) as MiddlewareMetadata[]
|
||||
@@ -28,8 +59,10 @@ export const registerController = (app: Application, config: Config, controller:
|
||||
);
|
||||
|
||||
routes.forEach(({ method, path, middlewares: routeMiddlewares, handlerName }) => {
|
||||
const authRole = authRoles && (authRoles[handlerName] ?? authRoles['*']);
|
||||
router[method](
|
||||
path,
|
||||
...(authRole ? [createAuthMiddleware(authRole)] : []),
|
||||
...controllerMiddlewares,
|
||||
...routeMiddlewares,
|
||||
send(async (req: Request, res: Response) =>
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { Request, Response, RequestHandler } from 'express';
|
||||
import type { RoleNames, RoleScopes } from '@db/entities/Role';
|
||||
|
||||
export type Method = 'get' | 'post' | 'put' | 'patch' | 'delete';
|
||||
|
||||
export type AuthRole = [RoleScopes, RoleNames] | 'any' | 'none';
|
||||
export type AuthRoleMetadata = Record<string, AuthRole>;
|
||||
|
||||
export interface MiddlewareMetadata {
|
||||
handlerName: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user