Added Permission Middleware, Renaming
This commit is contained in:
40
package-lock.json
generated
40
package-lock.json
generated
@@ -2539,6 +2539,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"jsonpath-plus": {
|
||||
"version": "0.18.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.18.1.tgz",
|
||||
"integrity": "sha512-4yQiuV641HROc4z9YGvnsr8yAdmzbu8JjdAen8WfiXsXewKvTG4ie2bSygF2maek9PcbtROmS6aLQs31BD+oNQ=="
|
||||
},
|
||||
"jsprim": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
|
||||
@@ -2620,6 +2625,16 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"lodash.flattendeep": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
|
||||
"integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI="
|
||||
},
|
||||
"logform": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz",
|
||||
@@ -2673,6 +2688,14 @@
|
||||
"integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=",
|
||||
"dev": true
|
||||
},
|
||||
"matcher": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/matcher/-/matcher-1.1.1.tgz",
|
||||
"integrity": "sha512-+BmqxWIubKTRKNWx/ahnCkk3mG8m7OturVlqq6HiojGJTd5hVYbgZm6WzcYPCoB+KBT4Vd6R7WSRG2OADNaCjg==",
|
||||
"requires": {
|
||||
"escape-string-regexp": "^1.0.4"
|
||||
}
|
||||
},
|
||||
"media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -2923,6 +2946,11 @@
|
||||
"validate-npm-package-license": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"notation": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/notation/-/notation-1.3.6.tgz",
|
||||
"integrity": "sha512-DIuJmrP/Gg1DcXKaApsqcjsJD6jEccqKSfmU3BUx/f1GHsMiTJh70cERwYc64tOmTRTARCeMwkqNNzjh3AHhiw=="
|
||||
},
|
||||
"npmlog": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz",
|
||||
@@ -3598,6 +3626,18 @@
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"role-acl": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/role-acl/-/role-acl-4.5.4.tgz",
|
||||
"integrity": "sha512-T7baCc5BCFzLaxUrtAEIDcfK8HM/WTt6l2SLiMts6zLADSp7wOrrU3op0HVKhwjKi01d0Q6T4NeZ0jdLii9WuQ==",
|
||||
"requires": {
|
||||
"jsonpath-plus": "^0.18.0",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.flattendeep": "^4.4.0",
|
||||
"matcher": "^1.0.0",
|
||||
"notation": "^1.3.5"
|
||||
}
|
||||
},
|
||||
"run-parallel": {
|
||||
"version": "1.1.10",
|
||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"lint-fix": "eslint . --ext .ts --fix",
|
||||
"update-client-hash": "node -e \"require('fs').writeFileSync('public/misc/hash.txt', require('randomstring').generate())\"",
|
||||
"debug-client": "tsc-watch --project ./public/js-source",
|
||||
"debug-server": "tsc-watch --onSuccess \"node --enable-source-maps --use-openssl-ca --unhandled-rejections=strict ./out/index\"",
|
||||
"debug-server": "tsc-watch --project ./src --onSuccess \"node --enable-source-maps --use-openssl-ca --unhandled-rejections=strict ./out/index\"",
|
||||
"debug": "concurrently npm:debug-*",
|
||||
"build": "tsc",
|
||||
"production": "node --use-openssl-ca --unhandled-rejections=strict ./out/index",
|
||||
@@ -31,6 +31,7 @@
|
||||
"proper-url-join": "^2.1.1",
|
||||
"pug": "^3.0.0",
|
||||
"redis": "^3.0.2",
|
||||
"role-acl": "^4.5.4",
|
||||
"threads": "^1.6.3",
|
||||
"tiny-worker": "^2.3.0",
|
||||
"uuid": "^8.3.1",
|
||||
|
||||
@@ -31,6 +31,7 @@ module.exports = {
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"comma-style": "error",
|
||||
"semi": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
|
||||
"no-restricted-imports": ["error",
|
||||
"assert", "buffer", "child_process", "cluster", "crypto", "dgram", "dns", "domain", "events", "freelist",
|
||||
|
||||
@@ -33,6 +33,8 @@ module.exports = {
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"comma-style": "error",
|
||||
"semi": "error",
|
||||
"no-implicit-coercion": "error",
|
||||
|
||||
"promise/always-return": "off",
|
||||
},
|
||||
};
|
||||
|
||||
24
src/app.ts
24
src/app.ts
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
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) {
|
||||
const router = Router();
|
||||
if (!Config.isProduction) {
|
||||
let uuid = v4();
|
||||
let updateTimeout: Timeout|undefined = undefined;
|
||||
import("node-watch").then((watch) => {
|
||||
watch.default('public', {recursive: true}, (evt, name) => {
|
||||
watch.default('public', {recursive: true}, () => {
|
||||
if (updateTimeout !== undefined) clearTimeout(updateTimeout);
|
||||
updateTimeout = setTimeout(() => {
|
||||
uuid = v4();
|
||||
@@ -19,7 +17,7 @@ export function getReloadRouter(): Router {
|
||||
});
|
||||
}).catch((err) => { Logger.error(err); });
|
||||
|
||||
reloadRouter.get("/auto-reload/client.js", (req, res) => {
|
||||
router.get("/auto-reload/client.js", (req, res) => {
|
||||
Logger.debug(req.url, req.originalUrl, req.baseUrl);
|
||||
res.setHeader('Content-Type', "application/javascript");
|
||||
// language=JavaScript
|
||||
@@ -54,11 +52,12 @@ export function getReloadRouter(): Router {
|
||||
`.replace(/\t/g, ""));
|
||||
});
|
||||
|
||||
reloadRouter.get("/auto-reload", (req, res) => {
|
||||
router.get("/auto-reload", (req, res) => {
|
||||
req.noLogging = true;
|
||||
res.json({uuid});
|
||||
});
|
||||
}
|
||||
|
||||
return reloadRouter;
|
||||
}
|
||||
|
||||
export const AutoReloader = {
|
||||
router,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
isProduction,
|
||||
EXTERNAL_BASE_URL:
|
||||
envs.EXTERNAL_BASE_URL ||
|
||||
urlJoin(`http://${envs.HOSTNAME}${envs.PORT !== 80 ? `:${envs.PORT}` : ""}`, envs.BASE_PATH),
|
||||
}
|
||||
isProduction,
|
||||
requireEnv,
|
||||
};
|
||||
|
||||
@@ -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
85
src/utils/permissions.ts
Normal 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();
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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
28
src/utils/session.ts
Normal 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
5
src/utils/types/permissions.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare namespace Express {
|
||||
interface Request {
|
||||
permissionDetails?: import('role-acl').Permission;
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,16 @@
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"sourceMap": true,
|
||||
"outDir": "out",
|
||||
"outDir": "./out",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noImplicitReturns": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
"./src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"public"
|
||||
"./node_modules",
|
||||
"./public"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user