expacl
Version:
Express Access Control List middleware
120 lines (96 loc) • 5.09 kB
text/typescript
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};