Files
factorio-clusterio/packages/lib/src/permissions.ts
T
bbassieandClaude Fable 5.1 87aa7b3b4c Require a token to scrape /metrics
The metrics endpoint fans out a gather to every connected host, so
anyone who could reach the controller could read cluster statistics and
put load on all hosts at will. Scraping now needs a user token holding
the new core.controller.metrics permission. The token is accepted via
Authorization: Bearer as well as X-Access-Token because Prometheus can
only send the former. Concurrent scrapes share one gather so hosts see
at most one metrics request at a time.

The three connection gauges in controller.ts now no-op before the
controller exists, like the other gauges there, so the endpoint can be
tested against the mock controller.

Closes #1009

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 22:28:43 +00:00

596 lines
16 KiB
TypeScript

/**
* User and permissions library
*
* Defines data types for users, roles and permissions.
*
* @author Hornwitser
* @module lib/users
*/
import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "./plugin.js";
import { Permission } from "./data/index.js";
export const permissions = new Map<string, Permission>();
/**
* Registry of known permission names.
*
* Plugins add their permissions with module augmentation:
*
* ```ts
* declare module "@clusterio/lib" {
* export interface Permissions {
* "foo_frobber.frobnicate": never;
* }
* }
* ```
*
* Only the keys matter, the values are always never.
*/
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface Permissions extends Record<(typeof corePermissions)[number]["name"], never> {}
/** Name of a permission known at compile time, see {@link Permissions}. */
export type PermissionName = keyof Permissions;
/** Definition of a permission, see {@link definePermission}. */
export type PermissionDefinition = {
/** The internal name for the permission. */
name: PermissionName,
/** User facing name for the permission. */
title: string,
/** User facing description. */
description: string,
/** If true this permission is granted by the generated Player role by default. */
grantByDefault?: boolean,
};
/**
* Define a new user permission for Clusterio
*
* Defines a permission for use in Clusterio. Defined permissions can be
* granted to roles, and checked on users. Name should be `lower_case`
* style and start with the plugin name followed by a dot.
*
* Plugins should prefer declaring permissions in the `permissions` array
* of their {@link PluginDeclaration}, which is registered by
* {@link registerPluginPermissions}.
*
* @param def - The definition for the permission.
*/
export function definePermission({
name,
title,
description,
grantByDefault = false,
}: PermissionDefinition) {
if (typeof name !== "string" || !name.length) {
throw new Error("Expected name to be a non-empty string");
}
if (typeof title !== "string" || !title.length) {
throw new Error("Expected title to be a non-empty string");
}
if (typeof description !== "string" || !description.length) {
throw new Error("Expected description to be a non-empty string");
}
if (typeof grantByDefault !== "boolean") {
throw new Error("Expected grantByDefault to be a boolean");
}
if (permissions.has(name)) {
throw new Error(`Permission '${name}' is already defined`);
}
permissions.set(name, new Permission(name, title, description, grantByDefault));
}
/**
* Define all permissions declared by the provided plugin infos
*
* @param pluginInfos - Array of plugin info objects.
*/
export function registerPluginPermissions(pluginInfos: PluginNodeEnvInfo[] | PluginWebpackEnvInfo[]) {
for (const pluginInfo of pluginInfos) {
for (const permission of pluginInfo.permissions ?? []) {
if (typeof permission.name !== "string" || !permission.name.startsWith(`${pluginInfo.name}.`)) {
throw new Error(
`Expected name of permission '${permission.name}' for ${pluginInfo.name} ` +
`to start with '${pluginInfo.name}.'`
);
}
definePermission(permission);
}
}
}
// Definitions for the built in permissions used in Clusterio.
// description should answer "this permission allows you to ___"
export const corePermissions = [
{
name: "core.admin",
title: "Administrator",
description: "Bypass all permission checks.",
},
{
name: "core.control.connect",
title: "Connect to WebSocket",
description: "Connect to the controller's WebSocket control interface.",
grantByDefault: true,
},
{
name: "core.controller.stop",
title: "Stop controller",
description:
"Stop the Node.js controller process making the cluster inoperable until someone with access to the " +
"system it runs on manually starts it again.",
},
{
name: "core.controller.restart",
title: "Restart controller",
description: "Restart the Node.js controller process if the system is set up for restarting.",
},
{
name: "core.controller.update",
title: "Update controller",
description: "Remotely update the controller if the controller allows for remote updates.",
},
{
name: "core.controller.get_config",
title: "Get controller config",
description: "Get the config of controller.",
},
{
name: "core.controller.update_config",
title: "Modify controller config",
description: "Modify the controller config or entries of the controller config.",
},
{
name: "core.controller.metrics",
title: "Scrape metrics",
description: "Fetch the Prometheus metrics for the cluster from the controller's /metrics endpoint.",
},
{
name: "core.system.subscribe",
title: "Subscribe to system updates",
description:
"Subscribe to be notified when the system info detailing system specific information along with metrics " +
"such as cpu, memory and disk usage for the controller and hosts are updated.",
},
{
name: "core.host.stop",
title: "Stop Hosts",
description:
"Stop Node.js host processes making the host inaccessible until someone with access to the system " +
"it runs on manually starts it again.",
},
{
name: "core.host.restart",
title: "Restart Hosts",
description: "Restart Node.js host processes if the system they run on are set up for restarting.",
},
{
name: "core.host.update",
title: "Update Hosts",
description: "Remotely update a host if the host allows for remote updates.",
},
{
name: "core.host.get_config",
title: "Get host configs",
description: "Get the config of hosts.",
},
{
name: "core.host.update_config",
title: "Modify host configs",
description: "Modify the host config or entries of the host config.",
},
{
name: "core.host.list",
title: "List hosts",
description: "Get the full list of hosts in the cluster.",
grantByDefault: true,
},
{
name: "core.host.subscribe",
title: "Subscribe to host updates",
description: "Subscribe to be notified on updates on the details of hosts.",
grantByDefault: true,
},
{
name: "core.host.generate_token",
title: "Generate host token",
description: "Generate tokens for hosts to connect to the cluster with.",
},
{
name: "core.host.revoke_token",
title: "Revoke host token",
description: "Revoke access token and terminate existing connection for hosts.",
},
{
name: "core.host.create_config",
title: "Create host config",
description: "Create host configs via the controller.",
},
{
name: "core.instance.get",
title: "Get instance",
description: "Get the details of an instance in the cluster.",
grantByDefault: true,
},
{
name: "core.instance.list",
title: "List instances",
description: "Get the full list of instances in the cluster.",
grantByDefault: true,
},
{
name: "core.instance.subscribe",
title: "Subscribe to instance updates",
description: "Subscribe to be notified on updates on the details and status of instances.",
grantByDefault: true,
},
{
name: "core.instance.create",
title: "Create instance",
description: "Create new instances on the controller.",
},
{
name: "core.instance.get_config",
title: "Get instance config",
description: "Get the config of instances.",
},
{
name: "core.instance.update_config",
title: "Modify instance config",
description: "Modify the config or entries of the config for instances.",
},
{
name: "core.instance.assign",
title: "Assign instance",
description: "Assign or reassign instances to a host.",
},
{
name: "core.instance.save.list",
title: "List saves",
description: "List the saves currently on instances.",
},
{
name: "core.instance.save.subscribe",
title: "Subscribe to save updates",
description: "Subscribe to be notifed on updates to the list of saves of instances.",
},
{
name: "core.instance.save.create",
title: "Create new save",
description: "Create new savegames on instances.",
},
{
name: "core.instance.save.rename",
title: "Rename save",
description: "Rename savegames on instances.",
},
{
name: "core.instance.save.copy",
title: "Copy save",
description: "Create copies of savegames on instances.",
},
{
name: "core.instance.save.delete",
title: "Delete save",
description: "Delete savegames on instances.",
},
{
name: "core.instance.save.upload",
title: "Upload save",
description: "Upload savegames to instances.",
},
{
name: "core.instance.save.transfer",
title: "Transfer save",
description: "Transfer savegames between instances.",
},
{
name: "core.instance.save.download",
title: "Download save",
description: "Download savegames from instances.",
},
{
name: "core.instance.export_data",
title: "Export locale and icons",
description: "Export the the locale and icons from an instance and upload it to the controller.",
},
{
name: "core.instance.extract_players",
title: "Extract player stats from running save",
description:
"Run extraction to create a user for each player in the save and set the online time from the save.",
},
{
name: "core.instance.start",
title: "Start instance",
description: "Start instances.",
},
{
name: "core.instance.restart",
title: "Restart instance",
description: "Restart instances.",
},
{
name: "core.instance.load_scenario",
title: "Load scenario",
description: "Start instances by loading a scenario.",
},
{
name: "core.instance.stop",
title: "Stop instance",
description: "Stop instances.",
},
{
name: "core.instance.kill",
title: "Kill instance",
description: "Terminate running instances without saving or cleanup.",
},
{
name: "core.instance.delete",
title: "Delete instance",
description: "Delete instances (includes config and all files stored like saves and logs).",
},
{
name: "core.instance.send_rcon",
title: "Send RCON",
description: "Send arbitrary RCON commands to instances.",
},
{
name: "core.mod_pack.get",
title: "Retrieve mod pack",
description: "Get the details of a mod pack in the cluster.",
grantByDefault: true,
},
{
name: "core.mod_pack.list",
title: "List mod packs",
description: "Get the full list of a mod packs in the cluster.",
grantByDefault: true,
},
{
name: "core.mod_pack.subscribe",
title: "Subscribe to mod pack updates",
description: "Subscribe to be notified on updates on the details of mod packs.",
grantByDefault: true,
},
{
name: "core.mod_pack.create",
title: "Create mod pack",
description: "Create new mod pack in the cluster.",
},
{
name: "core.mod_pack.update",
title: "Update mod pack",
description: "Edit existing mod pack definition in the cluster.",
},
{
name: "core.mod_pack.delete",
title: "Delete mod pack",
description: "Delete mod pack definition in the cluster.",
},
{
name: "core.mod.get",
title: "Get mods",
description: "Get the details of a mod stored in the cluster.",
grantByDefault: true,
},
{
name: "core.mod.list",
title: "List mods",
description: "Get the full list of mods stored on the controller.",
grantByDefault: true,
},
{
name: "core.mod.search",
title: "Search mods",
description: "Search through the list of mods stored on the controller.",
grantByDefault: true,
},
{
name: "core.mod.search_portal",
title: "Search mod portal",
description: "Search for mods on the Factorio mod portal.",
grantByDefault: true,
},
{
name: "core.mod.subscribe",
title: "Subscribe to mod updates",
description: "Subscribe to be notified on updates on the details of mods.",
grantByDefault: true,
},
{
name: "core.mod.upload",
title: "Upload mods",
description: "Upload mods to the controller.",
},
{
name: "core.mod.download",
title: "Download mod",
description: "Download mods stored on the controller.",
grantByDefault: true,
},
{
name: "core.mod.delete",
title: "Delete mod",
description: "Delete mods stored on the controller.",
},
{
name: "core.mod.download_from_portal",
title: "Download mods from portal",
description: "Allow downloading mods directly from the Factorio mod portal to the controller.",
grantByDefault: true,
},
{
name: "core.permission.list",
title: "List permissions",
description: "Get the full list of permissions in the cluster.",
grantByDefault: true,
},
{
name: "core.role.list",
title: "List roles",
description: "Get the full list of roles and their permissions in the cluster.",
grantByDefault: true,
},
{
name: "core.role.subscribe",
title: "Subscribe to role updates",
description: "Subscribe to be notified on updates on the details of roles.",
grantByDefault: true,
},
{
name: "core.role.create",
title: "Create role",
description: "Create new roles with permissions.",
},
{
name: "core.role.update",
title: "Update role",
description: "Edit existing roles including permissions they grant.",
},
{
name: "core.role.delete",
title: "Delete role",
description: "Permanently delete roles.",
},
{
name: "core.user.get",
title: "Get user",
description: "Get the details of a user in the cluster.",
grantByDefault: true,
},
{
name: "core.user.list",
title: "List users",
description: "Get the full list of users in the cluster.",
grantByDefault: true,
},
{
name: "core.user.subscribe",
title: "Subscribe to user updates",
description: "Subscribe to be notified on updates on the details and status of users.",
grantByDefault: true,
},
{
name: "core.user.create",
title: "Create users",
description: "Create user acounts with a given name.",
},
{
name: "core.user.revoke_token",
title: "Revoke user token",
description: "Revoke access token and terminate all existing control connections for oneself.",
grantByDefault: true,
},
{
name: "core.user.revoke_other_token",
title: "Revoke other user's token",
description: "Allow revoking tokens for other users than oneself. Requires Revoke user token.",
},
{
name: "core.user.update_roles",
title: "Update user roles",
description: "Add or remove any role to any user.",
},
{
name: "core.user.set_admin",
title: "Set user admin status",
description: "Promote or demote any user to Factorio admin.",
},
{
name: "core.user.set_banned",
title: "Set user ban status",
description: "Ban or unban any user.",
},
{
name: "core.user.set_whitelisted",
title: "Set user whitelist status",
description: "Add or remove any user to/from the whitelist.",
},
{
name: "core.user.delete",
title: "Delete user",
description: "Delete users and all data stored for them.",
},
{
name: "core.user.bulk_import",
title: "Bulk user import",
description: "Bulk import users including admin, whitelist, and bans." +
" (Imports types are restricted by other permissions, e.g. core.user.set_admin)",
},
{
name: "core.user.bulk_export",
title: "Bulk user export",
description: "Bulk export users including admin, whitelist, and bans.",
},
{
name: "core.user.bulk_restore",
title: "Bulk user restore",
description: "Bulk restore users including admin, whitelist, and bans." +
" (Restore types are restricted by other permissions, e.g. core.user.set_admin)",
},
{
name: "core.log.follow",
title: "Follow cluster log",
description: "Receive new entries in the cluster log. Required to see instance console.",
},
{
name: "core.log.query",
title: "Query cluster log",
description: "Query past entries in the cluster log. Required to see past entries in instance console.",
},
{
name: "core.plugin.list",
title: "List plugins",
description: "List all installed plugins on a machine.",
},
{
name: "core.plugin.update",
title: "Update plugin",
description: "Remotely update a plugin if the target allows remote updates of plugins.",
},
{
name: "core.plugin.install",
title: "Install plugin",
description: "Remotely install a plugin if the target allows remote installs of plugins.",
},
{
name: "core.debug.dump_ws",
title: "Dump WebSocket",
description: "Dump all WebSocket communicatation from the controller.",
},
{
name: "core.external.get_factorio_versions",
title: "Get factorio versions",
description: "Get list of all factorio versions from Wube",
},
{
name: "core.external.get_latest_releases",
title: "Get factorio releases",
description: "Get the latest stable and experimental factorio releases from Wube",
},
] as const satisfies readonly {
name: string,
title: string,
description: string,
grantByDefault?: boolean,
}[];
for (const permission of corePermissions) {
definePermission(permission);
}