From 84450b83b4f013fc03c3ca39dea45650be448957 Mon Sep 17 00:00:00 2001 From: Hornwitser Date: Sat, 15 Aug 2020 11:34:28 +0200 Subject: [PATCH] Implement adminlist, whitelist and banlist sync Add integration of the server adminlist, whitelist and banlist into clusterio. Membership is stored as booleans in the user acconuts and changes are synchronized live to slaves which pass it on to instances which in turn will update the running server's lists with RCON commands. --- .eslintrc.js | 3 +- clusterctl.js | 46 ++++++++++ docs/maniging-a-cluster.md | 132 +++++++++++++++++++++++++++ lib/config/definitions.js | 24 +++++ lib/factorio/server.js | 4 + lib/link/messages.js | 91 +++++++++++++++++++ lib/users.js | 71 +++++++++++++-- master.js | 92 ++++++++++++++++++- slave.js | 166 +++++++++++++++++++++++++++++++++- test/integration/clusterio.js | 46 +++++++++- test/lib/users.js | 2 + 11 files changed, 666 insertions(+), 11 deletions(-) create mode 100644 docs/maniging-a-cluster.md diff --git a/.eslintrc.js b/.eslintrc.js index e518c29a..8cc36d30 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,6 +18,7 @@ module.exports = { }, "rules": { "prefer-arrow-callback": "off", + "no-invalid-this": "off", }, }, ], @@ -96,7 +97,7 @@ module.exports = { "max-statements": "off", "max-statements-per-line": "off", "multiline-comment-style": ["warn", "separate-lines"], - "multiline-ternary": "warn", + "multiline-ternary": "off", "new-cap": "warn", "new-parens": "error", "newline-after-var": "off", diff --git a/clusterctl.js b/clusterctl.js index bf8eca3b..310d5265 100644 --- a/clusterctl.js +++ b/clusterctl.js @@ -407,6 +407,52 @@ userCommands.add(new command.Command({ }, })); +userCommands.add(new command.Command({ + definition: ["set-admin ", "Promote or demote a user to admin", (yargs) => { + yargs.positional("user", { describe: "Name of user set admin status for", type: "string" }); + yargs.options({ + "revoke": { describe: "Revoke admin status", nargs: 0, type: "boolean", default: false }, + "create": { describe: "Create user if it does not exist", nargs: 0, type: "boolean", default: false }, + }); + }], + handler: async function(args, control) { + await link.messages.setUserAdmin.send(control, { + name: args.user, create: args.create, admin: !args.revoke, + }); + }, +})); + +userCommands.add(new command.Command({ + definition: ["set-whitelisted ", "Add or remove user from the whitelist", (yargs) => { + yargs.positional("user", { describe: "Name of user to set whitelist status for", type: "string" }); + yargs.options({ + "remove": { describe: "Remove from whitelist", nargs: 0, type: "boolean", default: false }, + "create": { describe: "Create user if it does not exist", nargs: 0, type: "boolean", default: false }, + }); + }], + handler: async function(args, control) { + await link.messages.setUserWhitelisted.send(control, { + name: args.user, create: args.create, whitelisted: !args.remove, + }); + }, +})); + +userCommands.add(new command.Command({ + definition: ["set-banned ", "Ban or pardon user from banlist", (yargs) => { + yargs.positional("user", { describe: "Name of user to set ban status for", type: "string" }); + yargs.options({ + "pardon": { describe: "Remove from banlist", nargs: 0, type: "boolean", default: false }, + "reason": { describe: "Ban reason", nargs: 1, type: "string", default: "" }, + "create": { describe: "Create user if it does not exist", nargs: 0, type: "boolean", default: false }, + }); + }], + handler: async function(args, control) { + await link.messages.setUserBanned.send(control, { + name: args.user, create: args.create, banned: !args.pardon, reason: args.reason, + }); + }, +})); + userCommands.add(new command.Command({ definition: ["set-roles [roles...]", "Replace user roles", (yargs) => { yargs.positional("user", { describe: "Name of user to change roles for", type: "string" }); diff --git a/docs/maniging-a-cluster.md b/docs/maniging-a-cluster.md new file mode 100644 index 00000000..4db36968 --- /dev/null +++ b/docs/maniging-a-cluster.md @@ -0,0 +1,132 @@ +Managing a Cluster +================== + +Clusterio clusers are managed through the master server by using the +`clusterctl` command line interface which is invoked by running +`node clusterctl ` in the clusterio directory. This document +uses the shorthand `ctl> foo` to indicate `node clusterctl foo` should +be executed. Mandatory parameters are shown in `` and +optional pameters are in `[square brackets]`. + +Before `clusterctl` can be used it needs to be configured for the +cluster it will connect to. The easiest way to do this is to run +`node master bootstrap create-ctl-config ` on the master +server, which creates the necessary `config-control.json` for managing +the cluster as the given user. + + +Slaves +------ + +To be written. + + +Instances +--------- + +To be written. + + +Users +----- + +Clusterio automatically creates user accounts for all players that join +an instance when save patching is enabled. These accounts are used to +store per player data shared between instances, like if the player +should be whitelisted, admin or banned. + +### List users + + ctl> user list + +Lists all user accounts in the cluster along with some data for each. + + +### Create User + + ctl> user create + +Creates a new empty user account for the given Factorio player. Note +that case matters here. This is usually not required as user accounts +are created automatically for players that join instances with save +patching enabled. + + +### Promote User to Server Admin + + ctl> user set-admin [--revoke] [--create] + +Promotes the user given to in-game admin on instances with the +`sync_adminlist` option enabled. If the `--revoke` switch is used the +user is removed from the adminlist. + +Since admin status is a part of the account data the account must exist +for this to succeed, passing `--create` will create the account if it +does not exist. + +**Note:** Being a server admin does not grant any access to manage the +cluster. See [set-roles](#set-cluster-roles) for adding roles which +grant access to managing the clusetr. + + +### Whitelist User + + ctl> user set-whitelisted [--remove] [--create] + +Add the user given to the whitelist on instances with the +`sync_whitelist` option enabled. If the `--remove` switch is used the +user is removed from the whitelist. + +Since whitelisted status is a part of the account data the account must +exist for this to succeed, passing `--create` will create the account if +it does not exist. + + +### Ban User + + ctl> user set-banned [--reason ] [--pardon] [--create] + +Ban user in-game from instances with the `sync_banlist` option enabled. +Reason is a message that will be shown to the user when attemption to +log in. If the `--pardon` switch is used it removes the ban. + +Since ban status is a part of the account data the account must exist +for this to succeed, passing `--create` will create the account if it +does not exist. + +Note: This bans the user from logging in to Factorio servers in the +cluster, it does not revoke access to any cluster management they might +have, see next section on setting roles for revoking tha. + + +### Set Cluster Roles + + ctl> user set-roles [roles...] + +Replaces the roles the user has in the cluster with a new list of roles. +Calling with an empty roles argument will remove all roles from the +user. + +By default there's a roled named Admin which grants access to +everything and a role named Player which grants a limited read access to +the cluster, see [the section on roles](#roles) for more information +about setting up roles and permissions. + + +### Delete user + + ctl> user delete + +Deletes everything stored on the master server for this user. + +Note: If the player is banned from the cluster this will effectively +unban them, as the ban status is stored with the user account. + +Note: If the player joins the cluster again a new account will be made +for them automatically. + + +Roles +----- + +To be written. diff --git a/lib/config/definitions.js b/lib/config/definitions.js index e098c66b..7e4b25d6 100644 --- a/lib/config/definitions.js +++ b/lib/config/definitions.js @@ -245,6 +245,12 @@ FactorioGroup.define({ type: "boolean", initial_value: true, }); +FactorioGroup.define({ + name: "enable_whitelist", + description: "Turn on whitelist for joining the server.", + type: "boolean", + initial_value: false, +}); FactorioGroup.define({ name: "settings", description: "Settings overridden in server-settings.json", @@ -254,6 +260,24 @@ FactorioGroup.define({ "auto_pause": false, }, }); +FactorioGroup.define({ + name: "sync_adminlist", + description: "Synchronize adminlist with master server", + type: "boolean", + initial_value: true, +}); +FactorioGroup.define({ + name: "sync_whitelist", + description: "Synchronize whitelist with master server", + type: "boolean", + initial_value: true, +}); +FactorioGroup.define({ + name: "sync_banlist", + description: "Synchronize banlist with master server", + type: "boolean", + initial_value: true, +}); FactorioGroup.finalize(); /** diff --git a/lib/factorio/server.js b/lib/factorio/server.js index 9b2a033b..77f0cc70 100644 --- a/lib/factorio/server.js +++ b/lib/factorio/server.js @@ -515,6 +515,8 @@ class FactorioServer extends events.EventEmitter { this.rconPort = options.rconPort || randomDynamicPort(); /** Password used for RCON on the Factorio game server */ this.rconPassword = options.rconPassword; + /** Enable player whitelist */ + this.enableWhitelist = options.enableWhitelist; this._state = "new"; this._server = null; this._rconClient = null; @@ -813,6 +815,7 @@ class FactorioServer extends events.EventEmitter { "--port", this.gamePort, "--rcon-port", this.rconPort, "--rcon-password", this.rconPassword, + ...(this.enableWhitelist ? ["--use-server-whitelist"] : []), ], { detached: true, @@ -846,6 +849,7 @@ class FactorioServer extends events.EventEmitter { "--port", this.gamePort, "--rcon-port", this.rconPort, "--rcon-password", this.rconPassword, + ...(this.enableWhitelist ? ["--use-server-whitelist"] : []), ], { detached: true, diff --git a/lib/link/messages.js b/lib/link/messages.js index 9a795317..648aef20 100644 --- a/lib/link/messages.js +++ b/lib/link/messages.js @@ -560,6 +560,9 @@ messages.listUsers = new Request({ properties: { "name": { type: "string" }, "roles": { type: "array", items: { type: "integer" }}, + "is_admin": { type: "boolean" }, + "is_banned": { type: "boolean" }, + "is_whitelisted": { type: "boolean" }, "instances": { type: "array", items: { type: "integer" }}, }, }, @@ -586,6 +589,40 @@ messages.updateUserRoles = new Request({ }, }); +messages.setUserAdmin = new Request({ + type: "set_user_admin", + links: ["control-master"], + permission: "core.user.set_admin", + requestProperties: { + "name": { type: "string" }, + "create": { type: "boolean" }, + "admin": { type: "boolean" }, + }, +}); + +messages.setUserWhitelisted = new Request({ + type: "set_user_whitelisted", + links: ["control-master"], + permission: "core.user.set_whitelisted", + requestProperties: { + "name": { type: "string" }, + "create": { type: "boolean" }, + "whitelisted": { type: "boolean" }, + }, +}); + +messages.setUserBanned = new Request({ + type: "set_user_banned", + links: ["control-master"], + permission: "core.user.set_banned", + requestProperties: { + "name": { type: "string" }, + "create": { type: "boolean" }, + "banned": { type: "boolean" }, + "reason": { type: "string" }, + }, +}); + messages.deleteUser = new Request({ type: "delete_user", links: ["control-master"], @@ -841,6 +878,60 @@ messages.masterConnectionEvent = new Event({ }, }); +messages.syncUserLists = new Event({ + type: "sync_user_lists", + links: ["master-slave"], + eventProperties: { + "adminlist": { + type: "array", + items: { type: "string" }, + }, + "banlist": { + type: "array", + items: { + type: "array", + additionalItems: false, + items: [{ type: "string" }, { type: "string" }], + }, + }, + "whitelist": { + type: "array", + items: { type: "string" }, + }, + }, +}); + +messages.banlistUpdate = new Event({ + type: "banlist_update", + links: ["master-slave", "slave-instance"], + broadcastTo: "instance", + eventProperties: { + "name": { type: "string" }, + "banned": { type: "boolean" }, + "reason": { type: "string" }, + }, +}); + +messages.adminlistUpdate = new Event({ + type: "adminlist_update", + links: ["master-slave", "slave-instance"], + eventProperties: { + "name": { type: "string" }, + "admin": { type: "boolean" }, + }, + broadcastTo: "instance", +}); + +messages.whitelistUpdate = new Event({ + type: "whitelist_update", + links: ["master-slave", "slave-instance"], + eventProperties: { + "name": { type: "string" }, + "whitelisted": { type: "boolean" }, + }, + broadcastTo: "instance", +}); + messages.playerEvent = new Event({ type: "player_event", links: ["instance-slave", "slave-master"], diff --git a/lib/users.js b/lib/users.js index 4ba85e96..aa2803fb 100644 --- a/lib/users.js +++ b/lib/users.js @@ -136,12 +136,12 @@ function ensureDefaultPlayerRole(roles) { * @static */ class User { - constructor({ name, roles, tokenValidAfter }, loadedRoles) { + constructor(serializedUser, loadedRoles) { /** * Factorio user name. * @type {string} */ - this.name = name; + this.name = serializedUser.name; /** * Instances this user is online on. @@ -153,15 +153,39 @@ class User { * Unix time in seconds the user token must be issued after to be valid. * @type {number} */ - this.tokenValidAfter = tokenValidAfter || 0; + this.tokenValidAfter = serializedUser.token_valid_after || 0; + + /** + * True if the user is promoted to admin on the Factorio instances. + * @type {boolean} + */ + this.isAdmin = Boolean(serializedUser.is_admin); + + /** + * True if the user is whitelisted on the Factorio instances. + * @type {boolean} + */ + this.isWhitelisted = Boolean(serializedUser.is_whitelisted); + + /** + * True if the user is banned from Factorio instances. + * @type {boolean} + */ + this.isBanned = Boolean(serializedUser.is_banned); + + /** + * Reason for being banned. Ignored if isBanned is false. + * @type {string} + */ + this.banReason = serializedUser.ban_reason || ""; /** * Roles this user has * @type {Set} */ this.roles = new Set(); - if (roles) { - for (let roleId of roles) { + if (serializedUser.roles) { + for (let roleId of serializedUser.roles) { let role = loadedRoles.get(roleId); if (role) { this.roles.add(role); @@ -180,7 +204,23 @@ class User { } if (this.tokenValidAfter) { - serialized.tokenValidAfter = this.tokenValidAfter; + serialized.token_valid_after = this.tokenValidAfter; + } + + if (this.isAdmin) { + serialized.is_admin = true; + } + + if (this.isWhitelisted) { + serialized.is_whitelisted = true; + } + + if (this.isBanned) { + serialized.is_banned = true; + } + + if (this.banReason) { + serialized.ban_reason = this.banReason; } return serialized; @@ -242,6 +282,10 @@ class User { } } } +/** + * Set of users currently online in the cluster. + * @type {module:lib/users.User} + */ User.onlineUsers = new Set(); // Definitions for the built in permissions used in Clusterio. @@ -386,6 +430,21 @@ definePermission({ title: "Update user roles", description: "Add or remove any role to any user.", }); +definePermission({ + name: "core.user.set_admin", + title: "Set user admin status", + description: "Promote or demote any user to Factorio admin.", +}); +definePermission({ + name: "core.user.set_banned", + title: "Set user ban status", + description: "Ban or unban any user.", +}); +definePermission({ + name: "core.user.set_whitelisted", + title: "Set user whitelist status", + description: "Add or remove any user to/from the whitelist.", +}); definePermission({ name: "core.user.delete", title: "Delete user", diff --git a/master.js b/master.js index 234bd8bc..b5f8d427 100644 --- a/master.js +++ b/master.js @@ -504,7 +504,7 @@ class BaseConnection extends link.Link { event.send(connection, message.data); } - async broadcastEventToInstance(message, event) { + async broadcastEventToSlaves(message, event) { for (let slaveConnection of slaveConnections.values()) { // Do not broadcast back to the source if (slaveConnection === this) { continue; } @@ -515,6 +515,10 @@ class BaseConnection extends link.Link { } } + async broadcastEventToInstance(message, event) { + await this.broadcastEventToSlaves(message, event); + } + async prepareDisconnectRequestHandler(message, request) { await plugin.invokeHook(masterPlugins, "onPrepareSlaveDisconnect", this); this.connector.setClosing(); @@ -803,6 +807,9 @@ class ControlConnection extends BaseConnection { list.push({ name: user.name, roles: [...user.roles].map(role => role.id), + is_admin: user.isAdmin, + is_banned: user.isBanned, + is_whitelisted: user.isWhitelisted, instances: [...user.instances], }); } @@ -832,10 +839,71 @@ class ControlConnection extends BaseConnection { user.roles = resolvedRoles; } + async setUserAdminRequestHandler(message) { + let { name, create, admin } = message.data; + let user = db.users.get(name); + if (!user) { + if (create) { + this.user.checkPermission("core.user.create"); + user = createUser(name); + } else { + throw new errors.RequestError(`User '${name}' does not exist`); + } + } + + user.isAdmin = admin; + this.broadcastEventToSlaves({ data: { name, admin }}, link.messages.adminlistUpdate); + } + + async setUserBannedRequestHandler(message) { + let { name, create, banned, reason } = message.data; + let user = db.users.get(name); + if (!user) { + if (create) { + this.user.checkPermission("core.user.create"); + user = createUser(name); + } else { + throw new errors.RequestError(`User '${name}' does not exist`); + } + } + + user.isBanned = banned; + user.banReason = reason; + this.broadcastEventToSlaves({ data: { name, banned, reason }}, link.messages.banlistUpdate); + } + + async setUserWhitelistedRequestHandler(message) { + let { name, create, whitelisted } = message.data; + let user = db.users.get(name); + if (!user) { + if (create) { + this.user.checkPermission("core.user.create"); + user = createUser(name); + } else { + throw new errors.RequestError(`User '${name}' does not exist`); + } + } + + user.isWhitelisted = whitelisted; + this.broadcastEventToSlaves({ data: { name, whitelisted }}, link.messages.whitelistUpdate); + } + async deleteUserRequestHandler(message) { - if (!db.users.delete(message.data.name)) { + let user = db.users.get(message.data.name); + if (!user) { throw new errors.RequestError(`User '${message.data.name}' does not exist`); } + + if (user.is_admin) { + this.broadcastEventToSlaves({ data: { name, admin: false }}, link.messages.adminlistUpdate); + } + if (user.is_whitelisted) { + this.broadcastEventToSlaves({ data: { name, whitelisted: false }}, link.messages.whitelistUpdate); + } + if (user.is_banned) { + this.broadcastEventToSlaves({ data: { name, banned: false, reason: "" }}, link.messages.banlistUpdate); + } + db.users.delete(message.data.name); } async debugDumpWsRequestHandler(message) { @@ -949,6 +1017,25 @@ class SlaveConnection extends BaseConnection { serialized_config: instanceConfig.serialize(), }); } + + // Push lists to make sure they are in sync. + let adminlist = []; + let banlist = []; + let whitelist = []; + + for (let user of db.users.values()) { + if (user.isAdmin) { + adminlist.push(user.name); + } + if (user.isBanned) { + banlist.push([user.name, user.banReason]); + } + if (user.isWhitelisted) { + whitelist.push(user.name); + } + } + + link.messages.syncUserLists.send(this, { adminlist, banlist, whitelist }); } async instanceOutputEventHandler(message) { @@ -1548,6 +1635,7 @@ async function startServer() { let adminRole = users.ensureDefaultAdminRole(db.roles); admin.roles.add(adminRole); + admin.isAdmin = true; await saveUsers(masterConfig.get("master.database_directory"), "users.json"); } else if (subCommand === "create-ctl-config") { diff --git a/slave.js b/slave.js index 062cecff..7852aa65 100644 --- a/slave.js +++ b/slave.js @@ -117,6 +117,8 @@ class Instance extends link.Link{ if (group.name === "factorio" && field === "settings") { this.updateFactorioSettings(group.get(field), prev).finally(hook); + } else if (group.name === "factorio" && field === "enable_whitelist") { + this.updateFactorioWhitelist(group.get(field)).finally(hook); } else { hook(); } @@ -128,6 +130,7 @@ class Instance extends link.Link{ gamePort: this.config.get("factorio.game_port"), rconPort: this.config.get("factorio.rcon_port"), rconPassword: this.config.get("factorio.rcon_password"), + enableWhitelist: this.config.get("factorio.enable_whitelist"), }; this._running = false; @@ -290,12 +293,38 @@ class Instance extends link.Link{ /** * Prepare instance for starting * - * Writes server settings and links mods. + * Writes server settings, admin/ban/white-lists and links mods. */ async prepare() { console.log("Clusterio | Writing server-settings.json"); await this.writeServerSettings(); + if (this.config.get("factorio.sync_adminlist")) { + console.log("Clusterio | Writing server-adminlist.json"); + fs.outputFile( + this.server.writePath("server-adminlist.json"), + JSON.stringify([...this._slave.adminlist], null, 4) + ); + } + + if (this.config.get("factorio.sync_banlist")) { + console.log("Clusterio | Writing server-banlist.json"); + fs.outputFile( + this.server.writePath("server-banlist.json"), + JSON.stringify([...this._slave.banlist].map( + ([username, reason]) => ({ username, reason }) + ), null, 4), + ); + } + + if (this.config.get("factorio.sync_whitelist")) { + console.log("Clusterio | Writing server-whitelist.json"); + fs.outputFile( + this.server.writePath("server-whitelist.json"), + JSON.stringify([...this._slave.whitelist], null, 4) + ); + } + console.log("Clusterio | Rotating old logs..."); // clean old log file to avoid crash try{ @@ -473,6 +502,60 @@ class Instance extends link.Link{ } } + /** + * Enable or disable the player whitelist + * + * @param {boolean} enable - + * True to enable the whitelist, False to disable the whitelist. + */ + async updateFactorioWhitelist(enable) { + if (!enable) { + await this.server.sendRcon("/whitelist disable"); + } + + if (this.config.get("factorio.sync_whitelist")) { + await this.server.sendRcon("/whitelist clear"); + for (let player of this._slave.whitelist) { + await this.server.sendRcon(`/whitelist ${player}`); + } + } + + if (enable) { + await this.server.sendRcon("/whitelist enable"); + } + } + + async adminlistUpdateEventHandler(message) { + if (!this.config.get("factorio.sync_adminlist")) { + return; + } + + let { name, admin } = message.data; + let command = admin ? `/promote ${name}` : `/demote ${name}`; + await this.server.sendRcon(command); + } + + async banlistUpdateEventHandler(message) { + if (!this.config.get("factorio.sync_banlist")) { + return; + } + + let { name, banned, reason } = message.data; + let command = banned ? `/ban ${name} ${reason}` : `/unban ${name}`; + await this.server.sendRcon(command); + } + + async whitelistUpdateEventHandler(message) { + if (!this.config.get("factorio.sync_whitelist")) { + return; + } + + let { name, whitelisted } = message.data; + let command = whitelisted ? `/whitelist add ${name}` : `/whiteliste remove ${name}`; + await this.server.sendRcon(command); + } + + /** * Stop the instance */ @@ -790,6 +873,10 @@ class Slave extends link.Link { this.instanceConnections = new Map(); this.instanceInfos = new Map(); + this.adminlist = new Set(); + this.banlist = new Map(); + this.whitelist = new Set(); + this.connector.on("hello", data => { this.serverVersion = data.version; this.serverPlugins = new Map(Object.entries(data.plugins)); @@ -906,6 +993,83 @@ class Slave extends link.Link { } } + async syncUserListsEventHandler(message) { + let updateList = (list, updatedList, prop, event) => { + let added = new Set(updatedList); + let removed = new Set(list); + list.forEach(Set.prototype.delete.bind(added)); + updatedList.forEach(Set.prototype.delete.bind(removed)); + + for (let name of added) { + list.add(name); + this.broadcastEventToInstance( + { data: { name, [prop]: true }}, event + ); + } + + for (let name of removed) { + list.delete(name); + this.broadcastEventToInstance( + { data: { name, [prop]: false }}, event + ); + } + }; + + updateList(this.adminlist, message.data.adminlist, "admin", link.messages.adminlistUpdate); + updateList(this.whitelist, message.data.whitelist, "whitelisted", link.messages.whitelistUpdate); + + let updatedBanlist = new Map(message.data.banlist); + let addedOrChanged = new Map(updatedBanlist); + let removed = new Set(this.banlist.keys()); + removed.forEach((name, reason) => { + if (addedOrChanged.get(name) === reason) { + addedOrChanged.delete(name); + } + }); + updatedBanlist.forEach(Set.prototype.delete.bind(removed)); + + for (let [name, reason] of addedOrChanged) { + this.banlist.set(name, reason); + this.broadcastEventToInstance( + { data: { name, banned: true, reason }}, link.messages.banlistUpdate + ); + } + + for (let name of removed) { + this.banlist.delete(name); + this.broadcastEventToInstance( + { data: { name, banned: false, reason: "" }}, link.messages.banlistUpdate + ); + } + } + + async adminlistUpdateEventHandler(message) { + let { name, admin } = message.data; + if (admin) { + this.adminlist.add(name); + } else { + this.adminlist.delete(name); + } + } + + async banlistUpdateEventHandler(message) { + let { name, banned, reason } = message.data; + if (banned) { + this.banlist.set(name, reason); + } else { + this.banlist.delete(name); + } + } + + async whitelistUpdateEventHandler(message) { + let { name, whitelisted } = message.data; + if (whitelisted) { + this.whitelist.add(name); + } else { + this.whitelist.delete(name); + } + } + async assignInstanceRequestHandler(message) { let { instance_id, serialized_config } = message.data; let instanceInfo = this.instanceInfos.get(instance_id); diff --git a/test/integration/clusterio.js b/test/integration/clusterio.js index 2370dc6b..be5be862 100644 --- a/test/integration/clusterio.js +++ b/test/integration/clusterio.js @@ -242,9 +242,53 @@ describe("Integration of Clusterio", function() { }); }); + async function getUsers() { + let result = await link.messages.listUsers.send(getControl()); + return new Map(result.list.map(user => [user.name, user])); + } + + describe("user set-admin/whitelisted/banned", function() { + let lists = ["admin", "whitelisted", "banned"]; + it("should add and remove the given user to the list", async function() { + slowTest(this); + await link.messages.createUser.send(getControl(), { name: "list_test" }); + let user = (await getUsers()).get("list_test"); + for (let list of lists) { + assert.equal(user[`is_${list}`], false, `unexpected ${list} status`); + await execCtl(`user set-${list} list_test`); + } + user = (await getUsers()).get("list_test"); + for (let list of lists) { + assert.equal(user[`is_${list}`], true, `unexpected ${list} status`); + let remove = { admin: "--revoke", whitelisted: "--remove", banned: "--pardon" }[list]; + await execCtl(`user set-${list} ${remove} list_test`); + } + user = (await getUsers()).get("list_test"); + for (let list of lists) { + assert.equal(user[`is_${list}`], false, `unexpected ${list} status`); + } + }); + it("should not create the user if not instructed to", async function() { + for (let list of lists) { + try { + await execCtl(`user set-${list} no_create_test`); + } catch (err) { /* ignore */ } + } + let user = (await getUsers()).get("no_create_test"); + assert.equal(user, undefined, "user was unexpectedly created"); + }); + it("should create the user if instructed to", async function() { + for (let list of lists) { + await execCtl(`user set-${list} --create test_create_${list}`); + let user = (await getUsers()).get(`test_create_${list}`); + assert.equal(user && user[`is_${list}`], true, `user not created and added to ${list}`); + } + }); + }); + describe("user set-roles", function() { it("should set the roles on the user", async function() { - await execCtl(`user set-roles temp Admin`); + await execCtl("user set-roles temp Admin"); let result = await link.messages.listUsers.send(getControl()); let user = result.list.find(user => user.name === "temp"); assert.deepEqual(user.roles, [0]); diff --git a/test/lib/users.js b/test/lib/users.js index 2e577135..35788d1d 100644 --- a/test/lib/users.js +++ b/test/lib/users.js @@ -81,6 +81,8 @@ describe("lib/users", function() { test_roundtrip({ name: "admin", roles: [1] }); test_roundtrip({ name: "user", roles: [2], token_valid_after: 12345 }); + test_roundtrip({ name: "user", is_admin: true, is_whitelisted: true }); + test_roundtrip({ name: "user", is_banned: true, ban_reason: "Bad user" }); }); it("should ignore invalid roles", function() { let user = new users.User({ name: "test", roles: [1, 4, 55] }, roles);