From c1b3079d93fb4c49ba62a4be5279b7bff8e5a54d Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 4 Mar 2021 12:46:31 -0500 Subject: [PATCH] feat: category privilege API routes closes #9342 --- public/openapi/write.yaml | 4 + .../write/categories/cid/privileges.yaml | 62 +++++++ .../categories/cid/privileges/privilege.yaml | 158 ++++++++++++++++++ public/src/admin/manage/privileges.js | 53 +++--- src/api/categories.js | 49 ++++++ src/controllers/write/categories.js | 24 +++ src/routes/write/categories.js | 4 + src/socket.io/admin/categories.js | 39 +---- .../admin/partials/privileges/category.tpl | 4 +- .../admin/partials/privileges/global.tpl | 4 +- 10 files changed, 337 insertions(+), 64 deletions(-) create mode 100644 public/openapi/write/categories/cid/privileges.yaml create mode 100644 public/openapi/write/categories/cid/privileges/privilege.yaml diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 274ef5ea76..b7d1363e1d 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -82,6 +82,10 @@ paths: $ref: 'write/categories.yaml' /categories/{cid}: $ref: 'write/categories/cid.yaml' + /categories/{cid}/privileges: + $ref: 'write/categories/cid/privileges.yaml' + /categories/{cid}/privileges/{privilege}: + $ref: 'write/categories/cid/privileges/privilege.yaml' /topics/: $ref: 'write/topics.yaml' /topics/{tid}: diff --git a/public/openapi/write/categories/cid/privileges.yaml b/public/openapi/write/categories/cid/privileges.yaml new file mode 100644 index 0000000000..1215622871 --- /dev/null +++ b/public/openapi/write/categories/cid/privileges.yaml @@ -0,0 +1,62 @@ +get: + tags: + - categories + summary: get a category's privilege set + description: This operation retrieves a category's privilege set. + parameters: + - in: path + name: cid + schema: + type: string + required: true + description: a valid category id, `0` for global privileges, `admin` for admin privileges + example: 1 + responses: + '200': + description: Category privileges successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + users: + type: array + items: + type: object + properties: + name: + type: string + nameEscaped: + type: string + privileges: + type: object + additionalProperties: + type: boolean + description: A set of privileges with either true or false + isPrivate: + type: boolean + isSystem: + type: boolean + groups: + type: array + items: + type: object + properties: + name: + type: string + nameEscaped: + type: string + privileges: + type: object + additionalProperties: + type: boolean + description: A set of privileges with either true or false + isPrivate: + type: boolean + isSystem: + type: boolean \ No newline at end of file diff --git a/public/openapi/write/categories/cid/privileges/privilege.yaml b/public/openapi/write/categories/cid/privileges/privilege.yaml new file mode 100644 index 0000000000..70dfe6ccbc --- /dev/null +++ b/public/openapi/write/categories/cid/privileges/privilege.yaml @@ -0,0 +1,158 @@ +put: + tags: + - categories + summary: Grant category privilege for user/group + description: This operation grants a category privilege for a specific user or group + parameters: + - in: path + name: cid + schema: + type: string + required: true + description: a valid category id, `0` for global privileges, `admin` for admin privileges + example: 1 + - in: path + name: privilege + schema: + type: string + required: true + description: The specific privilege you would like to grant. Privileges for groups must be prefixed `group:` + example: 'groups:ban' + requestBody: + content: + application/json: + schema: + type: object + properties: + member: + type: string + description: A valid user id or group name + example: 'guests' + responses: + '200': + description: Privilege successfully granted + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + users: + type: array + items: + type: object + properties: + name: + type: string + nameEscaped: + type: string + privileges: + type: object + additionalProperties: + type: boolean + description: A set of privileges with either true or false + isPrivate: + type: boolean + isSystem: + type: boolean + groups: + type: array + items: + type: object + properties: + name: + type: string + nameEscaped: + type: string + privileges: + type: object + additionalProperties: + type: boolean + description: A set of privileges with either true or false + isPrivate: + type: boolean + isSystem: + type: boolean +delete: + tags: + - categories + summary: Resvinds category privilege for user/group + description: This operation rescinds a category privilege for a specific user or group + parameters: + - in: path + name: cid + schema: + type: string + required: true + description: a valid category id, `0` for global privileges, `admin` for admin privileges + example: 1 + - in: path + name: privilege + schema: + type: string + required: true + description: The specific privilege you would like to rescind. Privileges for groups must be prefixed `group:` + example: 'groups:ban' + requestBody: + content: + application/json: + schema: + type: object + properties: + member: + type: string + description: A valid user id or group name + example: 'guests' + responses: + '200': + description: Privilege successfully rescinded + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + users: + type: array + items: + type: object + properties: + name: + type: string + nameEscaped: + type: string + privileges: + type: object + additionalProperties: + type: boolean + description: A set of privileges with either true or false + isPrivate: + type: boolean + isSystem: + type: boolean + groups: + type: array + items: + type: object + properties: + name: + type: string + nameEscaped: + type: string + privileges: + type: object + additionalProperties: + type: boolean + description: A set of privileges with either true or false + isPrivate: + type: boolean + isSystem: + type: boolean \ No newline at end of file diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js index 281e06c56d..3508240ac6 100644 --- a/public/src/admin/manage/privileges.js +++ b/public/src/admin/manage/privileges.js @@ -1,13 +1,14 @@ 'use strict'; define('admin/manage/privileges', [ + 'api', 'autocomplete', 'bootbox', 'translator', 'categorySelector', 'mousetrap', 'admin/modules/checkboxRowSelector', -], function (autocomplete, bootbox, translator, categorySelector, mousetrap, checkboxRowSelector) { +], function (api, autocomplete, bootbox, translator, categorySelector, mousetrap, checkboxRowSelector) { var Privileges = {}; var cid; @@ -141,9 +142,17 @@ define('admin/manage/privileges', [ return Privileges.setPrivilege(member, privilege, state); }); - Promise.allSettled(requests).then(function () { + Promise.allSettled(requests).then((results) => { Privileges.refreshPrivilegeTable(); - app.alertSuccess('[[admin/manage/privileges:alert.saved]]'); + + const rejects = results.filter(r => r.status === 'rejected'); + if (rejects.length) { + rejects.forEach((result) => { + app.alertError(result.reason); + }); + } else { + app.alertSuccess('[[admin/manage/privileges:alert.saved]]'); + } }); }; @@ -153,23 +162,21 @@ define('admin/manage/privileges', [ }; Privileges.refreshPrivilegeTable = function (groupToHighlight) { - socket.emit('admin.categories.getPrivilegeSettings', cid, function (err, privileges) { - if (err) { - return app.alertError(err.message); - } - - ajaxify.data.privileges = privileges; + api.get(`/categories/${cid}/privileges`, {}).then((privileges) => { + ajaxify.data.privileges = { ...ajaxify.data.privileges, ...privileges }; var tpl = parseInt(cid, 10) ? 'admin/partials/privileges/category' : 'admin/partials/privileges/global'; - app.parseAndTranslate(tpl, { - privileges: privileges, - }, function (html) { - $('.privilege-table-container').html(html); + Promise.all([ + app.parseAndTranslate(tpl, 'privileges.groups', { privileges }), + app.parseAndTranslate(tpl, 'privileges.users', { privileges }), + ]).then((html) => { + $('.privilege-table-container tbody').first().html(html[0]); + $('.privilege-table-container tbody').last().html(html[1]); Privileges.exposeAssumedPrivileges(); checkboxRowSelector.updateAll(); hightlightRowByDataAttr('data-group-name', groupToHighlight); }); - }); + }).catch(app.alertError); }; Privileges.exposeAssumedPrivileges = function (isBanned) { @@ -195,23 +202,7 @@ define('admin/manage/privileges', [ applyPrivileges(registeredUsersPrivs, getRegisteredUsersInputSelector); }; - Privileges.setPrivilege = function (member, privilege, state) { - return new Promise(function (resolve, reject) { - socket.emit('admin.categories.setPrivilege', { - cid: isNaN(cid) ? 0 : cid, - privilege: privilege, - set: state, - member: member, - }, function (err) { - if (err) { - reject(err); - return app.alertError(err.message); - } - - resolve(); - }); - }); - }; + Privileges.setPrivilege = (member, privilege, state) => api[state ? 'put' : 'delete'](`/categories/${isNaN(cid) ? 0 : cid}/privileges/${privilege}`, { member }); Privileges.addUserToPrivilegeTable = function () { var modal = bootbox.dialog({ diff --git a/src/api/categories.js b/src/api/categories.js index a3df7860c9..d5d9a5e4e1 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -2,6 +2,8 @@ const categories = require('../categories'); const events = require('../events'); +const user = require('../user'); +const groups = require('../groups'); const privileges = require('../privileges'); const categoriesAPI = module.exports; @@ -39,3 +41,50 @@ categoriesAPI.delete = async function (caller, data) { name: name, }); }; + +categoriesAPI.getPrivileges = async (caller, cid) => { + let responsePayload; + + if (cid === 'admin') { + responsePayload = await privileges.admin.list(caller.uid); + } else if (!parseInt(cid, 10)) { + responsePayload = await privileges.global.list(); + } else { + responsePayload = await privileges.categories.list(cid); + } + + // The various privilege .list() methods return superfluous data for the template, return only a minimal set + const validKeys = ['users', 'groups']; + Object.keys(responsePayload).forEach((key) => { + if (!validKeys.includes(key)) { + delete responsePayload[key]; + } + }); + + return responsePayload; +}; + +categoriesAPI.setPrivilege = async (caller, data) => { + const [userExists, groupExists] = await Promise.all([ + user.exists(data.member), + groups.exists(data.member), + ]); + + if (!userExists && !groupExists) { + throw new Error('[[error:no-user-or-group]]'); + } + + await privileges.categories[data.set ? 'give' : 'rescind']( + Array.isArray(data.privilege) ? data.privilege : [data.privilege], data.cid, data.member + ); + + await events.log({ + uid: caller.uid, + type: 'privilege-change', + ip: caller.ip, + privilege: data.privilege.toString(), + cid: data.cid, + action: data.set ? 'grant' : 'rescind', + target: data.member, + }); +}; diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index 17f56ab78b..b4214068b7 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -42,3 +42,27 @@ Categories.delete = async (req, res) => { await api.categories.delete(req, { cid: req.params.cid }); helpers.formatApiResponse(200, res); }; + +Categories.getPrivileges = async (req, res) => { + if (!await privileges.admin.can('admin:privileges', req.uid)) { + throw new Error('[[error:no-privileges]]'); + } + + const privilegeSet = await api.categories.getPrivileges(req, req.params.cid); + helpers.formatApiResponse(200, res, privilegeSet); +}; + +Categories.setPrivilege = async (req, res) => { + if (!await privileges.admin.can('admin:privileges', req.uid)) { + throw new Error('[[error:no-privileges]]'); + } + + await api.categories.setPrivilege(req, { + ...req.params, + member: req.body.member, + set: req.method === 'PUT', + }); + + const privilegeSet = await api.categories.getPrivileges(req, req.params.cid); + helpers.formatApiResponse(200, res, privilegeSet); +}; diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index 6e1c584af8..5e4dc45dae 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -15,5 +15,9 @@ module.exports = function () { setupApiRoute(router, 'put', '/:cid', [...middlewares], controllers.write.categories.update); setupApiRoute(router, 'delete', '/:cid', [...middlewares], controllers.write.categories.delete); + setupApiRoute(router, 'get', '/:cid/privileges', [...middlewares], controllers.write.categories.getPrivileges); + setupApiRoute(router, 'put', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); + setupApiRoute(router, 'delete', '/:cid/privileges/:privilege', [...middlewares, middleware.checkRequired.bind(null, ['member'])], controllers.write.categories.setPrivilege); + return router; }; diff --git a/src/socket.io/admin/categories.js b/src/socket.io/admin/categories.js index 08643b2cdf..31ac606a08 100644 --- a/src/socket.io/admin/categories.js +++ b/src/socket.io/admin/categories.js @@ -2,12 +2,8 @@ const winston = require('winston'); -const groups = require('../../groups'); -const user = require('../../user'); const categories = require('../../categories'); -const privileges = require('../../privileges'); const plugins = require('../../plugins'); -const events = require('../../events'); const api = require('../../api'); const sockets = require('..'); @@ -55,40 +51,21 @@ Categories.update = async function (socket, data) { }; Categories.setPrivilege = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT /api/v3/categories/:cid/privileges/:privilege'); + if (!data) { throw new Error('[[error:invalid-data]]'); } - const [userExists, groupExists] = await Promise.all([ - user.exists(data.member), - groups.exists(data.member), - ]); - - if (!userExists && !groupExists) { - throw new Error('[[error:no-user-or-group]]'); - } - - await privileges.categories[data.set ? 'give' : 'rescind']( - Array.isArray(data.privilege) ? data.privilege : [data.privilege], data.cid, data.member - ); - - await events.log({ - uid: socket.uid, - type: 'privilege-change', - ip: socket.ip, - privilege: data.privilege.toString(), - cid: data.cid, - action: data.set ? 'grant' : 'rescind', - target: data.member, - }); + return await api.categories.setPrivilege(socket, data); }; Categories.getPrivilegeSettings = async function (socket, cid) { - if (cid === 'admin') { - return await privileges.admin.list(socket.uid); - } else if (!parseInt(cid, 10)) { - return await privileges.global.list(); + sockets.warnDeprecated(socket, 'GET /api/v3/categories/:cid/privileges'); + + if (!isFinite(cid) && cid !== 'admin') { + throw new Error('[[error:invalid-data]]'); } - return await privileges.categories.list(cid); + return await api.categories.getPrivileges(socket, cid); }; Categories.copyPrivilegesToChildren = async function (socket, data) { diff --git a/src/views/admin/partials/privileges/category.tpl b/src/views/admin/partials/privileges/category.tpl index 5e3bfc50ab..127e9b203a 100644 --- a/src/views/admin/partials/privileges/category.tpl +++ b/src/views/admin/partials/privileges/category.tpl @@ -57,6 +57,8 @@ {function.spawnPrivilegeStates, privileges.groups.name, ../privileges} + +
@@ -79,7 +81,7 @@
- +
[[admin/manage/categories:privileges.inherit]] diff --git a/src/views/admin/partials/privileges/global.tpl b/src/views/admin/partials/privileges/global.tpl index 79e6c4dc57..b854eaa2f3 100644 --- a/src/views/admin/partials/privileges/global.tpl +++ b/src/views/admin/partials/privileges/global.tpl @@ -29,6 +29,8 @@ {function.spawnPrivilegeStates, privileges.groups.name, ../privileges} + +
@@ -39,7 +41,7 @@
- +
[[admin/manage/categories:privileges.inherit]]