refactor(core): Setup decorator based RBAC (no-changelog) (#5787)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2023-04-24 09:45:31 +00:00
committed by GitHub
parent feb2ba09b9
commit 1eeadc6114
23 changed files with 133 additions and 165 deletions

View 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');

View File

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

View File

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

View File

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

View File

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