Merge pull request #313 from Hornwitser/list-management

List management
This commit is contained in:
Hornwitser authored and GitHub committed 2020-09-16 15:08:21 +02:00
commit 6fde461da5
13 files changed
+732 -59

No files matched your search

+2 -1
View File
@@ -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",
+2
View File
@@ -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
+46
View File
@@ -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" });
+132
View File
@@ -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.
+24
View File
@@ -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();
/**
+4
View File
@@ -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,
+91
View File
@@ -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
View File
@@ -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",
+90 -2
View File
@@ -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") {
+165 -1
View File
@@ -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);
+85 -41
View File
@@ -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");
+7 -1
View File
@@ -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
View File
@@ -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);