Added Permission Middleware, Renaming

This commit is contained in:
Sebastian Seedorf
2020-11-15 22:03:09 +01:00
parent da4cda2507
commit 2c15f0ba23
15 changed files with 256 additions and 95 deletions

View File

@@ -33,6 +33,8 @@ module.exports = {
"comma-dangle": ["error", "always-multiline"],
"comma-style": "error",
"semi": "error",
"no-implicit-coercion": "error",
"promise/always-return": "off",
},
};

View File

@@ -2,13 +2,10 @@ import * as createError from 'http-errors';
import * as express from 'express';
import {NextFunction, Request, Response} from 'express';
import * as path from 'path';
import * as redisStore from 'connect-redis';
import * as session from 'express-session';
import * as sassMiddleware from 'node-sass-middleware';
import * as compression from 'compression';
import indexRouter from './routes';
import {HttpLogger, Redis, Config, setupAuthProxy, getReloadRouter, polyfillRoute} from './utils';
import {Store} from 'express-session';
import {HttpLogger, Config, AuthProxy, AutoReloader, Polyfill, Session} from './utils';
export const app = express();
@@ -31,26 +28,15 @@ app.use(express.urlencoded({extended: false}));
app.use(compression());
// auth proxy middleware
router.use(setupAuthProxy);
router.use(AuthProxy.router);
// auto reloader (when running in debug mode)
router.use(getReloadRouter());
router.use(AutoReloader.router);
// session
let sessionStore: Store|undefined = undefined;
if (Redis.client) {
const RedisStore = redisStore(session);
sessionStore = new RedisStore({client: Redis.client});
}
router.use(session({
store: sessionStore,
secret: Config.SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: {secure: false},
}));
router.use(Session.getRouter());
// static config
router.use("/js/polyfill.js", polyfillRoute);
router.use("/js/polyfill.js", Polyfill.router);
router.use(sassMiddleware({
src: path.join(__dirname, '../public'),
dest: path.join(__dirname, '../public'),

View File

@@ -4,7 +4,7 @@ import {Resolvable} from './helpers/resolvable';
import fetch from 'node-fetch';
import {urlJoin} from './helpers/urlJoin';
export const setupAuthProxy: RequestHandler = (req: Request, res, next) => {
const router: RequestHandler = (req: Request, res, next) => {
const resolvable = new Resolvable(async () => {
if (!Config.USERINFO_HEADER) {
return undefined;
@@ -34,3 +34,7 @@ export const setupAuthProxy: RequestHandler = (req: Request, res, next) => {
};
next();
};
export const AuthProxy = {
router,
};

View File

@@ -1,64 +1,63 @@
import {Router} from 'express';
import {Config} from './config';
import {Logger} from './logging';
import {Config, Logger} from '.';
import {urlJoin} from './helpers/urlJoin';
import {v4} from 'uuid';
import Timeout = NodeJS.Timeout;
export function getReloadRouter(): Router {
const reloadRouter = Router();
if (!Config.isProduction) {
let uuid = v4();
let updateTimeout: Timeout|undefined = undefined;
import("node-watch").then((watch) => {
watch.default('public', {recursive: true}, (evt, name) => {
if (updateTimeout !== undefined) clearTimeout(updateTimeout);
updateTimeout = setTimeout(() => {
uuid = v4();
}, 200);
});
}).catch((err) => { Logger.error(err); });
reloadRouter.get("/auto-reload/client.js", (req, res) => {
Logger.debug(req.url, req.originalUrl, req.baseUrl);
res.setHeader('Content-Type', "application/javascript");
// language=JavaScript
res.send(`
const loc = window.location;
const url = loc.protocol+'//'+loc.host+'${urlJoin(req.baseUrl, "/auto-reload")}';
// const parse = async res => (await res.json()).uuid;
const parse = function(res) {
return res.json()
.then(function(json) {return json.uuid;}) }
let hash = undefined;
let hadError = false;
setInterval(function() {
try {
fetch(url)
.then(function(res) { return parse(res) })
.then(function(data) {
if (data) {
hash = hash === undefined ? data : hash;
if (hash !== data) {
window.location.reload();
}
}
});
} catch (e) {
if (hadError === false) {
console.log(e);
hadError = true;
}
}
}, 3000);
`.replace(/\t/g, ""));
const router = Router();
if (!Config.isProduction) {
let uuid = v4();
let updateTimeout: Timeout|undefined = undefined;
import("node-watch").then((watch) => {
watch.default('public', {recursive: true}, () => {
if (updateTimeout !== undefined) clearTimeout(updateTimeout);
updateTimeout = setTimeout(() => {
uuid = v4();
}, 200);
});
}).catch((err) => { Logger.error(err); });
reloadRouter.get("/auto-reload", (req, res) => {
req.noLogging = true;
res.json({uuid});
});
}
router.get("/auto-reload/client.js", (req, res) => {
Logger.debug(req.url, req.originalUrl, req.baseUrl);
res.setHeader('Content-Type', "application/javascript");
// language=JavaScript
res.send(`
const loc = window.location;
const url = loc.protocol+'//'+loc.host+'${urlJoin(req.baseUrl, "/auto-reload")}';
// const parse = async res => (await res.json()).uuid;
const parse = function(res) {
return res.json()
.then(function(json) {return json.uuid;}) }
let hash = undefined;
let hadError = false;
setInterval(function() {
try {
fetch(url)
.then(function(res) { return parse(res) })
.then(function(data) {
if (data) {
hash = hash === undefined ? data : hash;
if (hash !== data) {
window.location.reload();
}
}
});
} catch (e) {
if (hadError === false) {
console.log(e);
hadError = true;
}
}
}, 3000);
`.replace(/\t/g, ""));
});
return reloadRouter;
router.get("/auto-reload", (req, res) => {
req.noLogging = true;
res.json({uuid});
});
}
export const AutoReloader = {
router,
};

View File

@@ -16,10 +16,10 @@ const envs = {
// external base url
EXTERNAL_BASE_URL: env.get('EXTERNAL_BASE_URL').asString(),
// url of redis session store
REDIS_URL: env.get('REDIS_URL').required(isProduction).asString() || undefined,
// cookie secret for the session id
SESSION_SECRET: env.get('REDIS_URL').required(isProduction).default('keyboard cat').asString(),
// url of redis session store (required in production using InMemory)
REDIS_URL: env.get('REDIS_URL').asString() || undefined,
// cookie secret for the session id (required in production using Session)
SESSION_SECRET: env.get('SESSION_SECRET').asString() || undefined,
// header where user info token is stored to request auth proxy
USERINFO_HEADER: env.get('USERINFO_HEADER').asString() || undefined,
@@ -29,14 +29,17 @@ const envs = {
AUTH_PROXY_USERINFO_URL: env.get('AUTH_PROXY_USERINFO_URL').asString() || undefined,
// override base url to init a logout
AUTH_PROXY_INIT_LOGOUT_URL: env.get('AUTH_PROXY_INIT_LOGOUT_URL').asString() || undefined,
};
function requireEnv(name: string, onlyInProduction = false): void {
env.get(name).required(!onlyInProduction || isProduction);
}
export const Config = {
...envs,
EXTERNAL_BASE_URL:
envs.EXTERNAL_BASE_URL ||
urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`, envs.BASE_PATH),
isProduction,
EXTERNAL_BASE_URL:
envs.EXTERNAL_BASE_URL ||
urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`, envs.BASE_PATH),
}
requireEnv,
};

View File

@@ -1,7 +1,9 @@
export {Config} from './config';
export {Redis} from './redis';
export {Logger, HttpLogger} from './logging';
export {setupAuthProxy} from './auth-proxy';
export {AuthProxy} from './auth-proxy';
export {Resolvable, WaitForSync} from './helpers/resolvable';
export {getReloadRouter} from './auto-reload';
export {polyfillRoute} from './polyfill';
export {AutoReloader} from './auto-reload';
export {Polyfill} from './polyfill';
export {Session} from './session';
export {Permissions} from './permissions';

85
src/utils/permissions.ts Normal file
View File

@@ -0,0 +1,85 @@
import {AccessControl, AccessControlError, IQueryInfo, Permission, Query} from 'role-acl';
import {Request, RequestHandler} from 'express';
// see https://www.npmjs.com/package/role-acl
class PermissionManager extends AccessControl {
public can(roleOrRequest: Request|string|string[]|IQueryInfo): PermQuery {
return new PermQuery(this.getGrants(), roleOrRequest);
}
public getRouter(resource: string, opts: Partial<RermRouterOpts>): RequestHandler {
return async (req: Request, res, next) => {
let query = this.can(req);
if (opts.context)
query = query.context(opts.context);
if (opts.action)
query = query.execute(opts.action);
if (opts.skipConditions)
query = query.skipConditions(opts.skipConditions);
const permission = await query.on(resource);
if (permission.granted) {
req.permissionDetails = permission;
next();
} else {
res.sendStatus(403);
}
};
}
}
export type RermRouterOpts = {
context: unknown,
action: string,
skipConditions: boolean
}
export class PermQuery extends Query {
protected resolveRequest: Request|undefined;
constructor(grants: unknown, roleOrRequest: Request|string|string[]|IQueryInfo) {
function isRequest(obj: unknown): obj is Request {
// eslint-disable-next-line no-prototype-builtins
return typeof obj === 'object' && obj && obj.hasOwnProperty('path') || false;
}
if (isRequest(roleOrRequest)) {
super(grants, []);
this.resolveRequest = roleOrRequest;
} else {
super(grants, roleOrRequest);
}
}
public async on(resource: string, skipConditions?: boolean): Promise<Permission> {
if (this.resolveRequest) {
const userInfo = await this.resolveRequest.getUserInfo();
this.role(userInfo?.groups ?? []);
}
return super.on(resource, skipConditions);
}
public context(context: unknown): PermQuery {
super.context(context);
return this;
}
public skipConditions(value: boolean): PermQuery {
super.skipConditions(value);
return this;
}
public with(context: unknown): PermQuery {
super.with(context);
return this;
}
public execute(action: string): PermQuery {
super.execute(action);
return this;
}
public sync(): PermQuery {
throw new AccessControlError("Sync method is not allowed on PermissionManager!");
}
}
export const Permissions = new PermissionManager();

View File

@@ -1,8 +1,7 @@
import {RequestHandler} from 'express';
import * as polyfillLibrary from 'polyfill-library';
import {Config} from './config';
import {Config, Logger} from '.';
import {PolyfillFeatureList} from 'polyfill-library';
import {Logger} from './logging';
import {WaitForSync} from './helpers/resolvable';
import {spawn, Thread, Worker} from 'threads';
import {WorkerFunction} from 'threads/dist/types/worker';
@@ -23,7 +22,7 @@ const features = new WaitForSync<PolyfillFeatureList>();
.catch(err => features.setError(err));
export const polyfillRoute: RequestHandler = async (req, res) => {
const router: RequestHandler = async (req, res) => {
const polyfillBundle = await polyfillLibrary.getPolyfillString({
uaString: req.header("user-agent"),
features: await features.resolve(),
@@ -34,3 +33,7 @@ export const polyfillRoute: RequestHandler = async (req, res) => {
res.setHeader('Content-Type', 'text/javascript');
res.send(polyfillBundle);
};
export const Polyfill = {
router: router,
};

View File

@@ -2,6 +2,8 @@ import * as redis from 'redis';
import {Config} from '.';
import {promisify} from 'util';
Config.requireEnv('REDIS_URL', true);
const redisClient = Config.REDIS_URL && redis.createClient({url: Config.REDIS_URL});
const inMemory: {[key: string]: string} = {};

28
src/utils/session.ts Normal file
View File

@@ -0,0 +1,28 @@
import {Store} from 'express-session';
import {Redis, Config} from '.';
import * as redisStore from "connect-redis";
import * as session from 'express-session';
import {RequestHandler} from 'express';
Config.requireEnv('SESSION_SECRET', true);
let sessionStore: Store|undefined = undefined;
function getRouter(options?: Partial<session.SessionOptions>): RequestHandler {
if (Redis.client && sessionStore !== undefined) {
const RedisStore = redisStore(session);
sessionStore = new RedisStore({client: Redis.client});
}
return session({
store: sessionStore,
secret: Config.SESSION_SECRET || 'keyboard cat',
resave: false,
saveUninitialized: true,
cookie: {secure: false},
...options,
});
}
export const Session = {
getRouter,
};

5
src/utils/types/permissions.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace Express {
interface Request {
permissionDetails?: import('role-acl').Permission;
}
}