mirror of
https://github.com/clusterio/clusterio.git
synced 2026-11-11 23: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>
168 lines
4.9 KiB
TypeScript
168 lines
4.9 KiB
TypeScript
import fs from "node:fs/promises";
|
|
import jwt from "jsonwebtoken";
|
|
|
|
import * as lib from "@clusterio/lib";
|
|
|
|
import User from "./User.js";
|
|
import UserRecord from "./UserRecord.js";
|
|
|
|
/**
|
|
* Access layer for users on the controller.
|
|
*
|
|
* In performance critical paths the raw records can be accessed directly.
|
|
* Otherwise this class will wrap data in a view class with a set and save method.
|
|
*
|
|
* @alias module:controller/src/UserManager
|
|
*/
|
|
export default class UserManager {
|
|
constructor(
|
|
public records: lib.SubscribableDatastore<UserRecord>,
|
|
private _controllerRoles: lib.SubscribableDatastore<lib.Role>,
|
|
private _controllerConfig: lib.ControllerConfig,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Prior to 2.0.0-alpha.22 users and roles were stored in a single file.
|
|
* This migration separates them so they can be loaded by datastore.
|
|
*
|
|
* @param usersFilePath The src and dst file path for the users json file
|
|
* @param rolesFilePath The dst for the roles json file
|
|
*/
|
|
static async attemptMigrateLegacyUsersFile(usersFilePath: string, rolesFilePath: string) {
|
|
try {
|
|
const combinedJson = JSON.parse(await fs.readFile(usersFilePath, { encoding: "utf8" }));
|
|
if (!("roles" in combinedJson)) {
|
|
return; // Files are already split, no action required
|
|
}
|
|
if (!("users" in combinedJson)) {
|
|
throw new Error("Legacy users json does not contain users property");
|
|
}
|
|
|
|
// We intentionally write the roles first because if it fails then the old user file will still contain them
|
|
await lib.safeOutputFile(rolesFilePath, JSON.stringify(combinedJson.roles));
|
|
await lib.safeOutputFile(usersFilePath, JSON.stringify(combinedJson.users));
|
|
} catch (err: any) {
|
|
if (err.code !== "ENOENT") {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Basic Accessors */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
has(id: UserRecord["id"]) {
|
|
return this.records.has(id);
|
|
}
|
|
|
|
getMutable(id: UserRecord["id"]) {
|
|
const userRecord = this.records.getMutable(id);
|
|
if (!userRecord) {
|
|
return undefined;
|
|
}
|
|
|
|
return User.fromUserRecord(
|
|
userRecord,
|
|
this.records,
|
|
this._controllerRoles,
|
|
);
|
|
}
|
|
|
|
get(id: UserRecord["id"]): Readonly<User> | undefined {
|
|
return this.getMutable(id);
|
|
}
|
|
|
|
hasName(name: UserRecord["name"]) {
|
|
return this.has(name.toLowerCase());
|
|
}
|
|
|
|
getByNameMutable(name: UserRecord["name"]) {
|
|
return this.getMutable(name.toLowerCase());
|
|
}
|
|
|
|
getByName(name: UserRecord["name"]) {
|
|
return this.get(name.toLowerCase());
|
|
}
|
|
|
|
* valuesMutable(): IterableIterator<User> {
|
|
const roles = this._controllerRoles;
|
|
const users = this.records;
|
|
|
|
for (const record of users.values()) {
|
|
yield User.fromUserRecord(record, users, roles);
|
|
}
|
|
}
|
|
|
|
values(): IterableIterator<Readonly<User>> {
|
|
return this.valuesMutable();
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Lifecycle */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Creates a new user and add it to the user database.
|
|
* @param name - Name of the user to create.
|
|
* @returns The created user.
|
|
*/
|
|
createUser(name: string): User {
|
|
if (this.getByName(name)) {
|
|
throw new Error(`User '${name}' already exists`);
|
|
}
|
|
|
|
let roles = new Set<number>();
|
|
let defaultRoleId = this._controllerConfig.get("controller.default_role_id");
|
|
if (defaultRoleId !== null && this._controllerRoles.has(defaultRoleId)) {
|
|
roles.add(defaultRoleId);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
/** Get or create a user with a given name. */
|
|
getOrCreateUser(name: string): User {
|
|
return this.getByNameMutable(name) ?? this.createUser(name);
|
|
}
|
|
|
|
/**
|
|
* Deletes an existing user
|
|
*
|
|
* @param user - user to delete
|
|
*/
|
|
deleteUser(user: UserRecord) {
|
|
return this.records.delete(user);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Helpers */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/**
|
|
* Sign access token for the given user
|
|
*
|
|
* @param user - user to sign token for
|
|
* @returns JWT access token for the user.
|
|
*/
|
|
signUserToken(user: UserRecord): string {
|
|
return jwt.sign(
|
|
{ aud: "user", user: user.id },
|
|
// auth secret is never null after startup
|
|
Buffer.from(this._controllerConfig.get("controller.auth_secret")!, "base64")
|
|
);
|
|
}
|
|
|
|
/** Clear all player stats for specific instance */
|
|
clearStatsOfInstance(instanceId: number) {
|
|
for (const user of this.values()) {
|
|
user.clearInstanceStats(instanceId);
|
|
}
|
|
}
|
|
}
|