From 5f20eaba019b3727f3789cef3f918f2abd76fda7 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 2 Jun 2020 15:20:12 -0400 Subject: [PATCH] feat: manage:categories privilege WIP --- public/src/admin/manage/privileges.js | 4 +-- src/middleware/admin.js | 23 ++++++++++++++ src/privileges/admin.js | 46 +++++++++++++++++++++++---- src/routes/index.js | 4 +-- src/socket.io/admin.js | 8 +++++ src/socket.io/admin/categories.js | 1 + 6 files changed, 75 insertions(+), 11 deletions(-) diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js index 23c137e2d1..76db67a687 100644 --- a/public/src/admin/manage/privileges.js +++ b/public/src/admin/manage/privileges.js @@ -145,7 +145,7 @@ define('admin/manage/privileges', [ autocomplete.user(inputEl, function (ev, ui) { var defaultPrivileges; - if (ajaxify.url === '/admin/manage/privileges/admin') { + if (ajaxify.data.url === '/admin/manage/privileges/admin') { defaultPrivileges = ['manage:categories']; } else { defaultPrivileges = cid ? ['find', 'read', 'topics:read'] : ['chat']; @@ -179,7 +179,7 @@ define('admin/manage/privileges', [ autocomplete.group(inputEl, function (ev, ui) { var defaultPrivileges; - if (ajaxify.url === '/admin/manage/privileges/admin') { + if (ajaxify.data.url === '/admin/manage/privileges/admin') { defaultPrivileges = ['groups:manage:categories']; } else { defaultPrivileges = cid ? ['groups:find', 'groups:read', 'groups:topics:read'] : ['groups:chat']; diff --git a/src/middleware/admin.js b/src/middleware/admin.js index bb1e83dc30..7e3d4fe11e 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -9,6 +9,7 @@ var semver = require('semver'); var user = require('../user'); var meta = require('../meta'); var plugins = require('../plugins'); +var privileges = require('../privileges'); var versions = require('../admin/versions'); var controllers = { @@ -126,4 +127,26 @@ module.exports = function (middleware) { middleware.admin.renderFooter = function (req, res, data, next) { req.app.render('admin/footer', data, next); }; + + middleware.admin.checkPrivileges = async (req, res, next) => { + // Kick out guests, obviously + if (!req.uid) { + return controllers.helpers.notAllowed(req, res); + } + + // Users in "administrators" group are considered super admins + const isAdmin = await user.isAdministrator(req.uid); + if (isAdmin) { + return next(); + } + + // Otherwise, check for privilege based on page (if not in mapping, deny access) + const path = req.path.replace(/^(\/api)?\/admin\//g, ''); + const privilege = privileges.admin.resolve(path); + if (!privilege || !await privileges.admin.can(privilege, req.uid)) { + return controllers.helpers.notAllowed(req, res); + } + + return next(); + }; }; diff --git a/src/privileges/admin.js b/src/privileges/admin.js index 5b6238093d..686629f3e1 100644 --- a/src/privileges/admin.js +++ b/src/privileges/admin.js @@ -22,6 +22,41 @@ module.exports = function (privileges) { privileges.admin.groupPrivilegeList = privileges.admin.userPrivilegeList.map(privilege => 'groups:' + privilege); + // Mapping for a page route (via direct match or regexp) to a privilege + privileges.admin.routeMap = { + 'manage/categories': 'manage:categories', + }; + privileges.admin.routeRegexpMap = { + '^manage/categories/\\d+': 'manage:categories', + }; + + // Mapping for socket call methods to a privilege + privileges.admin.socketMap = { + 'admin.categories.getAll': 'manage:categories', + 'admin.categories.create': 'manage:categories', + 'admin.categories.update': 'manage:categories', + 'admin.categories.purge': 'manage:categories', + 'admin.categories.copySettingsFrom': 'manage:categories', + }; + + privileges.admin.resolve = (path) => { + if (privileges.admin.routeMap[path]) { + return privileges.admin.routeMap[path]; + } + + let privilege; + Object.keys(privileges.admin.routeRegexpMap).forEach((regexp) => { + if (!privilege) { + console.log('here', new RegExp(regexp), path); + if (new RegExp(regexp).test(path)) { + privilege = privileges.admin.routeRegexpMap[regexp]; + } + } + }); + + return privilege; + }; + privileges.admin.list = async function () { async function getLabels() { return await utils.promiseParallel({ @@ -61,13 +96,10 @@ module.exports = function (privileges) { // }); // }; - // privileges.admin.can = async function (privilege, uid) { - // const [isAdministrator, isUserAllowedTo] = await Promise.all([ - // user.isAdministrator(uid), - // helpers.isUserAllowedTo(privilege, uid, [0]), - // ]); - // return isAdministrator || isUserAllowedTo[0]; - // }; + privileges.admin.can = async function (privilege, uid) { + const isUserAllowedTo = await helpers.isUserAllowedTo(privilege, uid, [0]); + return isUserAllowedTo[0]; + }; // privileges.admin.canGroup = async function (privilege, groupName) { // return await groups.isMember(groupName, 'cid:0:privileges:groups:' + privilege); diff --git a/src/routes/index.js b/src/routes/index.js index fcab003624..e8a4bc26d6 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -98,8 +98,8 @@ module.exports = async function (app, middleware) { var ensureLoggedIn = require('connect-ensure-login'); router.all('(/+api|/+api/*?)', middleware.prepareAPI); - router.all('(/+api/admin|/+api/admin/*?)', middleware.isAdmin); - router.all('(/+admin|/+admin/*?)', ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login?local=1'), middleware.applyCSRF, middleware.isAdmin); + router.all('(/+api/admin|/+api/admin/*?)', middleware.admin.checkPrivileges); + router.all('(/+admin|/+admin/*?)', ensureLoggedIn.ensureLoggedIn(nconf.get('relative_path') + '/login?local=1'), middleware.applyCSRF, middleware.admin.checkPrivileges); app.use(middleware.stripLeadingSlashes); diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 2c06be484c..c541fba5f6 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -6,6 +6,7 @@ const meta = require('../meta'); const user = require('../user'); const events = require('../events'); const db = require('../database'); +const privileges = require('../privileges'); const websockets = require('./index'); const index = require('./index'); const getAdminSearchDict = require('../admin/search').getDictionary; @@ -37,6 +38,13 @@ SocketAdmin.before = async function (socket, method) { if (isAdmin) { return; } + + // Check admin privileges mapping (if not in mapping, deny access) + const privilege = privileges.admin.socketMap[method]; + if (privilege && await privileges.admin.can(privilege, socket.uid)) { + return; + } + winston.warn('[socket.io] Call to admin method ( ' + method + ' ) blocked (accessed by uid ' + socket.uid + ')'); throw new Error('[[error:no-privileges]]'); }; diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js index 50e61f94bc..2c5c514fc5 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -61,6 +61,7 @@ Categories.setPrivilege = async function (socket, data) { throw new Error('[[error:no-user-or-group]]'); } + console.log('setting', data); await privileges.categories[data.set ? 'give' : 'rescind']( Array.isArray(data.privilege) ? data.privilege : [data.privilege], data.cid, data.member );