mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
Merge pull request #313 from Hornwitser/list-management
List management
This commit is contained in:
13 files changed
+732
-59
No files matched your search
+2
-1
@@ -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",
|
||||
|
||||
@@ -42,6 +42,8 @@ Version 2.0.0
|
||||
- Added support for extracting locale and item icons from Factorio and mods.
|
||||
- Added integrated user management for controlling access and storing per
|
||||
player data.
|
||||
- Added synchronization of in-game adminlist, banlist and whitelist to
|
||||
Clusterio core. Previously this was handled by the playerManager plugin.
|
||||
|
||||
### Changes
|
||||
|
||||
|
||||
@@ -407,6 +407,52 @@ userCommands.add(new command.Command({
|
||||
},
|
||||
}));
|
||||
|
||||
userCommands.add(new command.Command({
|
||||
definition: ["set-admin <user>", "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 <user>", "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 <user>", "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 <user> [roles...]", "Replace user roles", (yargs) => {
|
||||
yargs.positional("user", { describe: "Name of user to change roles for", type: "string" });
|
||||
|
||||
@@ -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 <command>` in the clusterio directory. This document
|
||||
uses the shorthand `ctl> foo` to indicate `node clusterctl foo` should
|
||||
be executed. Mandatory parameters are shown in `<angles bracket>` 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 <username>` 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 <name>
|
||||
|
||||
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 <name> [--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 <name> [--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 <name> [--reason <message>] [--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 <name> [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 <name>
|
||||
|
||||
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.
|
||||
@@ -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();
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
|
||||
+69
-7
@@ -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<module:lib/permissions.Role>}
|
||||
*/
|
||||
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);
|
||||
@@ -173,11 +197,30 @@ class User {
|
||||
serialize() {
|
||||
let serialized = {
|
||||
name: this.name,
|
||||
roles: [...this.roles].map(role => role.id),
|
||||
};
|
||||
|
||||
if (this.roles.size) {
|
||||
serialized.roles = [...this.roles].map(role => role.id);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -239,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.
|
||||
@@ -383,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",
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -7,7 +7,7 @@ const parallel = require("mocha.parallel");
|
||||
|
||||
const link = require("lib/link");
|
||||
|
||||
const { slowTest, get, exec, sendRcon, getControl, controlConfigPath, instancesDir } = require("./index");
|
||||
const { slowTest, get, execCtl, sendRcon, getControl, instancesDir } = require("./index");
|
||||
|
||||
|
||||
describe("Integration of Clusterio", function() {
|
||||
@@ -30,62 +30,62 @@ describe("Integration of Clusterio", function() {
|
||||
|
||||
|
||||
describe("clusterctl", function() {
|
||||
describe("list-slaves", function() {
|
||||
describe("slave list", function() {
|
||||
it("runs", async function() {
|
||||
await exec(`node clusterctl --config ${controlConfigPath} slave list`);
|
||||
await execCtl("slave list");
|
||||
});
|
||||
});
|
||||
describe("list-instances", function() {
|
||||
describe("instance list", function() {
|
||||
it("runs", async function() {
|
||||
await exec(`node clusterctl --config ${controlConfigPath} instance list`);
|
||||
await execCtl("instance list");
|
||||
});
|
||||
});
|
||||
|
||||
describe("create-instances", function() {
|
||||
describe("instance create", function() {
|
||||
it("runs", async function() {
|
||||
await exec(`node clusterctl --config ${controlConfigPath} instance create test --id 44`);
|
||||
await execCtl("instance create test --id 44");
|
||||
});
|
||||
});
|
||||
|
||||
describe("assign-instance", function() {
|
||||
describe("instance assign", function() {
|
||||
it("creates the instance files", async function() {
|
||||
await exec(`node clusterctl --config ${controlConfigPath} instance assign test 4`);
|
||||
await execCtl("instance assign test 4");
|
||||
assert(await fs.exists(path.join(instancesDir, "test")), "Instance was not created");
|
||||
});
|
||||
});
|
||||
|
||||
describe("create-save", function() {
|
||||
describe("instance create-save", function() {
|
||||
it("creates a save", async function() {
|
||||
slowTest(this);
|
||||
await exec(`node clusterctl --config ${controlConfigPath} instance create-save test`);
|
||||
await execCtl("instance create-save test");
|
||||
});
|
||||
});
|
||||
|
||||
describe("export-data", function() {
|
||||
describe("instance export-data", function() {
|
||||
it("exports the data", async function() {
|
||||
slowTest(this);
|
||||
await exec(`node clusterctl --config ${controlConfigPath} instance export-data test`);
|
||||
await execCtl("instance export-data test");
|
||||
assert(await fs.exists(path.join("static", "export", "locale.json")), "Export was not created");
|
||||
});
|
||||
});
|
||||
|
||||
describe("start-instance", function() {
|
||||
describe("instance start", function() {
|
||||
it("starts the instance", async function() {
|
||||
slowTest(this);
|
||||
await exec(`node clusterctl --config ${controlConfigPath} instance start test`);
|
||||
await execCtl("instance start test");
|
||||
// TODO check that the instance actually started
|
||||
});
|
||||
});
|
||||
|
||||
describe("send-rcon", function() {
|
||||
describe("instance send-rcon", function() {
|
||||
it("sends the command", async function() {
|
||||
slowTest(this);
|
||||
await exec(`node clusterctl --config ${controlConfigPath} instance send-rcon test test`);
|
||||
await execCtl("instance send-rcon test test");
|
||||
// TODO check that the command was received
|
||||
});
|
||||
});
|
||||
|
||||
describe("set-instance-config-prop", function() {
|
||||
describe("instance config set-prop", function() {
|
||||
it("applies factorio settings while running", async function() {
|
||||
slowTest(this);
|
||||
|
||||
@@ -159,7 +159,7 @@ describe("Integration of Clusterio", function() {
|
||||
for (let [prop, value, ,] of testConfigs) {
|
||||
value = `"'${JSON.stringify(value).replace(/"/g, process.platform === "win32" ? '""' : '\\"')}'"`;
|
||||
let args = `test factorio.settings ${prop} ${value}`;
|
||||
await exec(`node clusterctl --config ${controlConfigPath} instance config set-prop ${args}`);
|
||||
await execCtl(`instance config set-prop ${args}`);
|
||||
}
|
||||
|
||||
// Do this afterwards to leave enough to time for the
|
||||
@@ -170,90 +170,134 @@ describe("Integration of Clusterio", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("stop-instance", function() {
|
||||
describe("user set-admin/whitelisted/banned", function() {
|
||||
async function getUsers() {
|
||||
let result = await link.messages.listUsers.send(getControl());
|
||||
return new Map(result.list.map(user => [user.name, user]));
|
||||
}
|
||||
|
||||
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("instance stop", function() {
|
||||
it("stops the instance", async function() {
|
||||
slowTest(this);
|
||||
await exec(`node clusterctl --config ${controlConfigPath} instance stop test`);
|
||||
await execCtl("instance stop test");
|
||||
// TODO check that the instance actually stopped
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete-instance", function() {
|
||||
describe("instance delete", function() {
|
||||
it("deletes the instance", async function() {
|
||||
slowTest(this);
|
||||
await exec(`node clusterctl --config ${controlConfigPath} instance delete test`);
|
||||
await execCtl("instance delete test");
|
||||
assert(!await fs.exists(path.join(instancesDir, "test")), "Instance was not deleted");
|
||||
});
|
||||
});
|
||||
|
||||
describe("list-permissions", function() {
|
||||
describe("permission list", function() {
|
||||
it("runs", async function() {
|
||||
await exec(`node clusterctl --config ${controlConfigPath} permission list`);
|
||||
await execCtl("permission list");
|
||||
});
|
||||
});
|
||||
|
||||
describe("list-roles", function() {
|
||||
describe("role list", function() {
|
||||
it("runs", async function() {
|
||||
await exec(`node clusterctl --config ${controlConfigPath} role list`);
|
||||
await execCtl("role list");
|
||||
});
|
||||
});
|
||||
|
||||
describe("create-role", function() {
|
||||
describe("role create", function() {
|
||||
it("should create the given role", async function() {
|
||||
let args = "--description \"A temp role\" --permissions core.control.connect";
|
||||
await exec(`node clusterctl --config ${controlConfigPath} role create temp ${args}`);
|
||||
await execCtl(`role create temp ${args}`);
|
||||
let result = await link.messages.listRoles.send(getControl());
|
||||
let role = result.list.find(role => role.name === "temp");
|
||||
assert.deepEqual(role, { id: 5, name: "temp", description: "A temp role", permissions: ["core.control.connect"] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("edit-role", function() {
|
||||
describe("role edit", function() {
|
||||
it("should modify the given role", async function() {
|
||||
let args = "--name new --description \"A new role\" --permissions";
|
||||
await exec(`node clusterctl --config ${controlConfigPath} role edit temp ${args}`);
|
||||
await execCtl(`role edit temp ${args}`);
|
||||
let result = await link.messages.listRoles.send(getControl());
|
||||
let role = result.list.find(role => role.name === "new");
|
||||
assert.deepEqual(role, { id: 5, name: "new", description: "A new role", permissions: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete-role", function() {
|
||||
describe("role delete", function() {
|
||||
it("should modify the given role", async function() {
|
||||
await exec(`node clusterctl --config ${controlConfigPath} role delete new`);
|
||||
await execCtl("role delete new");
|
||||
let result = await link.messages.listRoles.send(getControl());
|
||||
let role = result.list.find(role => role.name === "new");
|
||||
assert(!role, "Role was not deleted");
|
||||
});
|
||||
});
|
||||
|
||||
describe("list-users", function() {
|
||||
describe("user list", function() {
|
||||
it("runs", async function() {
|
||||
await exec(`node clusterctl --config ${controlConfigPath} user list`);
|
||||
await execCtl("user list");
|
||||
});
|
||||
});
|
||||
|
||||
describe("create-user", function() {
|
||||
describe("user create", function() {
|
||||
it("should create the given user", async function() {
|
||||
await exec(`node clusterctl --config ${controlConfigPath} user create temp`);
|
||||
await execCtl("user create temp");
|
||||
let result = await link.messages.listUsers.send(getControl());
|
||||
let user = result.list.find(user => user.name === "temp");
|
||||
assert(user, "user was not created");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edit-user-roles", function() {
|
||||
describe("user set-roles", function() {
|
||||
it("should set the roles on the user", async function() {
|
||||
await exec(`node clusterctl --config ${controlConfigPath} 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]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete-user", function() {
|
||||
describe("user delete", function() {
|
||||
it("should delete the user", async function() {
|
||||
await exec(`node clusterctl --config ${controlConfigPath} user delete temp`);
|
||||
await execCtl("user delete temp");
|
||||
let result = await link.messages.listUsers.send(getControl());
|
||||
let user = result.list.find(user => user.name === "temp");
|
||||
assert(!user, "user was note deleted");
|
||||
|
||||
@@ -69,6 +69,11 @@ async function exec(...args) {
|
||||
return await util.promisify(child_process.exec)(...args);
|
||||
}
|
||||
|
||||
async function execCtl(...args) {
|
||||
args[0] = `node clusterctl --config ${controlConfigPath} ${args[0]}`;
|
||||
return await exec(...args);
|
||||
}
|
||||
|
||||
async function sendRcon(instanceId, command) {
|
||||
let response = await link.messages.sendRcon.send(control, { instance_id: instanceId, command });
|
||||
return response.result;
|
||||
@@ -124,7 +129,7 @@ before(async function() {
|
||||
masterProcess = await spawn("master:", `node master --config ${masterConfigPath} run`, "All plugins loaded");
|
||||
|
||||
let createArgs = `--id 4 --name slave --generate-token --output ${slaveConfigPath}`;
|
||||
await exec(`node clusterctl --config ${controlConfigPath} slave create-config ${createArgs}`);
|
||||
await execCtl(`slave create-config ${createArgs}`);
|
||||
await exec(`node slave --config ${slaveConfigPath} config set slave.instances_directory ${instancesDir}`);
|
||||
|
||||
slaveProcess = await spawn("slave:", `node slave --config ${slaveConfigPath} run`, "SOCKET | received ready from master");
|
||||
@@ -162,6 +167,7 @@ module.exports = {
|
||||
slowTest,
|
||||
get,
|
||||
exec,
|
||||
execCtl,
|
||||
sendRcon,
|
||||
getControl,
|
||||
|
||||
|
||||
+15
-6
@@ -71,13 +71,22 @@ describe("lib/users", function() {
|
||||
[2, new users.Role({ id: 2, name: "b", description: "b role", permissions: ["test"] })],
|
||||
]);
|
||||
it("should round trip serialize", function() {
|
||||
let orig = new users.User({ name: "admin", roles: [1] }, roles);
|
||||
let copy = new users.User(orig.serialize(), roles);
|
||||
assert.deepEqual(copy, orig);
|
||||
function test_roundtrip(serialized) {
|
||||
let user = new users.User(serialized, roles);
|
||||
let user_serialized = user.serialize();
|
||||
assert.deepEqual(user_serialized, serialized);
|
||||
let user_deserialized = new users.User(user_serialized, roles);
|
||||
assert.deepEqual(user_deserialized, user);
|
||||
}
|
||||
|
||||
orig = new users.User({ name: "user", roles: [2], tokenValidAfter: 12345 }, roles);
|
||||
copy = new users.User(orig.serialize(), roles);
|
||||
assert.deepEqual(copy, orig);
|
||||
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);
|
||||
assert.equal(user.roles.size, 1, "Unexpected count of roles");
|
||||
});
|
||||
it("should track online users", function() {
|
||||
let user = new users.User({ name: "admin", roles: [1] }, roles);
|
||||
|
||||
Reference in new issue
Block a user