UNPKG

expacl

Version:

Express Access Control List middleware

120 lines (96 loc) 5.09 kB
import {expacl} from "../../expacl"; import {Request, Response, NextFunction} from 'express-serve-static-core'; const middleware: expacl.MiddlewareFactory = (opts: expacl.ACLOptions) => { const MAX_SUB_VARIANTS = 20; const route2parsed = (route: expacl.ACLRoute): expacl.ACLParsedRoute => { const splittedPart: (string | object)[] = (typeof route.path === 'string') ? route.path.split("/").filter(p => p.length > 0) : [route.path]; return { path: splittedPart, pathLen: splittedPart.length, roles: route.roles ? ((Array.isArray(route.roles) ? route.roles : [route.roles])) : ['*'], methods: route.methods ? (Array.isArray(route.methods) ? route.methods.map(m => m.toLowerCase() as expacl.Method) : [route.methods.toLowerCase() as expacl.Method]) : ['*'], action: route.action || opts.defaultAction || 'allow', }; }; const parsedRouteSubVariants = (route: expacl.ACLParsedRoute): expacl.ACLParsedRoute[] => { const arr = [route] .concat( new Array(MAX_SUB_VARIANTS) .fill(undefined) .map((_: any, idx: number) => Object.assign( {}, route, { path: route.path.concat(new Array(idx + 1).fill('*')), pathLen: route.pathLen + idx + 1 })) ); return arr; }; const parseRoute = (route: expacl.ACLRoute): expacl.ACLParsedRoute[] => { const parsedRoute = route2parsed(route); if (route.subroutes && route.subroutes.length > 0) { if (parsedRoute.path[parsedRoute.pathLen - 1] === '*') { throw new Error(`${'*'} (any) path route should not have subroutes`); } const subroutes = parseRoutes(route.subroutes); return subroutes.map(r => { return { path: parsedRoute.path.concat(r.path), pathLen: parsedRoute.pathLen + r.pathLen, roles: r.roles, methods: r.methods, action: r.action, } as expacl.ACLParsedRoute }).concat(route.transient ? [] : [parsedRoute]); } else { return route.transient ? [] : ((parsedRoute.path[parsedRoute.pathLen - 1] === '*') ? parsedRouteSubVariants(parsedRoute) : [parsedRoute]); } }; const parseRoutes = (routes: expacl.ACLRoute[]): expacl.ACLParsedRoute[] => { return routes .map((route: expacl.ACLRoute): expacl.ACLParsedRoute[] => parseRoute(route)) .reduce((acc: expacl.ACLParsedRoute[], val: expacl.ACLParsedRoute[]) => acc.concat(val), []); }; const routes = parseRoutes(opts.routes); const _middleware: expacl.Middleware = (req: Request, res: Response, next: NextFunction): any => { const path: string[] = (opts.resource ? opts.resource(req as expacl.ACLRequest) : req.url).split('/').filter((p: string) => p.length > 0); const pathLen = path.length; const method: expacl.Method = req.method.toLowerCase() as expacl.Method; const notAllowed = () => { const authenticated = opts.authenticated || ((req: expacl.ACLRequest) => !!req.user); if (authenticated(req as expacl.ACLRequest)) { return (opts.onNotAuthorized) ? opts.onNotAuthorized(req as expacl.ACLRequest, res, next) : res.status(403).send("403 Not authorized"); } else { return (opts.onNotAuthenticated) ? opts.onNotAuthenticated(req as expacl.ACLRequest, res, next) : res.status(401).send("401 Not authenticated"); } }; const notFound = () => (opts.onNotFound) ? opts.onNotFound(req as expacl.ACLRequest, res, next) : res.status(404).send("404 Not found"); const route: expacl.ACLParsedRoute | undefined = routes.find(r => { const shallowCheck = (r.pathLen === pathLen) && (r.methods.includes('*') || r.methods.includes(method)); if (!shallowCheck) { return false; } const pathCheck = r.path.reduce((acc, p, idx) => { return acc && ((typeof p === 'string') ? ((p === '*') || (p === path[idx])) : ((p as RegExp).test(path[idx]))); }, true); return pathCheck; }); if (!route) { return (opts.missingRoute != 'allow') ? notFound() : next(); } if (route.roles.includes('*')) { return next(); } const roles: string[] = (opts.roles ? opts.roles(req as expacl.ACLRequest) : ((req as expacl.ACLRequest).user ? (req as expacl.ACLRequest).user.roles : undefined)) || []; const roleCheck = roles.findIndex(r => route.roles.includes(r)); if (roleCheck === -1) { notAllowed(); } return next(); }; return _middleware; }; export {middleware};