mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-12 07:04:52 +09:00
Drop the special case that deferred terminating the connection issuing the delete, so deleting yourself behaves the same as revoke-token and every session of the deleted user is closed before the handler returns. Set tokenValidAfter to the current time in createUser instead of 0. Deleting a user and recreating it under the same name left tokens issued before the deletion valid, as the check is iat < tokenValidAfter. Doing it in createUser covers every path that makes a user. A token signed in the same second as creation is still accepted, so create-admin and the other production paths sign after the user exists and are unaffected. The integration harness signed the control token at import time, before bootstrap create-admin created the user. Sign it in the root before hook instead and reuse it in link.js. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
190 lines
7.0 KiB
JavaScript
190 lines
7.0 KiB
JavaScript
import path from "node:path";
|
|
import fs from "node:fs/promises";
|
|
import assert from "node:assert/strict";
|
|
import jwt from "jsonwebtoken";
|
|
import { Role, SubscribableDatastore, ControllerConfig } from "@clusterio/lib";
|
|
import { UserRecord, UserManager } from "@clusterio/controller";
|
|
|
|
const TEMP_DIR = path.join("temp", "test", "UserManager");
|
|
|
|
describe("controller/UserManager", function () {
|
|
/** @type {SubscribableDatastore<UserRecord>} */
|
|
let records;
|
|
/** @type {SubscribableDatastore<Role>} */
|
|
let roles;
|
|
/** @type {ControllerConfig} */
|
|
let config;
|
|
/** @type {UserManager} */
|
|
let userManager;
|
|
|
|
beforeEach(async function () {
|
|
records = new SubscribableDatastore();
|
|
roles = new SubscribableDatastore();
|
|
config = new ControllerConfig("controller", {
|
|
"controller.default_role_id": null,
|
|
"controller.auth_secret": Buffer.from("mysecret").toString("base64"),
|
|
});
|
|
userManager = new UserManager(records, roles, config);
|
|
await fs.rm(TEMP_DIR, { force: true, recursive: true, maxRetries: 10 });
|
|
await fs.mkdir(TEMP_DIR, { recursive: true });
|
|
});
|
|
|
|
describe(".has() / .get() / .getMutable() / .hasName() / .getByName() / .getByNameMutable()", function () {
|
|
it("should return user when exists", function () {
|
|
const user = userManager.createUser("Alice");
|
|
assert.deepEqual(userManager.has(user.id), true);
|
|
assert.deepEqual(userManager.get(user.id), user);
|
|
assert.deepEqual(userManager.getMutable(user.id), user);
|
|
assert.deepEqual(userManager.hasName("Alice"), true);
|
|
assert.deepEqual(userManager.getByName("Alice"), user);
|
|
assert.deepEqual(userManager.getByNameMutable("Alice"), user);
|
|
});
|
|
|
|
it("should return undefined when user does not exist", function () {
|
|
assert.equal(userManager.has("nonexistent"), false);
|
|
assert.equal(userManager.get("nonexistent"), undefined);
|
|
assert.equal(userManager.hasName("nonexistent"), false);
|
|
assert.equal(userManager.getByName("nonexistent"), undefined);
|
|
});
|
|
});
|
|
|
|
describe(".values() / .valuesMutable()", function () {
|
|
it("should iterate all users", function () {
|
|
const u1 = userManager.createUser("Alice");
|
|
const u2 = userManager.createUser("Bob");
|
|
|
|
const values = Array.from(userManager.values());
|
|
const valuesMutable = Array.from(userManager.valuesMutable());
|
|
|
|
assert.deepEqual(values, [u1, u2]);
|
|
assert.deepEqual(valuesMutable, [u1, u2]);
|
|
});
|
|
});
|
|
|
|
describe(".createUser()", function () {
|
|
it("should create and store a new user", function () {
|
|
const user = userManager.createUser("Charlie");
|
|
assert.deepEqual(userManager.getByName("Charlie"), user);
|
|
assert.deepEqual(records.getMutable(user.id).toJSON(), user.toJSON());
|
|
});
|
|
|
|
it("should throw if user already exists", function () {
|
|
userManager.createUser("Dave");
|
|
assert.throws(() => userManager.createUser("Dave"), /already exists/);
|
|
});
|
|
|
|
it("should assign default role if configured and exists", function () {
|
|
roles.set(new Role(1, "admin"));
|
|
config.set("controller.default_role_id", 1);
|
|
const user = userManager.createUser("Eve");
|
|
assert(user.roleIds.has(1));
|
|
});
|
|
|
|
it("should set tokenValidAfter to the current time", function () {
|
|
const before = Math.floor(Date.now() / 1000);
|
|
const user = userManager.createUser("Mallory");
|
|
assert(user.tokenValidAfter >= before, "tokenValidAfter predates the call");
|
|
assert(user.tokenValidAfter <= Math.floor(Date.now() / 1000), "tokenValidAfter is in the future");
|
|
});
|
|
|
|
it("should invalidate tokens issued before a user of the same name was recreated", function () {
|
|
const user = userManager.createUser("Niaj");
|
|
const oldTokenIat = user.tokenValidAfter - 60;
|
|
userManager.deleteUser(user);
|
|
|
|
// WsServer.registerControl rejects the token when iat < tokenValidAfter
|
|
const recreated = userManager.createUser("Niaj");
|
|
assert(oldTokenIat < recreated.tokenValidAfter, "token from before the deletion is still valid");
|
|
});
|
|
});
|
|
|
|
describe(".getOrCreateUser()", function () {
|
|
it("should return existing user", function () {
|
|
const existing = userManager.createUser("Frank");
|
|
const found = userManager.getOrCreateUser("Frank");
|
|
assert.deepEqual(found, existing);
|
|
});
|
|
|
|
it("should create user if missing", function () {
|
|
const found = userManager.getOrCreateUser("Grace");
|
|
assert(userManager.getByName("Grace"));
|
|
assert.deepEqual(found, userManager.getByName("Grace"));
|
|
});
|
|
});
|
|
|
|
describe(".deleteUser()", function () {
|
|
it("should delete user from records", function () {
|
|
const user = userManager.createUser("Heidi");
|
|
assert(userManager.getByName("Heidi"));
|
|
userManager.deleteUser(user);
|
|
assert.equal(userManager.getByName("Heidi"), undefined);
|
|
});
|
|
});
|
|
|
|
describe(".signUserToken()", function () {
|
|
it("should return a valid JWT for user", function () {
|
|
const user = userManager.createUser("Ivan");
|
|
const token = userManager.signUserToken(user);
|
|
const payload = jwt.verify(
|
|
token,
|
|
Buffer.from(config.get("controller.auth_secret"), "base64")
|
|
);
|
|
const { iat, ...rest } = payload;
|
|
assert.equal(typeof iat, "number");
|
|
assert.deepEqual(rest, { aud: "user", user: user.id });
|
|
});
|
|
});
|
|
|
|
describe(".clearStatsOfInstance()", function () {
|
|
it("should clear instance stats for all users", function () {
|
|
const instanceId = 42;
|
|
|
|
records.set(UserRecord.fromJSON({ name: "Jack", instance_stats: [[instanceId, { join_count: 1 }]] }));
|
|
records.set(UserRecord.fromJSON({ name: "Jill", instance_stats: [[instanceId, { join_count: 1 }]] }));
|
|
|
|
userManager.clearStatsOfInstance(instanceId);
|
|
|
|
for (const user of userManager.values()) {
|
|
assert(!user.instanceStats.has(instanceId), `${user.id} still has instanceStats for ${instanceId}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("static attemptMigrateLegacyUsersFile()", function () {
|
|
const usersFile = path.join(TEMP_DIR, "users.json");
|
|
const rolesFile = path.join(TEMP_DIR, "roles.json");
|
|
|
|
it("should do nothing if file does not exist", async function () {
|
|
await UserManager.attemptMigrateLegacyUsersFile(usersFile, rolesFile);
|
|
});
|
|
|
|
it("should do nothing if roles key missing", async function () {
|
|
await fs.writeFile(usersFile, JSON.stringify({ users: [] }));
|
|
await UserManager.attemptMigrateLegacyUsersFile(usersFile, rolesFile);
|
|
const content = JSON.parse(await fs.readFile(usersFile));
|
|
assert.deepEqual(content, { users: [] });
|
|
});
|
|
|
|
it("should throw if users key missing in legacy file", async function () {
|
|
await fs.writeFile(usersFile, JSON.stringify({ roles: [] }));
|
|
await assert.rejects(
|
|
() => UserManager.attemptMigrateLegacyUsersFile(usersFile, rolesFile),
|
|
/Error: Legacy users json does not contain users property/
|
|
);
|
|
});
|
|
|
|
it("should migrate valid legacy file", async function () {
|
|
const legacy = { roles: [{ id: 1 }, { id: 2 }], users: [{ name: "Karl" }] };
|
|
await fs.writeFile(usersFile, JSON.stringify(legacy));
|
|
|
|
await UserManager.attemptMigrateLegacyUsersFile(usersFile, rolesFile);
|
|
|
|
const writtenRoles = JSON.parse(await fs.readFile(rolesFile));
|
|
const writtenUsers = JSON.parse(await fs.readFile(usersFile));
|
|
|
|
assert.deepEqual(writtenRoles, [{ id: 1 }, { id: 2 }]);
|
|
assert.deepEqual(writtenUsers, [{ name: "Karl" }]);
|
|
});
|
|
});
|
|
});
|