From 8c03ac15abdbd39a6b2f46744a7573a24248ee62 Mon Sep 17 00:00:00 2001 From: Bastiaan <17990055+bbassie@users.noreply.github.com> Date: Sat, 12 Sep 2026 00:28:30 +0200 Subject: [PATCH] Define permissions on PluginDeclaration (#988) Adds a permissions array to PluginDeclaration, a type-level Permissions registry that plugins extend with module augmentation, and registerPluginPermissions() called from all entrypoints. checkPermission and the web UI hasPermission helpers now take PermissionName so typos are compile errors. Core definitions move into a corePermissions array that the interface derives its keys from. Closes #987 Co-authored-by: Claude Fable 5.1 Co-authored-by: Cooldude2606 <25043174+Cooldude2606@users.noreply.github.com> --- docs/writing-plugins.md | 44 + packages/controller/controller.ts | 1 + packages/controller/src/ControlConnection.ts | 2 +- packages/controller/src/User.ts | 4 +- packages/create/templates/plugin-js/index.js | 69 +- .../create/templates/plugin-js/web/plugin.jsx | 2 +- packages/create/templates/plugin-ts/index.ts | 89 +- .../templates/plugin-ts/tsconfig.browser.json | 2 +- .../create/templates/plugin-ts/web/plugin.tsx | 2 +- packages/ctl/ctl.ts | 1 + packages/host/host.ts | 1 + packages/lib/src/data/UserDetails.ts | 4 +- packages/lib/src/permissions.ts | 1004 +++++++++-------- packages/lib/src/plugin.ts | 3 +- packages/lib/src/subscriptions.ts | 3 +- packages/web_ui/src/BaseWebPlugin.ts | 10 +- packages/web_ui/src/bootstrap.tsx | 1 + packages/web_ui/src/model/account.ts | 10 +- plugins/inventory_sync/index.ts | 18 +- plugins/inventory_sync/tsconfig.browser.json | 2 +- plugins/subspace_storage/index.ts | 18 +- .../subspace_storage/tsconfig.browser.json | 2 +- test/lib/users.js | 41 + 23 files changed, 758 insertions(+), 575 deletions(-) diff --git a/docs/writing-plugins.md b/docs/writing-plugins.md index f86e9582..84c4e983 100644 --- a/docs/writing-plugins.md +++ b/docs/writing-plugins.md @@ -10,6 +10,7 @@ The plugin classes have pre-defined hooks that are called during various stages - [Defining the plugin class](#defining-the-plugin-class) - [Logging Messages](#logging-messages) - [Plugin Configuration](#plugin-configuration) +- [Plugin Permissions](#plugin-permissions) - [Communicating with Factorio](#communicating-with-factorio) - [Defining Link Messages](#defining-link-messages) - [Sending Link Messages](#sending-link-messages) @@ -101,6 +102,10 @@ The following properties are recognized: Object with link messages definitions for this plugin. See guide for [defining link messages](#defining-link-messages) below. +**permissions**: + Array of permission definitions for this plugin. + See [Plugin Permissions](#plugin-permissions) + The optional module folder contains a Clusterio module that will be patched into the save when the plugin is loaded. See the section on [Clusterio Modules](developing-for-clusterio.md) in the Developing for Clusterio document. The only restriction imposed on modules embedded into plugins is that they must be named the same as the plugin. @@ -249,6 +254,45 @@ async init() { ``` +## Plugin Permissions + +Permissions are granted to roles and checked on users, both on the controller and in the web UI. +A plugin declares the permissions it uses under `permissions` in the `plugin` export. +Names must start with the plugin name followed by a dot. +For example in index.ts: + +```ts +import type * as lib from "@clusterio/lib"; + +declare module "@clusterio/lib" { + // Extend the interface of known permission names so that + // user.checkPermission(...) accepts our permission. + export interface Permissions { + "foo_frobber.frobnicate": never; + } +} + +export default { + ... + permissions: [ + { + name: "foo_frobber.frobnicate", + title: "Frobnicate", + description: "Run frobnication on instances.", + grantByDefault: false, // Whether the generated Player role is granted this permission + }, + ], +} satisfies lib.PluginDeclaration; +``` + +The `Permissions` interface only exists at the type level, its values are always `never` and only the keys are used. +Adding your permission names to it gives you autocomplete and a compile error on typos when calling `user.checkPermission`, `account.hasPermission` or setting the `permission` of a web UI page. +In JavaScript the `declare module` block is not needed. + +Permissions declared this way are registered when the plugin is loaded. +The older `lib.definePermission()` function still works, but a plugin should not use both for the same permission name. + + ## Communicating with Factorio For pushing data into Factorio there's RCON, which lets you send arbitrary Lua commands to invoke whatever code you want in the game. diff --git a/packages/controller/controller.ts b/packages/controller/controller.ts index 4c344ba9..2551aee0 100644 --- a/packages/controller/controller.ts +++ b/packages/controller/controller.ts @@ -402,6 +402,7 @@ async function initialize(): Promise { logger.info("Loading Plugin info"); const pluginInfos = await lib.loadPluginInfos(pluginList); lib.registerPluginMessages(pluginInfos); + lib.registerPluginPermissions(pluginInfos); lib.addPluginConfigFields(pluginInfos); const controllerConfigPath = args.config; diff --git a/packages/controller/src/ControlConnection.ts b/packages/controller/src/ControlConnection.ts index 63e2d667..cf110b7a 100644 --- a/packages/controller/src/ControlConnection.ts +++ b/packages/controller/src/ControlConnection.ts @@ -169,7 +169,7 @@ export default class ControlConnection extends BaseConnection { } if (typeof permission === "string") { - this.user.checkPermission(permission); + this.user.checkPermission(permission as lib.PermissionName); return; } diff --git a/packages/controller/src/User.ts b/packages/controller/src/User.ts index c4fe115f..c7d50147 100644 --- a/packages/controller/src/User.ts +++ b/packages/controller/src/User.ts @@ -1,7 +1,7 @@ import { IUser, Role, SubscribableDatastore, - PermissionError, permissions, + PermissionError, PermissionName, permissions, } from "@clusterio/lib"; import UserRecord from "./UserRecord.js"; @@ -126,7 +126,7 @@ export default class User extends UserRecord implements IUser { this.saveRecord(); } - checkPermission(permission: string) { + checkPermission(permission: PermissionName) { if (!permissions.has(permission)) { throw new Error(`permission ${permission} does not exist`); } diff --git a/packages/create/templates/plugin-js/index.js b/packages/create/templates/plugin-js/index.js index 932b460d..8ac2056f 100644 --- a/packages/create/templates/plugin-js/index.js +++ b/packages/create/templates/plugin-js/index.js @@ -1,38 +1,6 @@ import * as lib from "@clusterio/lib"; //%if multi_context import Messages from "./messages.js"; - -lib.definePermission({ - name: "__plugin_name__.example.permission.event", - title: "Example permission event", - description: "Example Description. Event. Change me in index.js", - grantByDefault: true, -}); - -lib.definePermission({ - name: "__plugin_name__.example.permission.request", - title: "Example permission request", - description: "Example Description. Request. Change me in index.js", - grantByDefault: true, -}); -//%endif -//%if controller & web // Subscribing requires web content and the controller - -lib.definePermission({ - name: "__plugin_name__.example.permission.subscribe", - title: "Example permission subscribe", - description: "Example Description. Subscribe. Change me in index.js", - grantByDefault: true, -}); -//%endif -//%if web - -lib.definePermission({ - name: "__plugin_name__.page.view", - title: "Example page view permission", - description: "Example Description. View. Change me in index.js", - grantByDefault: true, -}); //%endif export const plugin = { @@ -115,6 +83,43 @@ export const plugin = { //%if multi_context // Subscribing requires multi context ], //%endif +//%if multi_context | web + + permissions: [ +//%endif +//%if multi_context + { + name: "__plugin_name__.example.permission.event", + title: "Example permission event", + description: "Example Description. Event. Change me in index.js", + grantByDefault: true, + }, + { + name: "__plugin_name__.example.permission.request", + title: "Example permission request", + description: "Example Description. Request. Change me in index.js", + grantByDefault: true, + }, +//%endif +//%if controller & web // Subscribing requires web content and the controller + { + name: "__plugin_name__.example.permission.subscribe", + title: "Example permission subscribe", + description: "Example Description. Subscribe. Change me in index.js", + grantByDefault: true, + }, +//%endif +//%if web + { + name: "__plugin_name__.page.view", + title: "Example page view permission", + description: "Example Description. View. Change me in index.js", + grantByDefault: true, + }, +//%endif +//%if multi_context | web + ], +//%endif //%if web // Web content template has an example route which is the plugin name webEntrypoint: "./web", diff --git a/packages/create/templates/plugin-js/web/plugin.jsx b/packages/create/templates/plugin-js/web/plugin.jsx index 48f90cf9..69ac6364 100644 --- a/packages/create/templates/plugin-js/web/plugin.jsx +++ b/packages/create/templates/plugin-js/web/plugin.jsx @@ -57,7 +57,7 @@ export class WebPlugin extends BaseWebPlugin { permission: "__plugin_name__.example.permission.subscribe", //%endif //%if !controller - permission: "__plugin_name__.example.permission.view", + permission: "__plugin_name__.page.view", //%endif content: , }, diff --git a/packages/create/templates/plugin-ts/index.ts b/packages/create/templates/plugin-ts/index.ts index 8027808b..f35b29e4 100644 --- a/packages/create/templates/plugin-ts/index.ts +++ b/packages/create/templates/plugin-ts/index.ts @@ -1,40 +1,8 @@ import * as lib from "@clusterio/lib"; //%if multi_context import * as Messages from "./messages.js"; - -lib.definePermission({ - name: "__plugin_name__.example.permission.event", - title: "Example permission event", - description: "Example Description. Event. Change me in index.ts", - grantByDefault: true, -}); - -lib.definePermission({ - name: "__plugin_name__.example.permission.request", - title: "Example permission request", - description: "Example Description. Request. Change me in index.ts", - grantByDefault: true, -}); //%endif -//%if controller & web // Subscribing requires web content and the controller - -lib.definePermission({ - name: "__plugin_name__.example.permission.subscribe", - title: "Example permission subscribe", - description: "Example Description. Subscribe. Change me in index.ts", - grantByDefault: true, -}); -//%endif -//%if web - -lib.definePermission({ - name: "__plugin_name__.page.view", - title: "Example page view permission", - description: "Example Description. View. Change me in index.ts", - grantByDefault: true, -}); -//%endif -//%if config +//%if config | multi_context | web declare module "@clusterio/lib" { //%endif @@ -58,7 +26,23 @@ declare module "@clusterio/lib" { "__plugin_name__.myControlField": string; } //%endif -//%if config +//%if multi_context | web + export interface Permissions { +//%endif +//%if multi_context + "__plugin_name__.example.permission.event": never; + "__plugin_name__.example.permission.request": never; +//%endif +//%if controller & web // Subscribing requires web content and the controller + "__plugin_name__.example.permission.subscribe": never; +//%endif +//%if web + "__plugin_name__.page.view": never; +//%endif +//%if multi_context | web + } +//%endif +//%if config | multi_context | web } //%endif @@ -143,6 +127,43 @@ export const plugin: lib.PluginDeclaration = { //%if multi_context // Subscribing requires multi context ], //%endif +//%if multi_context | web + + permissions: [ +//%endif +//%if multi_context + { + name: "__plugin_name__.example.permission.event", + title: "Example permission event", + description: "Example Description. Event. Change me in index.ts", + grantByDefault: true, + }, + { + name: "__plugin_name__.example.permission.request", + title: "Example permission request", + description: "Example Description. Request. Change me in index.ts", + grantByDefault: true, + }, +//%endif +//%if controller & web // Subscribing requires web content and the controller + { + name: "__plugin_name__.example.permission.subscribe", + title: "Example permission subscribe", + description: "Example Description. Subscribe. Change me in index.ts", + grantByDefault: true, + }, +//%endif +//%if web + { + name: "__plugin_name__.page.view", + title: "Example page view permission", + description: "Example Description. View. Change me in index.ts", + grantByDefault: true, + }, +//%endif +//%if multi_context | web + ], +//%endif //%if web // Web content template has an example route which is the plugin name webEntrypoint: "./web", diff --git a/packages/create/templates/plugin-ts/tsconfig.browser.json b/packages/create/templates/plugin-ts/tsconfig.browser.json index fe5100ca..a8232b97 100644 --- a/packages/create/templates/plugin-ts/tsconfig.browser.json +++ b/packages/create/templates/plugin-ts/tsconfig.browser.json @@ -9,5 +9,5 @@ "noEmitOnError": true, "outDir": "${configDir}/dist/browser", }, - "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], + "include": [ "web/**/*.tsx", "web/**/*.ts", "index.ts", "messages.ts", "package.json" ], } diff --git a/packages/create/templates/plugin-ts/web/plugin.tsx b/packages/create/templates/plugin-ts/web/plugin.tsx index 3584a098..92fbb5cd 100644 --- a/packages/create/templates/plugin-ts/web/plugin.tsx +++ b/packages/create/templates/plugin-ts/web/plugin.tsx @@ -57,7 +57,7 @@ export class WebPlugin extends BaseWebPlugin { permission: "__plugin_name__.example.permission.subscribe", //%endif //%if !controller - permission: "__plugin_name__.example.permission.view", + permission: "__plugin_name__.page.view", //%endif content: , }, diff --git a/packages/ctl/ctl.ts b/packages/ctl/ctl.ts index b3141d5f..2625ea5a 100755 --- a/packages/ctl/ctl.ts +++ b/packages/ctl/ctl.ts @@ -108,6 +108,7 @@ export class Control extends lib.Link { async function loadPlugins(pluginList: Map) { let pluginInfos = await lib.loadPluginInfos(pluginList); lib.registerPluginMessages(pluginInfos); + lib.registerPluginPermissions(pluginInfos); lib.addPluginConfigFields(pluginInfos); let ctlPlugins = new Map(); diff --git a/packages/host/host.ts b/packages/host/host.ts index a1f9a9b8..37d7f14f 100755 --- a/packages/host/host.ts +++ b/packages/host/host.ts @@ -163,6 +163,7 @@ async function startHost() { logger.info("Loading Plugin info"); let pluginInfos = await lib.loadPluginInfos(pluginList); lib.registerPluginMessages(pluginInfos); + lib.registerPluginPermissions(pluginInfos); lib.addPluginConfigFields(pluginInfos); const hostConfigPath = args.config; diff --git a/packages/lib/src/data/UserDetails.ts b/packages/lib/src/data/UserDetails.ts index 28a1646a..ffdb2585 100644 --- a/packages/lib/src/data/UserDetails.ts +++ b/packages/lib/src/data/UserDetails.ts @@ -1,5 +1,5 @@ import { Static, Type } from "@sinclair/typebox"; - +import type { PermissionName } from "../permissions.js"; import PlayerStats from "./PlayerStats.js"; /** Represents a user in the cluster */ @@ -164,5 +164,5 @@ export interface IUser extends UserDetails { * @throws {Error} If the given permission does not exist. * @throws {libErrors.PermissionError} if the user does noh have the given permission. */ - checkPermission(permission: string): void + checkPermission(permission: PermissionName): void } diff --git a/packages/lib/src/permissions.ts b/packages/lib/src/permissions.ts index b7ef91a9..2c9abed0 100644 --- a/packages/lib/src/permissions.ts +++ b/packages/lib/src/permissions.ts @@ -6,10 +6,44 @@ * @author Hornwitser * @module lib/users */ +import type { PluginNodeEnvInfo, PluginWebpackEnvInfo } from "./plugin.js"; import { Permission } from "./data/index.js"; export const permissions = new Map(); +/** + * 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 * @@ -17,25 +51,18 @@ export const permissions = new Map(); * granted to roles, and checked on users. Name should be `lower_case` * style and start with the plugin name followed by a dot. * - * @param def - The definition for the permission. - * @param def.name - The internal name for the permission. - * @param def.title - User facing name for permission. - * @param def.description - User facing description. - * @param def.grantByDefault - - * If true this role is granted by the generated Player role by default. + * 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, -}: { - name: string, - title: string, - description: string, - grantByDefault?: boolean, -}) { +}: PermissionDefinition) { if (typeof name !== "string" || !name.length) { throw new Error("Expected name to be a non-empty string"); } @@ -56,477 +83,508 @@ export function definePermission({ 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 ___" -definePermission({ - name: "core.admin", - title: "Administrator", - description: "Bypass all permission checks.", -}); -definePermission({ - name: "core.control.connect", - title: "Connect to WebSocket", - description: "Connect to the controller's WebSocket control interface.", - grantByDefault: true, -}); +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, + }, -definePermission({ - 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.", -}); -definePermission({ - name: "core.controller.restart", - title: "Restart controller", - description: "Restart the Node.js controller process if the system is set up for restarting.", -}); -definePermission({ - name: "core.controller.update", - title: "Update controller", - description: "Remotely update the controller if the controller allows for remote updates.", -}); -definePermission({ - name: "core.controller.get_config", - title: "Get controller config", - description: "Get the config of controller.", -}); -definePermission({ - name: "core.controller.update_config", - title: "Modify controller config", - description: "Modify the controller config or entries of the controller config.", -}); -definePermission({ - 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.", -}); -definePermission({ - 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.", -}); -definePermission({ - name: "core.host.restart", - title: "Restart Hosts", - description: "Restart Node.js host processes if the system they run on are set up for restarting.", -}); -definePermission({ - name: "core.host.update", - title: "Update Hosts", - description: "Remotely update a host if the host allows for remote updates.", -}); -definePermission({ - name: "core.host.get_config", - title: "Get host configs", - description: "Get the config of hosts.", -}); -definePermission({ - name: "core.host.update_config", - title: "Modify host configs", - description: "Modify the host config or entries of the host config.", -}); -definePermission({ - name: "core.host.list", - title: "List hosts", - description: "Get the full list of hosts in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.host.subscribe", - title: "Subscribe to host updates", - description: "Subscribe to be notified on updates on the details of hosts.", - grantByDefault: true, -}); -definePermission({ - name: "core.host.generate_token", - title: "Generate host token", - description: "Generate tokens for hosts to connect to the cluster with.", -}); -definePermission({ - name: "core.host.revoke_token", - title: "Revoke host token", - description: "Revoke access token and terminate existing connection for hosts.", -}); -definePermission({ - name: "core.host.create_config", - title: "Create host config", - description: "Create host configs via the controller.", -}); + { + 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.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.", + }, -definePermission({ - name: "core.instance.get", - title: "Get instance", - description: "Get the details of an instance in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.instance.list", - title: "List instances", - description: "Get the full list of instances in the cluster.", - grantByDefault: true, -}); -definePermission({ - 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, -}); -definePermission({ - name: "core.instance.create", - title: "Create instance", - description: "Create new instances on the controller.", -}); -definePermission({ - name: "core.instance.get_config", - title: "Get instance config", - description: "Get the config of instances.", -}); -definePermission({ - name: "core.instance.update_config", - title: "Modify instance config", - description: "Modify the config or entries of the config for instances.", -}); -definePermission({ - name: "core.instance.assign", - title: "Assign instance", - description: "Assign or reassign instances to a host.", -}); -definePermission({ - name: "core.instance.save.list", - title: "List saves", - description: "List the saves currently on instances.", -}); -definePermission({ - name: "core.instance.save.subscribe", - title: "Subscribe to save updates", - description: "Subscribe to be notifed on updates to the list of saves of instances.", -}); -definePermission({ - name: "core.instance.save.create", - title: "Create new save", - description: "Create new savegames on instances.", -}); -definePermission({ - name: "core.instance.save.rename", - title: "Rename save", - description: "Rename savegames on instances.", -}); -definePermission({ - name: "core.instance.save.copy", - title: "Copy save", - description: "Create copies of savegames on instances.", -}); -definePermission({ - name: "core.instance.save.delete", - title: "Delete save", - description: "Delete savegames on instances.", -}); -definePermission({ - name: "core.instance.save.upload", - title: "Upload save", - description: "Upload savegames to instances.", -}); -definePermission({ - name: "core.instance.save.transfer", - title: "Transfer save", - description: "Transfer savegames between instances.", -}); -definePermission({ - name: "core.instance.save.download", - title: "Download save", - description: "Download savegames from instances.", -}); -definePermission({ - 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.", -}); -definePermission({ - 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.", -}); -definePermission({ - name: "core.instance.start", - title: "Start instance", - description: "Start instances.", -}); -definePermission({ - name: "core.instance.restart", - title: "Restart instance", - description: "Restart instances.", -}); -definePermission({ - name: "core.instance.load_scenario", - title: "Load scenario", - description: "Start instances by loading a scenario.", -}); -definePermission({ - name: "core.instance.stop", - title: "Stop instance", - description: "Stop instances.", -}); -definePermission({ - name: "core.instance.kill", - title: "Kill instance", - description: "Terminate running instances without saving or cleanup.", -}); -definePermission({ - name: "core.instance.delete", - title: "Delete instance", - description: "Delete instances (includes config and all files stored like saves and logs).", -}); -definePermission({ - name: "core.instance.send_rcon", - title: "Send RCON", - description: "Send arbitrary RCON commands to instances.", -}); + { + 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.", + }, -definePermission({ - name: "core.mod_pack.get", - title: "Retrieve mod pack", - description: "Get the details of a mod pack in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod_pack.list", - title: "List mod packs", - description: "Get the full list of a mod packs in the cluster.", - grantByDefault: true, -}); -definePermission({ - 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, -}); -definePermission({ - name: "core.mod_pack.create", - title: "Create mod pack", - description: "Create new mod pack in the cluster.", -}); -definePermission({ - name: "core.mod_pack.update", - title: "Update mod pack", - description: "Edit existing mod pack definition in the cluster.", -}); -definePermission({ - name: "core.mod_pack.delete", - title: "Delete mod pack", - description: "Delete mod pack definition in the cluster.", -}); -definePermission({ - name: "core.mod.get", - title: "Get mods", - description: "Get the details of a mod stored in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.list", - title: "List mods", - description: "Get the full list of mods stored on the controller.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.search", - title: "Search mods", - description: "Search through the list of mods stored on the controller.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.search_portal", - title: "Search mod portal", - description: "Search for mods on the Factorio mod portal.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.subscribe", - title: "Subscribe to mod updates", - description: "Subscribe to be notified on updates on the details of mods.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.upload", - title: "Upload mods", - description: "Upload mods to the controller.", -}); -definePermission({ - name: "core.mod.download", - title: "Download mod", - description: "Download mods stored on the controller.", - grantByDefault: true, -}); -definePermission({ - name: "core.mod.delete", - title: "Delete mod", - description: "Delete mods stored on the controller.", -}); -definePermission({ - 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.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, + }, -definePermission({ - name: "core.permission.list", - title: "List permissions", - description: "Get the full list of permissions in the cluster.", - grantByDefault: true, -}); + { + name: "core.permission.list", + title: "List permissions", + description: "Get the full list of permissions in the cluster.", + grantByDefault: true, + }, -definePermission({ - name: "core.role.list", - title: "List roles", - description: "Get the full list of roles and their permissions in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.role.subscribe", - title: "Subscribe to role updates", - description: "Subscribe to be notified on updates on the details of roles.", - grantByDefault: true, -}); -definePermission({ - name: "core.role.create", - title: "Create role", - description: "Create new roles with permissions.", -}); -definePermission({ - name: "core.role.update", - title: "Update role", - description: "Edit existing roles including permissions they grant.", -}); -definePermission({ - name: "core.role.delete", - title: "Delete role", - description: "Permanently delete roles.", -}); + { + 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.", + }, -definePermission({ - name: "core.user.get", - title: "Get user", - description: "Get the details of a user in the cluster.", - grantByDefault: true, -}); -definePermission({ - name: "core.user.list", - title: "List users", - description: "Get the full list of users in the cluster.", - grantByDefault: true, -}); -definePermission({ - 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, -}); -definePermission({ - name: "core.user.create", - title: "Create users", - description: "Create user acounts with a given name.", -}); -definePermission({ - name: "core.user.revoke_token", - title: "Revoke user token", - description: "Revoke access token and terminate all existing control connections for oneself.", - grantByDefault: true, -}); -definePermission({ - 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.", -}); -definePermission({ - name: "core.user.update_roles", - 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", - description: "Delete users and all data stored for them.", -}); -definePermission({ - 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)", -}); -definePermission({ - name: "core.user.bulk_export", - title: "Bulk user export", - description: "Bulk export users including admin, whitelist, and bans.", -}); -definePermission({ - 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.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)", + }, -definePermission({ - name: "core.log.follow", - title: "Follow cluster log", - description: "Receive new entries in the cluster log. Required to see instance console.", -}); -definePermission({ - 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.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.", + }, -definePermission({ - name: "core.plugin.list", - title: "List plugins", - description: "List all installed plugins on a machine.", -}); -definePermission({ - name: "core.plugin.update", - title: "Update plugin", - description: "Remotely update a plugin if the target allows remote updates of plugins.", -}); -definePermission({ - name: "core.plugin.install", - title: "Install plugin", - description: "Remotely install a plugin if the target allows remote installs of plugins.", -}); + { + 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.", + }, -definePermission({ - name: "core.debug.dump_ws", - title: "Dump WebSocket", - description: "Dump all WebSocket communicatation from the controller.", -}); + { + name: "core.debug.dump_ws", + title: "Dump WebSocket", + description: "Dump all WebSocket communicatation from the controller.", + }, -definePermission({ - name: "core.external.get_factorio_versions", - title: "Get factorio versions", - description: "Get list of all factorio versions from Wube", -}); + { + name: "core.external.get_factorio_versions", + title: "Get factorio versions", + description: "Get list of all factorio versions from Wube", + }, -definePermission({ - name: "core.external.get_latest_releases", - title: "Get factorio releases", - description: "Get the latest stable and experimental factorio releases 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); +} diff --git a/packages/lib/src/plugin.ts b/packages/lib/src/plugin.ts index b5060561..0f88f43a 100644 --- a/packages/lib/src/plugin.ts +++ b/packages/lib/src/plugin.ts @@ -5,9 +5,9 @@ import * as libHelpers from "./helpers.js"; import type { Logger } from "./logging.js"; import type { FieldDefinition } from "./config/index.js"; +import type { PermissionDefinition } from "./permissions.js"; import type { PlayerStats } from "./data/index.js"; - export const PluginFeatureFlags = [ /** The plugin requires module code to be patched into the save */ "SavePatching", @@ -37,6 +37,7 @@ export type PluginDeclaration = { features?: (typeof PluginFeatureFlags)[number][]; messages?: any[]; + permissions?: PermissionDefinition[]; routes?: string[]; } diff --git a/packages/lib/src/subscriptions.ts b/packages/lib/src/subscriptions.ts index 4324f53e..298bf5d7 100644 --- a/packages/lib/src/subscriptions.ts +++ b/packages/lib/src/subscriptions.ts @@ -2,6 +2,7 @@ import { Type, Static } from "@sinclair/typebox"; import { Link, Event, EventClass, RequestHandler, WebSocketBaseConnector } from "./link/index.js"; import { Address, MessageRequest, IUser, JsonBoolean, StringEnum } from "./data/index.js"; import isDeepStrictEqual from "#is_deep_strict_equal"; +import type { PermissionName } from "./permissions.js"; import { RequestError } from "./errors.js"; import { logger } from "./logging.js"; @@ -155,7 +156,7 @@ export class SubscriptionRequest { const entry = Link._eventsByName.get(data[0]); if (entry && entry.Event.permission) { if (typeof entry.Event.permission === "string") { - user.checkPermission(entry.Event.permission); + user.checkPermission(entry.Event.permission as PermissionName); } else { entry.Event.permission(user, message); } diff --git a/packages/web_ui/src/BaseWebPlugin.ts b/packages/web_ui/src/BaseWebPlugin.ts index 4ce654ae..35978615 100644 --- a/packages/web_ui/src/BaseWebPlugin.ts +++ b/packages/web_ui/src/BaseWebPlugin.ts @@ -1,5 +1,5 @@ import type React from "react"; -import type { AccountRole, FieldDefinition, Logger, PluginWebpackEnvInfo } from "@clusterio/lib"; +import type { AccountRole, FieldDefinition, Logger, PermissionName, PluginWebpackEnvInfo } from "@clusterio/lib"; import type { Control } from "./util/websocket"; /** @@ -37,11 +37,11 @@ export type UserAccount = { /** Roles of the corrently logged in account. */ roles: AccountRole[]; /** Check if the currently logged in account has the given permission. */ - hasPermission: (permission: string) => boolean | null; + hasPermission: (permission: PermissionName) => boolean | null; /** Check if the currently logged in account has any of the given permissions. */ - hasAnyPermission: (...permissions: string[]) => boolean | null; + hasAnyPermission: (...permissions: PermissionName[]) => boolean | null; /** Check if the currently logged in account has all of given permissions. */ - hasAllPermission: (...permissions: string[]) => boolean | null; + hasAllPermission: (...permissions: PermissionName[]) => boolean | null; /** Logs out of the web interface. */ logOut: () => void; }; @@ -75,7 +75,7 @@ export interface PluginPage { /** * Permission to access page. function are expected to throw an error if access is deny. */ - permission?: string | ((account: UserAccount) => (boolean|null)); + permission?: PermissionName | ((account: UserAccount) => (boolean|null)); }; /** diff --git a/packages/web_ui/src/bootstrap.tsx b/packages/web_ui/src/bootstrap.tsx index 2695d154..74359367 100644 --- a/packages/web_ui/src/bootstrap.tsx +++ b/packages/web_ui/src/bootstrap.tsx @@ -136,6 +136,7 @@ export default async function bootstrap() { })); let [pluginInfos, pluginSetKey] = await loadPluginInfos(); lib.registerPluginMessages(pluginInfos); + lib.registerPluginPermissions(pluginInfos); lib.addPluginConfigFields(pluginInfos); let wsUrl = new URL(webRoot, document.location.href); diff --git a/packages/web_ui/src/model/account.ts b/packages/web_ui/src/model/account.ts index dfce6832..fd18e5d1 100644 --- a/packages/web_ui/src/model/account.ts +++ b/packages/web_ui/src/model/account.ts @@ -22,13 +22,13 @@ export function useAccount(): UserAccount { }; }, [control]); - function checkPermissionExists(permission: string) { + function checkPermissionExists(permission: lib.PermissionName) { if (!lib.permissions.has(permission)) { throw new Error(`permission ${permission} does not exist`); } } - function hasPermission(permission: string): boolean { + function hasPermission(permission: lib.PermissionName): boolean { for (let role of roles) { if (role.permissions.includes("core.admin") || role.permissions.includes(permission)) { return true; @@ -40,14 +40,14 @@ export function useAccount(): UserAccount { return { name, roles, - hasPermission(permission: string): boolean | null { + hasPermission(permission: lib.PermissionName): boolean | null { if (!roles) { return null; } checkPermissionExists(permission); return hasPermission(permission); }, - hasAnyPermission(...permissions: string[]): boolean | null { + hasAnyPermission(...permissions: lib.PermissionName[]): boolean | null { if (!roles) { return null; } @@ -61,7 +61,7 @@ export function useAccount(): UserAccount { } return false; }, - hasAllPermission(...permissions: string[]): boolean | null { + hasAllPermission(...permissions: lib.PermissionName[]): boolean | null { if (!roles) { return null; } diff --git a/plugins/inventory_sync/index.ts b/plugins/inventory_sync/index.ts index dc78621b..17fddfc0 100644 --- a/plugins/inventory_sync/index.ts +++ b/plugins/inventory_sync/index.ts @@ -8,15 +8,11 @@ declare module "@clusterio/lib" { export interface ControllerConfigFields { "inventory_sync.player_lock_timeout": number; } + export interface Permissions { + "inventory_sync.inventory.view": never; + } } -lib.definePermission({ - name: "inventory_sync.inventory.view", - title: "View player inventories", - description: "View player inventories", - grantByDefault: true, -}); - export const plugin: lib.PluginDeclaration = { name: "inventory_sync", title: "Inventory sync", @@ -57,6 +53,14 @@ export const plugin: lib.PluginDeclaration = { messages.DownloadRequest, messages.DatabaseStatsRequest, ], + permissions: [ + { + name: "inventory_sync.inventory.view", + title: "View player inventories", + description: "View player inventories", + grantByDefault: true, + }, + ], webEntrypoint: "./web", routes: ["/inventory"], }; diff --git a/plugins/inventory_sync/tsconfig.browser.json b/plugins/inventory_sync/tsconfig.browser.json index 325fd71d..ba2d8c39 100644 --- a/plugins/inventory_sync/tsconfig.browser.json +++ b/plugins/inventory_sync/tsconfig.browser.json @@ -4,5 +4,5 @@ { "path": "../../packages/lib/tsconfig.browser.json" }, { "path": "../../packages/web_ui/tsconfig.browser.json" }, ], - "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], + "include": [ "web/**/*.tsx", "web/**/*.ts", "index.ts", "messages.ts", "package.json" ], } diff --git a/plugins/subspace_storage/index.ts b/plugins/subspace_storage/index.ts index 00cb5914..bdbecf03 100644 --- a/plugins/subspace_storage/index.ts +++ b/plugins/subspace_storage/index.ts @@ -9,15 +9,11 @@ declare module "@clusterio/lib" { export interface InstanceConfigFields { "subspace_storage.log_item_transfers": boolean; } + export interface Permissions { + "subspace_storage.storage.view": never; + } } -lib.definePermission({ - name: "subspace_storage.storage.view", - title: "View Subspace Storage", - description: "View the items and fluids stored in the shared subspace.", - grantByDefault: true, -}); - export const plugin: lib.PluginDeclaration = { name: "subspace_storage", title: "Subspace Storage", @@ -60,6 +56,14 @@ export const plugin: lib.PluginDeclaration = { messages.UpdateStorageEvent, messages.SetStorageSubscriptionRequest, ], + permissions: [ + { + name: "subspace_storage.storage.view", + title: "View Subspace Storage", + description: "View the items and fluids stored in the shared subspace.", + grantByDefault: true, + }, + ], webEntrypoint: "./web", routes: ["/storage"], }; diff --git a/plugins/subspace_storage/tsconfig.browser.json b/plugins/subspace_storage/tsconfig.browser.json index 325fd71d..ba2d8c39 100644 --- a/plugins/subspace_storage/tsconfig.browser.json +++ b/plugins/subspace_storage/tsconfig.browser.json @@ -4,5 +4,5 @@ { "path": "../../packages/lib/tsconfig.browser.json" }, { "path": "../../packages/web_ui/tsconfig.browser.json" }, ], - "include": [ "web/**/*.tsx", "web/**/*.ts", "messages.ts", "package.json" ], + "include": [ "web/**/*.tsx", "web/**/*.ts", "index.ts", "messages.ts", "package.json" ], } diff --git a/test/lib/users.js b/test/lib/users.js index 344f304d..e8bd2479 100644 --- a/test/lib/users.js +++ b/test/lib/users.js @@ -43,4 +43,45 @@ describe("lib/users", function() { ); }); }); + + describe("registerPluginPermissions()", function() { + it("should define the permissions declared by plugins", function() { + lib.registerPluginPermissions([ + { name: "foo", permissions: [ + { name: "foo.first", title: "First", description: "The first" }, + { name: "foo.second", title: "Second", description: "The second", grantByDefault: true }, + ]}, + { name: "bar" }, + { name: "baz", permissions: [] }, + ]); + assert(lib.permissions.has("foo.first"), "foo.first was not defined"); + assert.equal(lib.permissions.get("foo.first").grantByDefault, false); + assert(lib.permissions.has("foo.second"), "foo.second was not defined"); + assert.equal(lib.permissions.get("foo.second").grantByDefault, true); + }); + + it("should throw if the name does not start with the plugin name", function() { + assert.throws( + () => lib.registerPluginPermissions([ + { name: "foo", permissions: [{ name: "bar.spam", title: "Spam", description: "Spam" }] }, + ]), + new Error("Expected name of permission 'bar.spam' for foo to start with 'foo.'") + ); + assert.throws( + () => lib.registerPluginPermissions([ + { name: "foo", permissions: [{ title: "Spam", description: "Spam" }] }, + ]), + new Error("Expected name of permission 'undefined' for foo to start with 'foo.'") + ); + }); + + it("should throw on already defined permission", function() { + assert.throws( + () => lib.registerPluginPermissions([ + { name: "foo", permissions: [{ name: "foo.first", title: "First", description: "The first" }] }, + ]), + new Error("Permission 'foo.first' is already defined") + ); + }); + }); });