2020-11-18 17:25:56 -05:00
|
|
|
'use strict';
|
2020-10-24 20:47:03 -04:00
|
|
|
|
2020-09-19 15:30:04 -04:00
|
|
|
const assert = require('assert').strict;
|
2020-08-25 17:04:34 -04:00
|
|
|
const log4js = require('log4js');
|
|
|
|
const httpLogger = log4js.getLogger('http');
|
|
|
|
const settings = require('../../utils/Settings');
|
2020-11-18 17:25:56 -05:00
|
|
|
const hooks = require('../../../static/js/pluginfw/hooks');
|
2020-10-01 15:44:24 -04:00
|
|
|
const readOnlyManager = require('../../db/ReadOnlyManager');
|
2012-02-25 13:38:09 +01:00
|
|
|
|
2020-08-27 14:33:58 -04:00
|
|
|
hooks.deprecationNotices.authFailure = 'use the authnFailure and authzFailure hooks instead';
|
|
|
|
|
2021-12-18 16:30:17 -05:00
|
|
|
// Promisified wrapper around hooks.aCallFirst.
|
|
|
|
const aCallFirst = (hookName, context, pred = null) => new Promise((resolve, reject) => {
|
|
|
|
hooks.aCallFirst(hookName, context, (err, r) => err != null ? reject(err) : resolve(r), pred);
|
|
|
|
});
|
|
|
|
|
|
|
|
const aCallFirst0 =
|
|
|
|
async (hookName, context, pred = null) => (await aCallFirst(hookName, context, pred))[0];
|
|
|
|
|
2020-09-11 19:26:26 -04:00
|
|
|
exports.normalizeAuthzLevel = (level) => {
|
|
|
|
if (!level) return false;
|
|
|
|
switch (level) {
|
|
|
|
case true:
|
|
|
|
return 'create';
|
2020-09-19 15:30:04 -04:00
|
|
|
case 'readOnly':
|
2020-09-11 19:46:47 -04:00
|
|
|
case 'modify':
|
2020-09-11 19:26:26 -04:00
|
|
|
case 'create':
|
|
|
|
return level;
|
|
|
|
default:
|
|
|
|
httpLogger.warn(`Unknown authorization level '${level}', denying access`);
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
};
|
|
|
|
|
2020-09-19 15:30:04 -04:00
|
|
|
exports.userCanModify = (padId, req) => {
|
2020-10-01 15:44:24 -04:00
|
|
|
if (readOnlyManager.isReadOnlyId(padId)) return false;
|
2020-09-19 15:30:04 -04:00
|
|
|
if (!settings.requireAuthentication) return true;
|
|
|
|
const {session: {user} = {}} = req;
|
|
|
|
assert(user); // If authn required and user == null, the request should have already been denied.
|
2020-09-28 06:22:06 -04:00
|
|
|
if (user.readOnly) return false;
|
2020-09-19 15:30:04 -04:00
|
|
|
assert(user.padAuthorizations); // This is populated even if !settings.requireAuthorization.
|
|
|
|
const level = exports.normalizeAuthzLevel(user.padAuthorizations[padId]);
|
|
|
|
assert(level); // If !level, the request should have already been denied.
|
|
|
|
return level !== 'readOnly';
|
|
|
|
};
|
|
|
|
|
2020-08-23 16:56:28 -04:00
|
|
|
// Exported so that tests can set this to 0 to avoid unnecessary test slowness.
|
|
|
|
exports.authnFailureDelayMs = 1000;
|
|
|
|
|
2021-12-18 16:30:17 -05:00
|
|
|
const preAuthorize = async (req, res, next) => {
|
|
|
|
const requireAdmin = req.path.toLowerCase().startsWith('/admin');
|
|
|
|
const locals = res.locals._webaccess = {requireAdmin, skip: false};
|
2020-11-18 17:31:18 -05:00
|
|
|
|
2021-12-18 16:30:17 -05:00
|
|
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Step 1: Check the preAuthorize hook for early permit/deny (permit is only allowed for non-admin
|
|
|
|
// pages). If any plugin explicitly grants or denies access, skip the remaining steps. Plugins can
|
|
|
|
// use the preAuthzFailure hook to override the default 403 error.
|
|
|
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
2012-04-19 16:04:03 +02:00
|
|
|
|
2021-12-18 16:30:17 -05:00
|
|
|
let results;
|
|
|
|
const preAuthorizeNext = (...args) => { locals.skip = true; next(...args); };
|
|
|
|
try {
|
|
|
|
results = await aCallFirst('preAuthorize', {req, res, next: preAuthorizeNext},
|
|
|
|
// This predicate will cause aCallFirst to call the hook functions one at a time until one
|
|
|
|
// of them returns a non-empty list, with an exception: If the request is for an /admin
|
|
|
|
// page, truthy entries are filtered out before checking to see whether the list is empty.
|
|
|
|
// This prevents plugin authors from accidentally granting admin privileges to the general
|
|
|
|
// public.
|
|
|
|
(r) => (locals.skip || (r != null && r.filter((x) => (!requireAdmin || !x)).length > 0)));
|
|
|
|
} catch (err) {
|
|
|
|
httpLogger.error(`Error in preAuthorize hook: ${err.stack || err.toString()}`);
|
|
|
|
if (!locals.skip) res.status(500).send('Internal Server Error');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if (locals.skip) return;
|
|
|
|
if (requireAdmin) {
|
|
|
|
// Filter out all 'true' entries to prevent plugin authors from accidentally granting admin
|
|
|
|
// privileges to the general public.
|
|
|
|
results = results.filter((x) => !x);
|
|
|
|
}
|
|
|
|
if (results.length > 0) {
|
|
|
|
// Access was explicitly granted or denied. If any value is false then access is denied.
|
|
|
|
if (!results.every((x) => x)) {
|
|
|
|
// Access explicitly denied.
|
|
|
|
if (await aCallFirst0('preAuthzFailure', {req, res})) return;
|
|
|
|
// No plugin handled the pre-authentication authorization failure.
|
|
|
|
return res.status(403).send('Forbidden');
|
|
|
|
}
|
|
|
|
// Access explicitly granted.
|
|
|
|
locals.skip = true;
|
|
|
|
return next('route');
|
|
|
|
}
|
|
|
|
next();
|
|
|
|
};
|
|
|
|
|
|
|
|
const checkAccess = async (req, res, next) => {
|
|
|
|
const {locals: {_webaccess: {requireAdmin, skip}}} = res;
|
|
|
|
if (skip) return next('route');
|
2020-08-23 16:56:28 -04:00
|
|
|
|
2020-11-18 17:31:18 -05:00
|
|
|
// This helper is used in steps 2 and 4 below, so it may be called twice per access: once before
|
|
|
|
// authentication is checked and once after (if settings.requireAuthorization is true).
|
|
|
|
const authorize = async () => {
|
2021-04-11 03:59:52 +02:00
|
|
|
const grant = async (level) => {
|
2020-09-11 19:26:26 -04:00
|
|
|
level = exports.normalizeAuthzLevel(level);
|
2020-11-18 17:31:18 -05:00
|
|
|
if (!level) return false;
|
2020-09-11 19:26:26 -04:00
|
|
|
const user = req.session.user;
|
2020-11-18 17:31:18 -05:00
|
|
|
if (user == null) return true; // This will happen if authentication is not required.
|
2020-10-01 21:29:38 -04:00
|
|
|
const encodedPadId = (req.path.match(/^\/p\/([^/]*)/) || [])[1];
|
2020-11-18 17:31:18 -05:00
|
|
|
if (encodedPadId == null) return true;
|
2021-04-11 03:59:52 +02:00
|
|
|
let padId = decodeURIComponent(encodedPadId);
|
|
|
|
if (readOnlyManager.isReadOnlyId(padId)) {
|
|
|
|
// pad is read-only, first get the real pad ID
|
|
|
|
padId = await readOnlyManager.getPadId(padId);
|
|
|
|
if (padId == null) return false;
|
|
|
|
}
|
2020-09-11 19:26:26 -04:00
|
|
|
// The user was granted access to a pad. Remember the authorization level in the user's
|
|
|
|
// settings so that SecurityManager can approve or deny specific actions.
|
|
|
|
if (user.padAuthorizations == null) user.padAuthorizations = {};
|
2020-09-24 13:59:07 -04:00
|
|
|
user.padAuthorizations[padId] = level;
|
2020-11-18 17:31:18 -05:00
|
|
|
return true;
|
2020-09-11 19:26:26 -04:00
|
|
|
};
|
2020-08-23 16:56:28 -04:00
|
|
|
const isAuthenticated = req.session && req.session.user;
|
2021-04-11 03:59:52 +02:00
|
|
|
if (isAuthenticated && req.session.user.is_admin) return await grant('create');
|
2020-08-23 16:56:28 -04:00
|
|
|
const requireAuthn = requireAdmin || settings.requireAuthentication;
|
2021-04-11 03:59:52 +02:00
|
|
|
if (!requireAuthn) return await grant('create');
|
|
|
|
if (!isAuthenticated) return await grant(false);
|
|
|
|
if (requireAdmin && !req.session.user.is_admin) return await grant(false);
|
|
|
|
if (!settings.requireAuthorization) return await grant('create');
|
|
|
|
return await grant(await aCallFirst0('authorize', {req, res, next, resource: req.path}));
|
2020-08-25 16:44:44 -04:00
|
|
|
};
|
2012-04-19 14:25:12 +02:00
|
|
|
|
2020-11-18 17:31:18 -05:00
|
|
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Step 2: Try to just access the thing. If access fails (perhaps authentication has not yet
|
|
|
|
// completed, or maybe different credentials are required), go to the next step.
|
|
|
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
if (await authorize()) return next();
|
|
|
|
|
|
|
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Step 3: Authenticate the user. (Or, if already logged in, reauthenticate with different
|
|
|
|
// credentials if supported by the authn scheme.) If authentication fails, give the user a 401
|
|
|
|
// error to request new credentials. Otherwise, go to the next step. Plugins can use the
|
|
|
|
// authnFailure hook to override the default error handling behavior (e.g., to redirect to a login
|
|
|
|
// page).
|
|
|
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
if (settings.users == null) settings.users = {};
|
|
|
|
const ctx = {req, res, users: settings.users, next};
|
|
|
|
// If the HTTP basic auth header is present, extract the username and password so it can be given
|
|
|
|
// to authn plugins.
|
|
|
|
const httpBasicAuth =
|
|
|
|
req.headers.authorization && req.headers.authorization.search('Basic ') === 0;
|
|
|
|
if (httpBasicAuth) {
|
|
|
|
const userpass =
|
|
|
|
Buffer.from(req.headers.authorization.split(' ')[1], 'base64').toString().split(':');
|
|
|
|
ctx.username = userpass.shift();
|
|
|
|
ctx.password = userpass.join(':');
|
|
|
|
}
|
|
|
|
if (!(await aCallFirst0('authenticate', ctx))) {
|
|
|
|
// Fall back to HTTP basic auth.
|
|
|
|
const {[ctx.username]: {password} = {}} = settings.users;
|
|
|
|
if (!httpBasicAuth || password == null || password !== ctx.password) {
|
|
|
|
httpLogger.info(`Failed authentication from IP ${req.ip}`);
|
|
|
|
if (await aCallFirst0('authnFailure', {req, res})) return;
|
|
|
|
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
|
|
|
// No plugin handled the authentication failure. Fall back to basic authentication.
|
|
|
|
res.header('WWW-Authenticate', 'Basic realm="Protected Area"');
|
|
|
|
// Delay the error response for 1s to slow down brute force attacks.
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, exports.authnFailureDelayMs));
|
|
|
|
res.status(401).send('Authentication Required');
|
|
|
|
return;
|
2020-08-28 21:03:45 -04:00
|
|
|
}
|
2020-11-18 17:31:18 -05:00
|
|
|
settings.users[ctx.username].username = ctx.username;
|
|
|
|
// Make a shallow copy so that the password property can be deleted (to prevent it from
|
|
|
|
// appearing in logs or in the database) without breaking future authentication attempts.
|
|
|
|
req.session.user = {...settings.users[ctx.username]};
|
|
|
|
delete req.session.user.password;
|
|
|
|
}
|
|
|
|
if (req.session.user == null) {
|
|
|
|
httpLogger.error('authenticate hook failed to add user settings to session');
|
|
|
|
return res.status(500).send('Internal Server Error');
|
|
|
|
}
|
|
|
|
const {username = '<no username>'} = req.session.user;
|
|
|
|
httpLogger.info(`Successful authentication from IP ${req.ip} for user ${username}`);
|
|
|
|
|
|
|
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Step 4: Try to access the thing again. If this fails, give the user a 403 error. Plugins can
|
|
|
|
// use the authzFailure hook to override the default error handling behavior (e.g., to redirect to
|
|
|
|
// a login page).
|
|
|
|
// ///////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
if (await authorize()) return next();
|
|
|
|
if (await aCallFirst0('authzFailure', {req, res})) return;
|
|
|
|
if (await aCallFirst0('authFailure', {req, res, next})) return;
|
|
|
|
// No plugin handled the authorization failure.
|
|
|
|
res.status(403).send('Forbidden');
|
2020-08-25 16:44:44 -04:00
|
|
|
};
|
2012-02-25 13:38:09 +01:00
|
|
|
|
2021-12-18 16:30:17 -05:00
|
|
|
/**
|
|
|
|
* Express middleware that allows plugins to explicitly grant/deny access via the `preAuthorize`
|
|
|
|
* hook before `checkAccess` is run. If access is explicitly granted:
|
|
|
|
* - `next('route')` will be called, which can be used to bypass later checks
|
2021-12-18 17:00:02 -05:00
|
|
|
* - `nextRouteIfPreAuthorized` will simply call `next('route')`
|
2021-12-18 16:30:17 -05:00
|
|
|
* - `checkAccess` will simply call `next('route')`
|
|
|
|
*/
|
|
|
|
exports.preAuthorize = (req, res, next) => {
|
|
|
|
preAuthorize(req, res, next).catch((err) => next(err || new Error(err)));
|
|
|
|
};
|
|
|
|
|
2021-12-18 17:00:02 -05:00
|
|
|
/**
|
|
|
|
* Express middleware that simply calls `next('route')` if the request has been explicitly granted
|
|
|
|
* access by `preAuthorize` (otherwise it calls `next()`). This can be used to bypass later checks.
|
|
|
|
*/
|
|
|
|
exports.nextRouteIfPreAuthorized = (req, res, next) => {
|
|
|
|
if (res.locals._webaccess.skip) return next('route');
|
|
|
|
next();
|
|
|
|
};
|
|
|
|
|
2021-12-18 01:05:31 -05:00
|
|
|
/**
|
|
|
|
* Express middleware to authenticate the user and check authorization. Must be installed after the
|
2021-12-18 16:30:17 -05:00
|
|
|
* express-session middleware. If the request is pre-authorized, this middleware simply calls
|
|
|
|
* `next('route')`.
|
2021-12-18 01:05:31 -05:00
|
|
|
*/
|
|
|
|
exports.checkAccess = (req, res, next) => {
|
|
|
|
checkAccess(req, res, next).catch((err) => next(err || new Error(err)));
|
2020-08-25 16:44:44 -04:00
|
|
|
};
|