mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 15:14:52 +09:00
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.
This commit is contained in:
1 parent
f08774bd4a
commit
84450b83b4
11 files changed
+666
-11
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",
|
||||
|
||||
@@ -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"],
|
||||
|
||||
+65
-6
@@ -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);
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user