From a82e9bd7f66faf5302f1320074a557375dd57d43 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 5 Jun 2020 15:26:51 -0400 Subject: [PATCH] feat: privileges for Admin Control Panel (#8355) * feat: acp privileges (WIP) * fix: restore global privilege hooks * refactor: using cid 0 in admin privs * fix: no need for zebrastripe-reset * feat: manage:categories privilege WIP * feat: renamed prefix to admin:, settigns and dashboard privs * fix: nofocus on acp privs group find modal * refactor: privileges.x.get() to not used hardcoded privs * fix: crash if unable to get latest version * feat: setting acp priv * Revert "fix: crash if unable to get latest version" This reverts commit afdb235f48eb0072d88de45f3a1e0151281095b3. * feat: user/privilege acp privs * fix: category selector in manage/privileges * fix: guests potentially becoming admins * fix: bug in setting admin privs * fix: some last minute things + api docs * fix: some more last minute fixes --- .../en-GB/admin/manage/privileges.json | 9 +- public/openapi/read.yaml | 2 + public/src/admin/manage/privileges.js | 34 +++- public/src/admin/modules/search.js | 4 + public/src/modules/helpers.js | 2 +- src/controllers/admin.js | 20 ++ src/controllers/admin/dashboard.js | 4 +- src/controllers/admin/privileges.js | 19 +- src/middleware/admin.js | 25 +++ src/privileges/admin.js | 183 ++++++++++++++++++ src/privileges/categories.js | 8 +- src/privileges/global.js | 18 +- src/privileges/index.js | 1 + src/routes/admin.js | 2 +- src/routes/index.js | 4 +- src/socket.io/admin.js | 8 + src/socket.io/admin/categories.js | 4 +- src/views/admin/dashboard.tpl | 2 + src/views/admin/manage/privileges.tpl | 10 +- src/views/admin/partials/menu.tpl | 33 +++- .../category.tpl} | 0 .../privileges.tpl => privileges/global.tpl} | 3 - .../admin/partials/quick_actions/buttons.tpl | 3 + 23 files changed, 344 insertions(+), 54 deletions(-) create mode 100644 src/privileges/admin.js rename src/views/admin/partials/{categories/privileges.tpl => privileges/category.tpl} (100%) rename src/views/admin/partials/{global/privileges.tpl => privileges/global.tpl} (96%) diff --git a/public/language/en-GB/admin/manage/privileges.json b/public/language/en-GB/admin/manage/privileges.json index faabb1a7b4..d85d1492b9 100644 --- a/public/language/en-GB/admin/manage/privileges.json +++ b/public/language/en-GB/admin/manage/privileges.json @@ -1,6 +1,7 @@ { "global": "Global", "global.no-users": "No user-specific global privileges.", + "admin": "Admin", "group-privileges": "Group Privileges", "user-privileges": "User Privileges", "chat": "Chat", @@ -31,5 +32,11 @@ "downvote-posts": "Downvote Posts", "delete-topics": "Delete Topics", "purge": "Purge", - "moderate": "Moderate" + "moderate": "Moderate", + + "admin-dashboard": "Dashboard", + "admin-categories": "Categories", + "admin-privileges": "Privileges", + "admin-users": "Users", + "admin-settings": "Settings" } \ No newline at end of file diff --git a/public/openapi/read.yaml b/public/openapi/read.yaml index 1baef2861d..6646149b46 100644 --- a/public/openapi/read.yaml +++ b/public/openapi/read.yaml @@ -354,6 +354,8 @@ paths: timestampISO: type: string description: An ISO 8601 formatted date string (complementing `timestamp`) + showSystemControls: + type: boolean - $ref: components/schemas/CommonProps.yaml#/CommonProps /api/admin/settings/languages: get: diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js index 91813b4626..553e4427e9 100644 --- a/public/src/admin/manage/privileges.js +++ b/public/src/admin/manage/privileges.js @@ -11,12 +11,15 @@ define('admin/manage/privileges', [ var cid; Privileges.init = function () { - cid = ajaxify.data.cid || 0; + cid = ajaxify.data.cid || 'admin'; categorySelector.init($('[component="category-selector"]'), function (category) { - var cid = parseInt(category.cid, 10); - ajaxify.go('admin/manage/privileges/' + (cid || '')); + cid = parseInt(category.cid, 10); + cid = isNaN(cid) ? 'admin' : cid; + Privileges.refreshPrivilegeTable(); + ajaxify.updateHistory('admin/manage/privileges/' + (cid || '')); }); + Privileges.setupPrivilegeTable(); }; @@ -81,7 +84,8 @@ define('admin/manage/privileges', [ if (err) { return app.alertError(err.message); } - var tpl = cid ? 'admin/partials/categories/privileges' : 'admin/partials/global/privileges'; + + var tpl = parseInt(cid, 10) ? 'admin/partials/privileges/category' : 'admin/partials/privileges/global'; Benchpress.parse(tpl, { privileges: privileges, }, function (html) { @@ -117,7 +121,7 @@ define('admin/manage/privileges', [ Privileges.setPrivilege = function (member, privilege, state, checkboxEl) { socket.emit('admin.categories.setPrivilege', { - cid: cid, + cid: isNaN(cid) ? 0 : cid, privilege: privilege, set: state, member: member, @@ -143,9 +147,14 @@ define('admin/manage/privileges', [ inputEl.focus(); autocomplete.user(inputEl, function (ev, ui) { - var defaultPrivileges = cid ? ['find', 'read', 'topics:read'] : ['chat']; + var defaultPrivileges; + if (ajaxify.data.url === '/admin/manage/privileges/admin') { + defaultPrivileges = ['admin:dashboard']; + } else { + defaultPrivileges = cid ? ['find', 'read', 'topics:read'] : ['chat']; + } socket.emit('admin.categories.setPrivilege', { - cid: cid, + cid: isNaN(cid) ? 0 : cid, privilege: defaultPrivileges, set: true, member: ui.item.user.uid, @@ -170,11 +179,18 @@ define('admin/manage/privileges', [ modal.on('shown.bs.modal', function () { var inputEl = modal.find('input'); + inputEl.focus(); autocomplete.group(inputEl, function (ev, ui) { - var defaultPrivileges = cid ? ['groups:find', 'groups:read', 'groups:topics:read'] : ['groups:chat']; + var defaultPrivileges; + if (ajaxify.data.url === '/admin/manage/privileges/admin') { + defaultPrivileges = ['groups:admin:dashboard']; + } else { + defaultPrivileges = cid ? ['groups:find', 'groups:read', 'groups:topics:read'] : ['groups:chat']; + } + socket.emit('admin.categories.setPrivilege', { - cid: cid, + cid: isNaN(cid) ? 0 : cid, privilege: defaultPrivileges, set: true, member: ui.item.group.name, diff --git a/public/src/admin/modules/search.js b/public/src/admin/modules/search.js index 77aafe7bfd..274d7a14b2 100644 --- a/public/src/admin/modules/search.js +++ b/public/src/admin/modules/search.js @@ -46,6 +46,10 @@ define('admin/modules/search', ['mousetrap'], function (mousetrap) { } search.init = function () { + if (!app.user.privileges['admin:settings']) { + return; + } + socket.emit('admin.getSearchDict', {}, function (err, dict) { if (err) { app.alertError(err); diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index 485dd1a5bb..6592a1418b 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -188,7 +188,7 @@ var spidersEnabled = ['groups:find', 'groups:read', 'groups:topics:read', 'groups:view:users', 'groups:view:tags', 'groups:view:groups']; var globalModDisabled = ['groups:moderate']; var disabled = - (member === 'guests' && guestDisabled.includes(priv.name)) || + (member === 'guests' && (guestDisabled.includes(priv.name) || priv.name.startsWith('groups:admin:'))) || (member === 'spiders' && !spidersEnabled.includes(priv.name)) || (member === 'Global Moderators' && globalModDisabled.includes(priv.name)); diff --git a/src/controllers/admin.js b/src/controllers/admin.js index 02e61463f5..b6412828a5 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -1,5 +1,8 @@ 'use strict'; +const privileges = require('../privileges'); +const helpers = require('./helpers'); + var adminController = { dashboard: require('./admin/dashboard'), categories: require('./admin/categories'), @@ -28,5 +31,22 @@ var adminController = { info: require('./admin/info'), }; +adminController.routeIndex = async (req, res) => { + const privilegeSet = await privileges.admin.get(req.uid); + + if (privilegeSet.superadmin || privilegeSet['admin:dashboard']) { + return adminController.dashboard.get(req, res); + } else if (privilegeSet['admin:categories']) { + return helpers.redirect(res, 'admin/manage/categories'); + } else if (privilegeSet['admin:privileges']) { + return helpers.redirect(res, 'admin/manage/privileges'); + } else if (privilegeSet['admin:users']) { + return helpers.redirect(res, 'admin/manage/users'); + } else if (privilegeSet['admin:settings']) { + return helpers.redirect(res, 'admin/settings/general'); + } + + return helpers.notAllowed(req, res); +}; module.exports = adminController; diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js index 04a0b78949..73c50e468f 100644 --- a/src/controllers/admin/dashboard.js +++ b/src/controllers/admin/dashboard.js @@ -16,11 +16,12 @@ const utils = require('../../utils'); const dashboardController = module.exports; dashboardController.get = async function (req, res) { - const [stats, notices, latestVersion, lastrestart] = await Promise.all([ + const [stats, notices, latestVersion, lastrestart, isAdmin] = await Promise.all([ getStats(), getNotices(), getLatestVersion(), getLastRestart(), + user.isAdministrator(), ]); const version = nconf.get('version'); @@ -34,6 +35,7 @@ dashboardController.get = async function (req, res) { stats: stats, canRestart: !!process.send, lastrestart: lastrestart, + showSystemControls: isAdmin, }); }; diff --git a/src/controllers/admin/privileges.js b/src/controllers/admin/privileges.js index 687ae3864a..d15a0400ed 100644 --- a/src/controllers/admin/privileges.js +++ b/src/controllers/admin/privileges.js @@ -6,9 +6,18 @@ const privileges = require('../../privileges'); const privilegesController = module.exports; privilegesController.get = async function (req, res) { - const cid = req.params.cid ? parseInt(req.params.cid, 10) : 0; + const cid = req.params.cid ? parseInt(req.params.cid, 10) || 0 : 0; + const isAdminPriv = req.params.cid === 'admin'; + + let method; + if (cid > 0) { + method = privileges.categories.list.bind(null, cid); + } else if (cid === 0) { + method = isAdminPriv ? privileges.admin.list : privileges.global.list; + } + const [privilegesData, categoriesData] = await Promise.all([ - cid ? privileges.categories.list(cid) : privileges.global.list(), + method(), categories.buildForSelectAll(), ]); @@ -16,12 +25,16 @@ privilegesController.get = async function (req, res) { cid: 0, name: '[[admin/manage/privileges:global]]', icon: 'fa-list', + }, { + cid: 'admin', // what do? + name: '[[admin/manage/privileges:admin]]', + icon: 'fa-lock', }); let selectedCategory; categoriesData.forEach(function (category) { if (category) { - category.selected = category.cid === cid; + category.selected = category.cid === (!isAdminPriv ? cid : 'admin'); if (category.selected) { selectedCategory = category; diff --git a/src/middleware/admin.js b/src/middleware/admin.js index 33e8a45f49..945b2c17f2 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -8,6 +8,7 @@ var semver = require('semver'); var user = require('../user'); var meta = require('../meta'); var plugins = require('../plugins'); +var privileges = require('../privileges'); var utils = require('../../public/src/utils'); var versions = require('../admin/versions'); var helpers = require('./helpers'); @@ -43,11 +44,13 @@ module.exports = function (middleware) { custom_header: plugins.fireHook('filter:admin.header.build', custom_header), configs: meta.configs.list(), latestVersion: getLatestVersion(), + privileges: privileges.admin.get(req.uid), }); var userData = results.userData; userData.uid = req.uid; userData['email:confirmed'] = userData['email:confirmed'] === 1; + userData.privileges = results.privileges; var acpPath = req.path.slice(1).split('/'); acpPath.forEach(function (path, i) { @@ -103,4 +106,26 @@ module.exports = function (middleware) { middleware.admin.renderFooter = async function (req, res, data) { return await req.app.renderAsync('admin/footer', data); }; + + 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 new file mode 100644 index 0000000000..5c654b0a39 --- /dev/null +++ b/src/privileges/admin.js @@ -0,0 +1,183 @@ + +'use strict'; + +const _ = require('lodash'); + +const user = require('../user'); +const groups = require('../groups'); +const helpers = require('./helpers'); +const plugins = require('../plugins'); +const utils = require('../utils'); + +module.exports = function (privileges) { + privileges.admin = {}; + + privileges.admin.privilegeLabels = [ + { name: '[[admin/manage/privileges:admin-dashboard]]' }, + { name: '[[admin/manage/privileges:admin-categories]]' }, + { name: '[[admin/manage/privileges:admin-privileges]]' }, + { name: '[[admin/manage/privileges:admin-users]]' }, + { name: '[[admin/manage/privileges:admin-settings]]' }, + ]; + + privileges.admin.userPrivilegeList = [ + 'admin:dashboard', + 'admin:categories', + 'admin:privileges', + 'admin:users', + 'admin:settings', + ]; + + 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 = { + dashboard: 'admin:dashboard', + 'manage/categories': 'admin:categories', + 'manage/privileges': 'admin:privileges', + 'manage/users': 'admin:users', + 'extend/plugins': 'admin:settings', + 'extend/widgets': 'admin:settings', + 'extend/rewards': 'admin:settings', + }; + privileges.admin.routeRegexpMap = { + '^manage/categories/\\d+': 'admin:categories', + '^manage/privileges/\\d+': 'admin:privileges', + '^settings/[\\w\\-]+$': 'admin:settings', + '^appearance/[\\w]+$': 'admin:settings', + '^plugins/[\\w\\-]+$': 'admin:settings', + }; + + // Mapping for socket call methods to a privilege + // In NodeBB v2, these socket calls will be removed in favour of xhr calls + privileges.admin.socketMap = { + 'admin.rooms.getAll': 'admin:dashboard', + 'admin.analytics.get': 'admin:dashboard', + + 'admin.categories.getAll': 'admin:categories', + 'admin.categories.create': 'admin:categories', + 'admin.categories.update': 'admin:categories', + 'admin.categories.purge': 'admin:categories', + 'admin.categories.copySettingsFrom': 'admin:categories', + + 'admin.categories.getPrivilegeSettings': 'admin:privileges', + 'admin.categories.setPrivilege': 'admin:privileges', + 'admin.categories.copyPrivilegesToChildren': 'admin:privileges', + 'admin.categories.copyPrivilegesFrom': 'admin:privileges', + 'admin.categories.copyPrivilegesToAllCategories': 'admin:privileges', + + 'admin.user.loadGroups': 'admin:users', + 'admin.groups.join': 'admin:users', + 'admin.groups.leave': 'admin:users', + 'user.banUsers': 'admin:users', + 'user.unbanUsers': 'admin:users', + 'admin.user.resetLockouts': 'admin:users', + 'admin.user.validateEmail': 'admin:users', + 'admin.user.sendValidationEmail': 'admin:users', + 'admin.user.sendPasswordResetEmail': 'admin:users', + 'admin.user.forcePasswordReset': 'admin:users', + 'admin.user.deleteUsers': 'admin:users', + 'admin.user.deleteUsersAndContent': 'admin:users', + 'admin.user.createUser': 'admin:users', + 'admin.user.search': 'admin:users', + 'admin.user.invite': 'admin:users', + + 'admin.getSearchDict': 'admin:settings', + 'admin.config.setMultiple': 'admin:settings', + 'admin.config.remove': 'admin:settings', + 'admin.themes.getInstalled': 'admin:settings', + 'admin.themes.set': 'admin:settings', + 'admin.reloadAllSessions': 'admin:settings', + 'admin.settings.get': 'admin:settings', + }; + + privileges.admin.resolve = (path) => { + if (privileges.admin.routeMap[path]) { + return privileges.admin.routeMap[path]; + } else if (path === '') { + return 'manage:dashboard'; + } + + let privilege; + Object.keys(privileges.admin.routeRegexpMap).forEach((regexp) => { + if (!privilege) { + 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({ + users: plugins.fireHook('filter:privileges.admin.list_human', privileges.admin.privilegeLabels.slice()), + groups: plugins.fireHook('filter:privileges.admin.groups.list_human', privileges.admin.privilegeLabels.slice()), + }); + } + const payload = await utils.promiseParallel({ + labels: getLabels(), + users: helpers.getUserPrivileges(0, 'filter:privileges.admin.list', privileges.admin.userPrivilegeList), + groups: helpers.getGroupPrivileges(0, 'filter:privileges.admin.groups.list', privileges.admin.groupPrivilegeList), + }); + // This is a hack because I can't do {labels.users.length} to echo the count in templates.js + payload.columnCount = payload.labels.users.length + 2; + return payload; + }; + + privileges.admin.get = async function (uid) { + const [userPrivileges, isAdministrator] = await Promise.all([ + helpers.isUserAllowedTo(privileges.admin.userPrivilegeList, uid, 0), + user.isAdministrator(uid), + ]); + + const combined = userPrivileges.map(allowed => allowed || isAdministrator); + const privData = _.zipObject(privileges.admin.userPrivilegeList, combined); + + privData.superadmin = isAdministrator; + return await plugins.fireHook('filter:privileges.admin.get', privData); + }; + + 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); + // }; + + privileges.admin.give = async function (privileges, groupName) { + await helpers.giveOrRescind(groups.join, privileges, 'admin', groupName); + plugins.fireHook('action:privileges.admin.give', { + privileges: privileges, + groupNames: Array.isArray(groupName) ? groupName : [groupName], + }); + }; + + privileges.admin.rescind = async function (privileges, groupName) { + await helpers.giveOrRescind(groups.leave, privileges, 'admin', groupName); + plugins.fireHook('action:privileges.admin.rescind', { + privileges: privileges, + groupNames: Array.isArray(groupName) ? groupName : [groupName], + }); + }; + + // privileges.admin.userPrivileges = async function (uid) { + // const tasks = {}; + // privileges.admin.userPrivilegeList.forEach(function (privilege) { + // tasks[privilege] = groups.isMember(uid, 'cid:0:privileges:' + privilege); + // }); + // return await utils.promiseParallel(tasks); + // }; + + // privileges.admin.groupPrivileges = async function (groupName) { + // const tasks = {}; + // privileges.admin.groupPrivilegeList.forEach(function (privilege) { + // tasks[privilege] = groups.isMember(groupName, 'cid:0:privileges:' + privilege); + // }); + // return await utils.promiseParallel(tasks); + // }; +}; diff --git a/src/privileges/categories.js b/src/privileges/categories.js index 8fa6165dac..868ea065fc 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -45,14 +45,12 @@ module.exports = function (privileges) { user.isModerator(uid, cid), ]); - const privData = _.zipObject(privs, userPrivileges); + const combined = userPrivileges.map(allowed => allowed || isAdministrator); + const privData = _.zipObject(privs, combined); const isAdminOrMod = isAdministrator || isModerator; return await plugins.fireHook('filter:privileges.categories.get', { - 'topics:create': privData['topics:create'] || isAdministrator, - 'topics:read': privData['topics:read'] || isAdministrator, - 'topics:tag': privData['topics:tag'] || isAdministrator, - read: privData.read || isAdministrator, + ...privData, cid: cid, uid: uid, editable: isAdminOrMod, diff --git a/src/privileges/global.js b/src/privileges/global.js index 6738ff09c3..7b851e1c9a 100644 --- a/src/privileges/global.js +++ b/src/privileges/global.js @@ -71,20 +71,10 @@ module.exports = function (privileges) { user.isAdministrator(uid), ]); - const privData = _.zipObject(privileges.global.userPrivilegeList, userPrivileges); - - return await plugins.fireHook('filter:privileges.global.get', { - chat: privData.chat || isAdministrator, - 'upload:post:image': privData['upload:post:image'] || isAdministrator, - 'upload:post:file': privData['upload:post:file'] || isAdministrator, - 'search:content': privData['search:content'] || isAdministrator, - 'search:users': privData['search:users'] || isAdministrator, - 'search:tags': privData['search:tags'] || isAdministrator, - 'view:users': privData['view:users'] || isAdministrator, - 'view:tags': privData['view:tags'] || isAdministrator, - 'view:groups': privData['view:groups'] || isAdministrator, - 'view:users:info': privData['view:users:info'] || isAdministrator, - }); + const combined = userPrivileges.map(allowed => allowed || isAdministrator); + const privData = _.zipObject(privileges.global.userPrivilegeList, combined); + + return await plugins.fireHook('filter:privileges.global.get', privData); }; privileges.global.can = async function (privilege, uid) { diff --git a/src/privileges/index.js b/src/privileges/index.js index 1a17f59241..b1cb6c8e76 100644 --- a/src/privileges/index.js +++ b/src/privileges/index.js @@ -43,6 +43,7 @@ privileges.groupPrivilegeList = privileges.userPrivilegeList.map(privilege => 'g privileges.privilegeList = privileges.userPrivilegeList.concat(privileges.groupPrivilegeList); require('./global')(privileges); +require('./admin')(privileges); require('./categories')(privileges); require('./topics')(privileges); require('./posts')(privileges); diff --git a/src/routes/admin.js b/src/routes/admin.js index 087911abf1..708a66a485 100644 --- a/src/routes/admin.js +++ b/src/routes/admin.js @@ -5,7 +5,7 @@ const helpers = require('./helpers'); module.exports = function (app, middleware, controllers) { const middlewares = [middleware.pluginHooks]; - helpers.setupAdminPageRoute(app, '/admin', middleware, middlewares, controllers.admin.dashboard.get); + helpers.setupAdminPageRoute(app, '/admin', middleware, middlewares, controllers.admin.routeIndex); helpers.setupAdminPageRoute(app, '/admin/dashboard', middleware, middlewares, controllers.admin.dashboard.get); diff --git a/src/routes/index.js b/src/routes/index.js index 2274a7f418..fca5527047 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 ee2c5dd52b..5848f106a4 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 c57eebc159..50e61f94bc 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -77,7 +77,9 @@ Categories.setPrivilege = async function (socket, data) { }; Categories.getPrivilegeSettings = async function (socket, cid) { - if (!parseInt(cid, 10)) { + if (cid === 'admin') { + return await privileges.admin.list(); + } else if (!parseInt(cid, 10)) { return await privileges.global.list(); } return await privileges.categories.list(cid); diff --git a/src/views/admin/dashboard.tpl b/src/views/admin/dashboard.tpl index 781d4ef782..36180b2e2c 100644 --- a/src/views/admin/dashboard.tpl +++ b/src/views/admin/dashboard.tpl @@ -124,6 +124,7 @@
+ {{{ if showSystemControls }}}
[[admin/dashboard:control-panel]]
@@ -152,6 +153,7 @@ [[admin/dashboard:realtime-chart-updates]] OFF
+ {{{ end }}}
[[admin/dashboard:active-users]]
diff --git a/src/views/admin/manage/privileges.tpl b/src/views/admin/manage/privileges.tpl index 527dd86607..9930a240cb 100644 --- a/src/views/admin/manage/privileges.tpl +++ b/src/views/admin/manage/privileges.tpl @@ -11,11 +11,11 @@
- - - - - + {{{ if cid }}} + + {{{ else }}} + + {{{ endif }}}
diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index 653a0ef9a8..36f24f6a59 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -12,9 +12,10 @@ + {{{ if user.privileges.admin:settings }}} - + {{{ end }}} + {{{ if user.privileges.superadmin }}} + {{{ end }}}
@@ -127,6 +132,7 @@ \ No newline at end of file diff --git a/src/views/admin/partials/categories/privileges.tpl b/src/views/admin/partials/privileges/category.tpl similarity index 100% rename from src/views/admin/partials/categories/privileges.tpl rename to src/views/admin/partials/privileges/category.tpl diff --git a/src/views/admin/partials/global/privileges.tpl b/src/views/admin/partials/privileges/global.tpl similarity index 96% rename from src/views/admin/partials/global/privileges.tpl rename to src/views/admin/partials/privileges/global.tpl index 7c4fc0e5fa..314beeebe7 100644 --- a/src/views/admin/partials/global/privileges.tpl +++ b/src/views/admin/partials/privileges/global.tpl @@ -1,9 +1,6 @@ - - - diff --git a/src/views/admin/partials/quick_actions/buttons.tpl b/src/views/admin/partials/quick_actions/buttons.tpl index 950283d69e..eed00da8f6 100644 --- a/src/views/admin/partials/quick_actions/buttons.tpl +++ b/src/views/admin/partials/quick_actions/buttons.tpl @@ -3,6 +3,8 @@ + +{{{ if user.privileges.superadmin }}}
  • @@ -13,6 +15,7 @@
  • +{{{ end }}}
  • [[admin/manage/categories:privileges.section-group]]