mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-13 23:14:53 +09:00
Terminate deleted user sessions immediately
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>
This commit is contained in:
1 parent
fb71ee7282
commit
1da7c1da67
6 files changed
+30
-16
No files matched your search
@@ -1071,13 +1071,7 @@ export default class ControlConnection extends BaseConnection {
|
||||
|
||||
this._controller.users.deleteUser(user);
|
||||
for (let controlConnection of this._controller.wsServer.controlConnections.values()) {
|
||||
if (controlConnection.user.id !== user.id) {
|
||||
continue;
|
||||
}
|
||||
if (controlConnection === this) {
|
||||
// Let the response go out before dropping our own session
|
||||
setImmediate(() => controlConnection.connector.terminate());
|
||||
} else {
|
||||
if (controlConnection.user.id === user.id) {
|
||||
controlConnection.connector.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,9 @@ export default class UserManager {
|
||||
roles.add(defaultRoleId);
|
||||
}
|
||||
|
||||
const user = new User(this.records, this._controllerRoles, 0, name, roles);
|
||||
// Reject tokens issued before the user existed, e.g. for a previous user by the same name
|
||||
const tokenValidAfter = Math.floor(Date.now() / 1000);
|
||||
const user = new User(this.records, this._controllerRoles, tokenValidAfter, name, roles);
|
||||
this.records.set(user);
|
||||
return user;
|
||||
}
|
||||
|
||||
@@ -135,12 +135,10 @@ describe("controller/src/ControlConnection", function() {
|
||||
assert.equal(admin.connector.terminated, false);
|
||||
});
|
||||
|
||||
it("terminates its own connection after the handler returns", async function() {
|
||||
it("terminates its own connection when deleting itself", async function() {
|
||||
const player = connect("player");
|
||||
|
||||
await deleteUser(player, "player");
|
||||
assert.equal(player.connector.terminated, false);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(player.connector.terminated, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,6 +79,23 @@ describe("controller/UserManager", function () {
|
||||
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 () {
|
||||
|
||||
@@ -191,7 +191,8 @@ let haveFactorioInstall;
|
||||
const baseHostConfig = loadJSON("config-host.json");
|
||||
|
||||
export let url = "https://localhost:4443/";
|
||||
export let controlToken = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64"));
|
||||
// Signed in the root before hook, as the user has to exist before the token is issued
|
||||
export let controlToken;
|
||||
export let instancesDir = path.join("temp", "test", "instances");
|
||||
export let modsDir = path.join("temp", "test", "mods");
|
||||
export let databaseDir = path.join("temp", "test", "database");
|
||||
@@ -373,6 +374,7 @@ before(async function() {
|
||||
console.log("Bootstrapping");
|
||||
await execController("bootstrap create-admin test");
|
||||
await execController("bootstrap create-ctl-config test");
|
||||
controlToken = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64"));
|
||||
|
||||
controllerProcess = await spawnNode("controller:", "../../packages/controller run", /Started controller/);
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ import assert from "node:assert/strict";
|
||||
import events from "node:events";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import { TestControlConnector, TestHostConnector, TestControl, get, exec, url, slowTest } from "./index.js";
|
||||
import {
|
||||
TestControlConnector, TestHostConnector, TestControl, get, exec, url, controlToken, slowTest,
|
||||
} from "./index.js";
|
||||
import { ConnectionClosed, ProtocolError, PolicyViolation, AuthenticationFailed } from "@clusterio/lib";
|
||||
|
||||
let token = jwt.sign({ aud: "user", user: "test" }, Buffer.from("TestSecretDoNotUse", "base64"));
|
||||
let tokenHost = jwt.sign({ aud: "host", host: 0 }, Buffer.from("TestSecretDoNotUse", "base64"));
|
||||
|
||||
describe("Integration of lib/link", function() {
|
||||
@@ -14,7 +15,7 @@ describe("Integration of lib/link", function() {
|
||||
let hostConnector;
|
||||
beforeEach(async function() {
|
||||
controlConnector = new TestControlConnector(url, 0.2);
|
||||
controlConnector.token = token;
|
||||
controlConnector.token = controlToken;
|
||||
control = new TestControl(controlConnector, false);
|
||||
hostConnector = new TestHostConnector(url, 0.2);
|
||||
hostConnector.token = tokenHost;
|
||||
@@ -100,7 +101,7 @@ describe("Integration of lib/link", function() {
|
||||
});
|
||||
|
||||
it("should refuse connection with incorrect audience", async function() {
|
||||
hostConnector.token = token;
|
||||
hostConnector.token = controlToken;
|
||||
await assert.rejects(
|
||||
hostConnector.connect(),
|
||||
new Error("Authentication failed: jwt audience invalid. expected: host or slave")
|
||||
|
||||
Reference in new issue
Block a user