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") + ); + }); + }); });