From 18e9fc8e7c41bb27f92f9b4f5a767d375d1078d2 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Mon, 20 Sep 2021 00:49:59 +0200 Subject: [PATCH] Provide roles and permission for the control user Add the roles and their permissions for the user the control connection is connected as along with updates when these change. This enables the web interface to know which permissions it has available to it. --- packages/ctl/ctl.js | 2 + packages/lib/link/messages.js | 24 ++++++++++ packages/lib/schema.js | 19 +++++++- packages/master/src/ControlConnection.js | 4 ++ packages/master/src/Master.js | 39 +++++++++++++++++ packages/master/src/WsServer.js | 5 +++ packages/web_ui/src/model/account.jsx | 56 ++++++++++++++++++++---- packages/web_ui/src/util/websocket.jsx | 37 ++++++++++++++++ test/integration/index.js | 2 + 9 files changed, 178 insertions(+), 10 deletions(-) diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index 6cdff8b5..ac01a274 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -927,6 +927,8 @@ class Control extends libLink.Link { this.keepOpen = false; } + async accountUpdateEventHandler() { } + async slaveUpdateEventHandler() { } async instanceUpdateEventHandler() { } diff --git a/packages/lib/link/messages.js b/packages/lib/link/messages.js index 5cc260d2..5a13f6dd 100644 --- a/packages/lib/link/messages.js +++ b/packages/lib/link/messages.js @@ -1085,6 +1085,30 @@ messages.debugWsMessage = new Event({ }, }); +messages.accountUpdate = new Event({ + type: "account_update", + links: ["master-control"], + eventRequired: [], + eventProperties: { + "roles": { + type: "array", + items: { + additionalProperties: false, + type: "object", + required: ["name", "id", "permissions"], + properties: { + "name": { type: "string" }, + "id": { type: "integer" }, + "permissions": { + type: "array", + items: { type: "string" }, + }, + }, + }, + }, + }, +}); + messages.logMessage = new Event({ type: "log_message", links: ["slave-master", "master-control"], diff --git a/packages/lib/schema.js b/packages/lib/schema.js index 6753fd69..da43a843 100644 --- a/packages/lib/schema.js +++ b/packages/lib/schema.js @@ -104,10 +104,27 @@ const serverHandshake = ajv.compile({ "session_timeout": { type: "number" }, "heartbeat_interval": { type: "number" }, "account": { + type: "object", additionalProperties: false, - required: ["name"], + required: ["name", "roles"], properties: { "name": { type: "string" }, + "roles": { + type: "array", + items: { + additionalProperties: false, + type: "object", + required: ["name", "id", "permissions"], + properties: { + "name": { type: "string" }, + "id": { type: "integer" }, + "permissions": { + type: "array", + items: { type: "string" }, + }, + }, + }, + }, }, }, }, diff --git a/packages/master/src/ControlConnection.js b/packages/master/src/ControlConnection.js index 1d2efd49..69e1f5e9 100644 --- a/packages/master/src/ControlConnection.js +++ b/packages/master/src/ControlConnection.js @@ -513,6 +513,7 @@ class ControlConnection extends BaseConnection { role.name = name; role.description = description; role.permissions = new Set(permissions); + this._master.rolePermissionsUpdated(role); } async grantDefaultRolePermissionsRequestHandler(message) { @@ -522,6 +523,7 @@ class ControlConnection extends BaseConnection { } role.grantDefaultPermissions(); + this._master.rolePermissionsUpdated(role); } async deleteRoleRequestHandler(message) { @@ -534,6 +536,7 @@ class ControlConnection extends BaseConnection { this._master.userManager.roles.delete(id); for (let user of this._master.userManager.users.values()) { user.roles.delete(role); + this._master.userPermissionsUpdated(user); } } @@ -573,6 +576,7 @@ class ControlConnection extends BaseConnection { } user.roles = resolvedRoles; + this._master.userPermissionsUpdated(user); } async setUserAdminRequestHandler(message) { diff --git a/packages/master/src/Master.js b/packages/master/src/Master.js index f7590049..52711027 100644 --- a/packages/master/src/Master.js +++ b/packages/master/src/Master.js @@ -10,6 +10,7 @@ const path = require("path"); const libConfig = require("@clusterio/lib/config"); const libErrors = require("@clusterio/lib/errors"); +const libLink = require("@clusterio/lib/link"); const libPlugin = require("@clusterio/lib/plugin"); const libPluginLoader = require("@clusterio/lib/plugin_loader"); const libPrometheus = require("@clusterio/lib/prometheus"); @@ -410,6 +411,44 @@ class Master { } } + /** + * Notify connected control clients under the given user that the + * permissions for this user may have changed. + * @param {module:lib/users.User} user - User permisions updated for. + */ + userPermissionsUpdated(user) { + for (let controlConnection of this.wsServer.controlConnections) { + if (controlConnection.user === user) { + libLink.messages.accountUpdate.send(controlConnection, { + "roles": [...user.roles].map(r => ({ + name: r.name, + id: r.id, + permissions: [...r.permissions], + })), + }); + } + } + } + + /** + * Notify connected control clients with the given role that the + * permissions may have changed. + * @param {module:lib/users.Role} role - Role permisions updated for. + */ + rolePermissionsUpdated(role) { + for (let controlConnection of this.wsServer.controlConnections) { + if (controlConnection.user.roles.has(role)) { + libLink.messages.accountUpdate.send(controlConnection, { + "roles": [...controlConnection.user.roles].map(r => ({ + name: r.name, + id: r.id, + permissions: [...r.permissions], + })), + }); + } + } + } + async loadPlugins() { for (let pluginInfo of this.pluginInfos) { if (!this.config.group(pluginInfo.name).get("load_plugin")) { diff --git a/packages/master/src/WsServer.js b/packages/master/src/WsServer.js index ea0ada5f..9c383976 100644 --- a/packages/master/src/WsServer.js +++ b/packages/master/src/WsServer.js @@ -298,6 +298,11 @@ ${err.stack}` this.controlConnections.push(connection); additionalReadyData["account"] = { name: user.name, + roles: [...user.roles].map(r => ({ + name: r.name, + id: r.id, + permissions: [...r.permissions], + })), }; } diff --git a/packages/web_ui/src/model/account.jsx b/packages/web_ui/src/model/account.jsx index e2b7631b..165a50d1 100644 --- a/packages/web_ui/src/model/account.jsx +++ b/packages/web_ui/src/model/account.jsx @@ -6,30 +6,68 @@ import ControlContext from "../components/ControlContext"; /** * @typedef {Object} Account * @property {?string} name - Name of the currently logged in account. + * @property {?Array} roles - Roles of the corrently logged in account. + * @property {function(string)} hasPermission - + * Check if the currently logged in account has the given permission. * @property {function()} logout - Logs out of the web interface. */ export function useAccount() { let control = useContext(ControlContext); let [name, setName] = useState(control.accountName); + let [roles, setRoles] = useState(control.accountRoles); useEffect(() => { - function onConnect(data) { - setName(data.account.name); + function onAccountUpdate(account) { + setName(account.name); + setRoles(account.roles); } - function onClose() { - setName(null); - } - control.connector.on("connect", onConnect); - control.connector.on("close", onClose); + control.onAccountUpdate(onAccountUpdate); return () => { - control.connector.off("connect", onConnect); - control.connector.off("close", onClose); + control.offAccountUpdate(onAccountUpdate); }; }, [control]); + function hasPermission(permission) { + for (let role of roles) { + if (role.permissions.includes("core.admin") || role.permissions.includes(permission)) { + return true; + } + } + return false; + } + return { name, + roles, + hasPermission(permission) { + if (!roles) { + return null; + } + return hasPermission(permission); + }, + hasAnyPermission(...permissions) { + if (!roles) { + return null; + } + for (let permission of permissions) { + if (hasPermission(permission)) { + return true; + } + } + return false; + }, + hasAllPermission(...permissions) { + if (!roles) { + return null; + } + for (let permission of permissions) { + if (!hasPermission(permission)) { + return false; + } + } + return true; + }, logOut() { control.loggingOut = true; control.shutdown(notifyErrorHandler("Error logging out")); diff --git a/packages/web_ui/src/util/websocket.jsx b/packages/web_ui/src/util/websocket.jsx index aab12ecf..18c54f1c 100644 --- a/packages/web_ui/src/util/websocket.jsx +++ b/packages/web_ui/src/util/websocket.jsx @@ -60,6 +60,13 @@ export class Control extends libLink.Link { */ this.accountName = null; + /** + * Roles of the account this control link is connected as. + * @type {?Array} + */ + this.accountRoles = null; + + this.accountUpdateHandlers = []; this.slaveUpdateHandlers = new Map(); this.instanceUpdateHandlers = new Map(); this.saveListUpdateHandlers = new Map(); @@ -67,6 +74,8 @@ export class Control extends libLink.Link { this.connector.on("connect", data => { this.accountName = data.account.name; + this.accountRoles = data.account.roles; + this.emitAccountUpdate(); this.updateSlaveSubscriptions().catch(err => logger.error( `Unexpected error updating slave subscriptions:\n${err.stack}` )); @@ -83,6 +92,8 @@ export class Control extends libLink.Link { this.connector.on("close", () => { this.accountName = null; + this.accountRoles = null; + this.emitAccountUpdate(); }); for (let event of ["connect", "drop", "resume", "close"]) { @@ -94,6 +105,32 @@ export class Control extends libLink.Link { } } + async accountUpdateEventHandler(message) { + this.accountRoles = message.data.roles; + this.emitAccountUpdate(); + } + + emitAccountUpdate() { + for (let handler of this.accountUpdateHandlers) { + handler({ + name: this.accountName, + roles: this.accountRoles, + }); + } + } + + onAccountUpdate(handler) { + this.accountUpdateHandlers.push(handler); + } + + offAccountUpdate(handler) { + let index = this.accountUpdateHandlers.lastIndexOf(handler); + if (index === -1) { + throw new Error("Given handler is not registered for account update"); + } + this.accountUpdateHandlers.splice(index, 1); + } + async slaveUpdateEventHandler(message) { let handlers = [].concat( this.slaveUpdateHandlers.get(null) || [], diff --git a/test/integration/index.js b/test/integration/index.js index 5dc04010..0bc08be8 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -49,6 +49,8 @@ class TestControl extends libLink.Link { async debugWsMessageEventHandler() { } + async accountUpdateEventHandler() { } + async slaveUpdateEventHandler(message) { this.slaveUpdates.push(message.data); }