diff --git a/exp_legacy/module/config/_file_loader.lua b/exp_legacy/module/config/_file_loader.lua index 88fdec5efe..4edd462e17 100644 --- a/exp_legacy/module/config/_file_loader.lua +++ b/exp_legacy/module/config/_file_loader.lua @@ -24,6 +24,4 @@ return { "modules.gui.vlayer", "modules.graftorio.require", -- graftorio - --- Config Files - "config.expcore.permission_groups", -- loads some predefined permission groups } diff --git a/exp_legacy/module/config/expcore/permission_groups.lua b/exp_legacy/module/config/expcore/permission_groups.lua deleted file mode 100644 index b0e972f355..0000000000 --- a/exp_legacy/module/config/expcore/permission_groups.lua +++ /dev/null @@ -1,143 +0,0 @@ ---- Use this file to add new permission groups to the game; --- start with Permission_Groups.new_group('name'); --- then use either :allow_all() or :disallow_all() to set the default for non specified actions; --- then use :allow{} and :disallow{} to specify certain actions to allow/disallow --- @config Permission-Groups - --- local Event = require("modules/exp_legacy/utils/event") -- @dep utils.event -local Groups = require("modules.exp_legacy.expcore.permission_groups") --- @dep expcore.permission_groups - -Groups.new_group("Admin") - :allow_all() - :disallow{ - "add_permission_group", -- admin - "delete_permission_group", - "edit_permission_group", - "import_permissions_string", - "map_editor_action", - "toggle_map_editor", - "change_multiplayer_config", - "set_heat_interface_mode", - "set_heat_interface_temperature", - "set_infinity_container_filter_item", - "set_infinity_container_remove_unfiltered_items", - "set_infinity_pipe_filter", - } - -Groups.new_group("Trusted") - :allow_all() - :disallow{ - "add_permission_group", -- admin - "delete_permission_group", - "edit_permission_group", - "import_permissions_string", - "map_editor_action", - "toggle_map_editor", - "change_multiplayer_config", - "set_heat_interface_mode", - "set_heat_interface_temperature", - "set_infinity_container_filter_item", - "set_infinity_container_remove_unfiltered_items", - "set_infinity_pipe_filter", - "admin_action", -- trusted - } - -Groups.new_group("Standard") - :allow_all() - :disallow{ - "add_permission_group", -- admin - "delete_permission_group", - "edit_permission_group", - "import_permissions_string", - "map_editor_action", - "toggle_map_editor", - "change_multiplayer_config", - "set_heat_interface_mode", - "set_heat_interface_temperature", - "set_infinity_container_filter_item", - "set_infinity_container_remove_unfiltered_items", - "set_infinity_pipe_filter", - "admin_action", -- trusted - "change_programmable_speaker_alert_parameters", -- standard - "drop_item", - "open_new_platform_button_from_rocket_silo", - "set_rocket_silo_send_to_orbit_automated_mode", - } - -Groups.new_group("Guest") - :allow_all() - :disallow{ - "add_permission_group", -- admin - "delete_permission_group", - "edit_permission_group", - "import_permissions_string", - "map_editor_action", - "toggle_map_editor", - "change_multiplayer_config", - "set_heat_interface_mode", - "set_heat_interface_temperature", - "set_infinity_container_filter_item", - "set_infinity_container_remove_unfiltered_items", - "set_infinity_pipe_filter", - "admin_action", -- trusted - "change_programmable_speaker_alert_parameters", -- standard - "drop_item", - "open_new_platform_button_from_rocket_silo", - "set_rocket_silo_send_to_orbit_automated_mode", - "change_programmable_speaker_parameters", -- guest - "change_train_stop_station", - -- 'deconstruct', - "remove_cables", - "remove_train_station", - "reset_assembling_machine", - "rotate_entity", - -- 'use_artillery_remote', -- not in 2.0 - "launch_rocket", - "cancel_research", - -- 'activate_cut', -- not in 2.0 - "flush_opened_entity_fluid", - "flush_opened_entity_specific_fluid", - } - -Groups.new_group("Restricted") - :disallow_all() - :allow("write_to_console") - ---[[ These events are used until a role system is added to make it easier for our admins - -local trusted_time = 60*60*60*10 -- 10 hour -local standard_time = 60*60*60*3 -- 3 hour -local function assign_group(player) - local current_group_name = player.permission_group and player.permission_group.name or 'None' - if player.admin then - Permission_Groups.set_player_group(player,'Admin') - elseif player.online_time > trusted_time or current_group_name == 'Trusted' then - Permission_Groups.set_player_group(player,'Trusted') - elseif player.online_time > standard_time or current_group_name == 'Standard' then - Permission_Groups.set_player_group(player,'Standard') - else - Permission_Groups.set_player_group(player,'Guest') - end -end - -Event.add(defines.events.on_player_joined_game,function(event) - local player = game.players[event.player_index] - assign_group(player) -end) - -Event.add(defines.events.on_player_promoted,function(event) - local player = game.players[event.player_index] - assign_group(player) -end) - -Event.add(defines.events.on_player_demoted,function(event) - local player = game.players[event.player_index] - assign_group(player) -end) - -local check_interval = 60*60*15 -- 15 minutes -Event.on_nth_tick(check_interval,function(event) - for _,player in pairs(game.connected_players) do - assign_group(player) - end -end)]] diff --git a/exp_legacy/module/expcore/permission_groups.lua b/exp_legacy/module/expcore/permission_groups.lua deleted file mode 100644 index edb0a315d9..0000000000 --- a/exp_legacy/module/expcore/permission_groups.lua +++ /dev/null @@ -1,355 +0,0 @@ ---[[-- Core Module - Permission Groups -- Permission group making for factorio so you never have to make one by hand again -@core Groups -@alias Permissions_Groups - -@usage--- Example Group (Allow All) --- here we will create an admin group however we do not want them to use the map editor or mess with the permission groups -Permission_Groups.new_group('Admin') -- this defines a new group called "Admin" -:allow_all() -- this makes the default to allow any input action unless set other wise -:disallow{ -- here we disallow the input action we don't want them to use - 'add_permission_group', - 'delete_permission_group', - 'import_permissions_string', - 'map_editor_action', - 'toggle_map_editor' -} - -@usage--- Example Group (Disallow All) --- here we will create a group that cant do anything but talk in chat -Permission_Groups.new_group('Restricted') -- this defines a new group called "Restricted" -:disallow_all() -- this makes the default to disallow any input action unless set other wise -:allow('write_to_console') -- here we allow them to chat, {} can be used here if we had more than one action - -]] - -local Event = require("modules/exp_legacy/utils/event") -local Async = require("modules/exp_util/async") - -local PermissionsGroups = { - groups = {}, -- store for the different groups that are created - _prototype = {}, -- stores functions that are used on group instances -} - --- Async function to add players to permission groups -local add_to_permission_group_async = - Async.register(function(permission_group, player) - permission_group.add_player(player) - end) -PermissionsGroups.add_to_permission_group_async = add_to_permission_group_async - --- Async function to remove players from permission groups -local remove_from_permission_group_async = - Async.register(function(permission_group, player) - permission_group.remove_player(player) - end) -PermissionsGroups.remove_from_permission_group_async = remove_from_permission_group_async - ---- Getters. --- Functions that get permission groups --- @section getters - ---[[-- Defines a new permission group that can have it actions set in the config -@tparam string name the name of the new group -@treturn Permissions_Groups._prototype the new group made with function to allow and disallow actions - -@usage-- Defining a new permission group -Groups.new_group('Admin') - -]] -function PermissionsGroups.new_group(name) - local group = setmetatable({ - name = name, - actions = {}, - allow_all_actions = true, - }, { - __index = PermissionsGroups._prototype, - }) - PermissionsGroups.groups[name] = group - return group -end - ---[[-- Returns the group with the given name, case sensitive -@tparam string name the name of the group to get -@treturn ?Permissions_Groups._prototype|nil the group with that name or nil if non found - -@usage-- Getting a permision group -local admin_group = Groups.get_group_by_name('Admin') - -]] -function PermissionsGroups.get_group_by_name(name) - return PermissionsGroups.groups[name] -end - ---[[-- Returns the group that a player is in -@tparam LuaPlayer player the player to get the group of can be name index etc -@treturn ?Permissions_Groups._prototype|nil the group with that player or nil if non found - -@usage-- Get your permission group -local group = Groups.get_group_from_player(game.player) - -]] -function PermissionsGroups.get_group_from_player(player) - local group = player.permission_group - if group then - return PermissionsGroups.groups[group.name] - end -end - ---- Setters. --- Functions that control all groups --- @section players - ---[[-- Reloads/creates all permission groups and sets them to they configured state - -@usage-- Reload the permission groups, used internally -Groups.reload_permissions() - -]] -function PermissionsGroups.reload_permissions() - for _, group in pairs(PermissionsGroups.groups) do - group:create() - end -end - ---[[-- Sets a player's group to the one given, a player can only have one group at a time -@tparam LuaPlayer player the player to effect can be name index etc -@tparam string group the name of the group to give to the player -@treturn boolean true if the player was added successfully, false other wise - -@usage-- Set your permission group -Groups.set_player_group(game.player, 'Admin') - -]] -function PermissionsGroups.set_player_group(player, group) - group = PermissionsGroups.get_group_by_name(group) - if not group or not player then return false end - group:add_player(player) - return true -end - ---- Actions. --- Functions that control group actions --- @section actions - ---[[-- Sets the allow state of an action for this group, used internally but is safe to use else where -@tparam ?string|defines.input_action action the action that you want to set the state of -@tparam boolean state the state that you want to set it to, true = allow, false = disallow -@treturn Permissions_Groups._prototype returns self so function can be chained - -@usage-- Set an action to be disallowed -group:set_action('toggle_map_editor', false) - -]] -function PermissionsGroups._prototype:set_action(action, state) - local input_action = defines.input_action[action] --[[@as defines.input_action?]] - if input_action == nil then input_action = action end - assert(type(input_action) == "number", tostring(action) .. " is not a valid input action") - self.actions[input_action] = state - return self -end - ---[[-- Sets an action or actions to be allowed for this group even with disallow_all triggered, Do not use in runtime -@tparam string|Array actions the action or actions that you want to allow for this group -@treturn Permissions_Groups._prototype returns self so function can be chained - -@usage-- Allow some actions -group:allow{ - 'write_to_console' -} - -]] -function PermissionsGroups._prototype:allow(actions) - if type(actions) ~= "table" then - actions = { actions } - end - for _, action in pairs(actions) do - self:set_action(action, true) - end - - return self -end - ---[[-- Sets an action or actions to be disallowed for this group even with allow_all triggered, Do not use in runtime -@tparam string|Array actions the action or actions that you want to disallow for this group -@treturn Permissions_Groups._prototype returns self so function can be chained - -@usage-- Disalow some actions -group:disallow{ - 'add_permission_group', - 'delete_permission_group', - 'import_permissions_string', - 'map_editor_action', - 'toggle_map_editor' -} - -]] -function PermissionsGroups._prototype:disallow(actions) - if type(actions) ~= "table" then - actions = { actions } - end - for _, action in pairs(actions) do - self:set_action(action, false) - end - - return self -end - ---[[-- Sets the default state for any actions not given to be allowed, useful with :disallow -@treturn Permissions_Groups._prototype returns self so function can be chained - -@usage-- Allow all actions unless given by disallow -group:allow_all() - -]] -function PermissionsGroups._prototype:allow_all() - self.allow_all_actions = true - return self -end - ---[[-- Sets the default state for any action not given to be disallowed, useful with :allow -@treturn Permissions_Groups._prototype returns self so function can be chained - -@usage-- Disallow all actions unless given by allow -group:disallow_all() - -]] -function PermissionsGroups._prototype:disallow_all() - self.allow_all_actions = false - return self -end - ---[[-- Returns if an input action is allowed for this group -@tparam ?string|defines.input_action action the action that you want to test for -@treturn boolean true if the group is allowed the action, false other wise - -@usage-- Test if a group is allowed an action -local allowed = group:is_allowed('write_to_console') - -]] -function PermissionsGroups._prototype:is_allowed(action) - if type(action) == "string" then - action = defines.input_action[action] - end - local state = self.actions[action] - if state == nil then - state = self.allow_all_actions - end - return state -end - ---- Players. --- Functions that control group players --- @section players - ---[[-- Creates or updates the permission group with the configured actions, used internally -@treturn LuaPermissionGroup the permission group that was created - -@usage-- Create the permission group so players can be added, used internally -group:create() - -]] -function PermissionsGroups._prototype:create() - local group = self:get_raw() - if not group then - group = game.permissions.create_group(self.name) --[[@as LuaPermissionGroup]] - end - for _, action in pairs(defines.input_action) do - group.set_allows_action(action, self:is_allowed(action)) - end - - return group -end - ---[[-- Returns the LuaPermissionGroup that was created with this group object, used internally -@treturn LuaPermissionGroup the raw lua permission group - -@usage-- Get the factorio api permision group, used internally -local permission_group = group:get_raw() - -]] -function PermissionsGroups._prototype:get_raw() - return game.permissions.get_group(self.name) -end - ---[[-- Adds a player to this group -@tparam LuaPlayer player LuaPlayer the player you want to add to this group can be name or index etc -@treturn boolean true if the player was added successfully, false other wise - -@usage-- Add a player to this permission group -group:add_player(game.player) - -]] -function PermissionsGroups._prototype:add_player(player) - local group = self:get_raw() - if not group or not player then return false end - add_to_permission_group_async(group, player) - return true -end - ---[[-- Removes a player from this group -@tparam LuaPlayer player LuaPlayer the player you want to remove from this group can be name or index etc -@treturn boolean true if the player was removed successfully, false other wise - -@usage-- Remove a player from this permission group -group:remove_player(game.player) - -]] -function PermissionsGroups._prototype:remove_player(player) - local group = self:get_raw() - if not group or not player then return false end - remove_from_permission_group_async(group, player) - return true -end - ---[[-- Returns all player that are in this group with the option to filter to online/offline only -@tparam[opt] boolean online if nil returns all players, if true online players only, if false returns online players only -@treturn table a table of players that are in this group; filtered if online param is given - -@usage-- Get all players in this group -local online_players = group:get_players() - -@usage-- Get all online players in this group -local online_players = group:get_players(true) - -]] -function PermissionsGroups._prototype:get_players(online) - local players = {} - local group = self:get_raw() - if group then - if online == nil then - return group.players - else - for _, player in pairs(group.players) do - if player.connected == online then - table.insert(player, player) - end - end - end - end - return players -end - ---[[-- Prints a message to every player in this group -@tparam string message the message that you want to send to the players -@treturn number the number of players that received the message - -@usage-- Print a message to all players in thie group -group:print('Hello, World!') - -]] -function PermissionsGroups._prototype:print(message) - local players = self:get_players(true) - for _, player in pairs(players) do - player.print(message) - end - - return #players -end - --- when the game starts it will make the permission groups -Event.on_init(function() - PermissionsGroups.reload_permissions() -end) - -return PermissionsGroups diff --git a/exp_roles/controller.ts b/exp_roles/controller.ts index 45b798906e..b9bf3a8b68 100644 --- a/exp_roles/controller.ts +++ b/exp_roles/controller.ts @@ -1,7 +1,6 @@ import { BaseControllerPlugin, InstanceRecord } from "@clusterio/controller"; import * as lib from "@clusterio/lib"; import * as messages from "./messages"; -import { SeedRole, seedRoles, flattenSeedPermissions } from "./seed"; import * as path from "node:path"; export class ControllerPlugin extends BaseControllerPlugin { @@ -35,7 +34,6 @@ export class ControllerPlugin extends BaseControllerPlugin { this.controller.handle(messages.RoleListRequest, this.handleRoleListRequest.bind(this)); this.controller.handle(messages.RoleMetaUpdateRequest, this.handleRoleMetaUpdateRequest.bind(this)); - this.controller.handle(messages.SeedRolesRequest, this.handleSeedRolesRequest.bind(this)); this.controller.handle(messages.AssignmentListRequest, this.handleAssignmentListRequest.bind(this)); this.controller.handle(messages.AssignmentUpdateRequest, this.handleAssignmentUpdateRequest.bind(this)); @@ -45,68 +43,6 @@ export class ControllerPlugin extends BaseControllerPlugin { await this.roleMeta.save(); } - /* - Seeding - */ - - /** - * Create the roles the scenario shipped with. Roles which already exist - * by name are reused and only gain the seed permissions. - */ - async handleSeedRolesRequest() { - for (const [index, seedRole] of seedRoles.entries()) { - const role = this.seedRole(seedRole); - if (!role) { - continue; - } - - this.roleMeta.set(new messages.RoleMetaRecord( - role.id, - index + 1, - seedRole.priority ?? 0, - seedRole.shortHand, - "", - seedRole.color, - seedRole.autoAssignHours === undefined ? null : seedRole.autoAssignHours * 3600000, - seedRole.blockAutoAssign ?? false, - )); - } - - this.logger.info(`Seeded ${seedRoles.length} roles`); - } - - /** Find or create the clusterio role for a seed role, returns undefined if it has no role to use. */ - seedRole(seedRole: SeedRole) { - const roles = this.controller.roles; - if (seedRole.isAdmin) { - return roles.get(lib.Role.DefaultAdminRoleId); - } - if (seedRole.isDefault) { - const defaultRoleId = this.controller.config.get("controller.default_role_id"); - return defaultRoleId !== null ? roles.get(defaultRoleId) : undefined; - } - - const permissions = flattenSeedPermissions(seedRole); - for (const permission of permissions) { - if (!lib.permissions.has(permission)) { - this.logger.warn(`Seed role ${seedRole.name} grants unknown permission ${permission}`); - } - } - - let role = [...roles.valuesMutable()].find(other => other.name === seedRole.name); - if (role) { - for (const permission of permissions) { - role.permissions.add(permission); - } - } else { - const id = Math.max(5, ...[...roles.keys()].map(other => other + 1)); - role = new lib.Role(id, seedRole.name, "", permissions); - this.logger.info(`Created role ${seedRole.name}`); - } - roles.set(role); - return role; - } - /* Role properties */ diff --git a/exp_roles/index.ts b/exp_roles/index.ts index d2c522fe18..e3cb409390 100644 --- a/exp_roles/index.ts +++ b/exp_roles/index.ts @@ -27,7 +27,6 @@ export const plugin: lib.PluginDeclaration = { messages.RoleListRequest, messages.RoleMetaUpdateRequest, - messages.SeedRolesRequest, messages.AssignmentListRequest, messages.AssignmentUpdateRequest, diff --git a/exp_roles/messages.ts b/exp_roles/messages.ts index 7ba71459db..3886970bc1 100644 --- a/exp_roles/messages.ts +++ b/exp_roles/messages.ts @@ -365,18 +365,6 @@ export class RoleMetaUpdateRequest { } } -/** Create the roles the scenario shipped with, see seed.ts. */ -export class SeedRolesRequest { - declare ["constructor"]: typeof SeedRolesRequest; - static plugin = "exp_roles" as const; - static type = "request" as const; - static src = "control" as const; - static dst = "controller" as const; - static permission = "core.role.create" as const; - - constructor() {} -} - /* Assignment requests */ diff --git a/exp_roles/package.json b/exp_roles/package.json index aa9f311238..c5f3ae57f1 100644 --- a/exp_roles/package.json +++ b/exp_roles/package.json @@ -26,7 +26,6 @@ "@clusterio/host": "workspace:^", "@clusterio/lib": "workspace:^", "@clusterio/web_ui": "workspace:^", - "@expcluster/scenario": "workspace:^", "@types/node": "catalog:", "@types/react": "catalog:", "antd": "catalog:", diff --git a/exp_roles/test/controller.test.js b/exp_roles/test/controller.test.js index bf2f843c97..4e66ee4810 100644 --- a/exp_roles/test/controller.test.js +++ b/exp_roles/test/controller.test.js @@ -4,17 +4,12 @@ const lib = require("@clusterio/lib"); const { Controller } = require("@clusterio/controller"); const { ControllerPlugin } = require("../dist/node/controller"); const messages = require("../dist/node/messages"); -const { seedRoles } = require("../dist/node/seed"); - -// Importing this defines the exp_scenario permissions the seed grants -require("@expcluster/scenario/dist/node/permissions"); // The controller validates message classes against the link registry lib.Link.register(messages.RoleUpdatedEvent); lib.Link.register(messages.AssignmentUpdatedEvent); lib.Link.register(messages.RoleListRequest); lib.Link.register(messages.RoleMetaUpdateRequest); -lib.Link.register(messages.SeedRolesRequest); lib.Link.register(messages.AssignmentListRequest); lib.Link.register(messages.AssignmentUpdateRequest); @@ -229,22 +224,5 @@ t.test("class ControllerPlugin", t2 => { t3.strictSame(none, null, "nothing is replayed when up to date"); }); - t2.test(".handleSeedRolesRequest() creates the roles and reuses them by name", async t3 => { - const { plugin, controller } = await startPlugin(t3, { - roles: [role(0, "Cluster Admin", ["core.admin"]), role(1, "Player")], - }); - - await plugin.handleSeedRolesRequest(); - t3.strictSame(controller.roles.size, seedRoles.length, "every seed role exists"); - - const moderator = [...controller.roles.values()].find(other => other.name === "Moderator"); - t3.ok(moderator.permissions.has("exp_scenario.command.jail"), "parent permissions are flattened in"); - t3.ok(plugin.roleMeta.get(moderator.id), "the role properties are created"); - t3.strictSame(plugin.roleMeta.get(moderator.id).shortHand, "Mod", "the properties match the seed"); - - await plugin.handleSeedRolesRequest(); - t3.strictSame(controller.roles.size, seedRoles.length, "seeding again reuses the roles"); - }); - t2.end(); }); diff --git a/exp_roles/web/components/SeedRoles.tsx b/exp_roles/web/components/SeedRoles.tsx deleted file mode 100644 index 9b7c52b2b1..0000000000 --- a/exp_roles/web/components/SeedRoles.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React, { useContext, useState } from "react"; -import { Button, Popconfirm } from "antd"; - -import { ControlContext, SectionHeader, useAccount, notifyErrorHandler } from "@clusterio/web_ui"; - -import { SeedRolesRequest } from "../../messages"; - -/** Button on the roles page which creates the roles the scenario shipped with. */ -export default function SeedRoles() { - const control = useContext(ControlContext); - const account = useAccount(); - const [seeding, setSeeding] = useState(false); - - if (!account.hasPermission("core.role.create")) { - return null; - } - - return { - setSeeding(true); - control.send(new SeedRolesRequest()) - .catch(notifyErrorHandler("Error seeding roles")) - .finally(() => setSeeding(false)); - }} - > - - } - />; -} diff --git a/exp_roles/web/index.tsx b/exp_roles/web/index.tsx index 1bd55066eb..644cd10d95 100644 --- a/exp_roles/web/index.tsx +++ b/exp_roles/web/index.tsx @@ -5,7 +5,6 @@ import * as lib from "@clusterio/lib"; import * as messages from "../messages"; import RoleProperties from "./components/RoleProperties"; -import SeedRoles from "./components/SeedRoles"; export class WebPlugin extends BaseWebPlugin { roles = new lib.MapSubscriber(messages.RoleUpdatedEvent, this.control); @@ -15,7 +14,6 @@ export class WebPlugin extends BaseWebPlugin { // does not carry in its type this.componentExtra = { RoleViewPage: RoleProperties as React.ComponentType, - RolesPage: SeedRoles, }; } diff --git a/exp_scenario/controller.ts b/exp_scenario/controller.ts index fc761f627c..1af4d27bf2 100644 --- a/exp_scenario/controller.ts +++ b/exp_scenario/controller.ts @@ -1,6 +1,157 @@ import * as lib from "@clusterio/lib"; import { BaseControllerPlugin } from "@clusterio/controller"; +import type { ControllerPlugin as RolesPlugin } from "@expcluster/roles/dist/node/controller"; +import type { ControllerPlugin as GroupsPlugin } from "@expcluster/permission-groups/dist/node/controller"; +import { RoleMetaRecord } from "@expcluster/roles/dist/node/messages"; +import { GroupRecord, GroupPermissions, RoleMappingRecord } from "@expcluster/permission-groups/dist/node/messages"; +import * as messages from "./messages"; +import { SeedRole, SeedGroup, seedRoles, seedGroups, flattenSeedPermissions } from "./seed"; export class ControllerPlugin extends BaseControllerPlugin { + async init() { + this.controller.handle(messages.SeedRequest, this.handleSeedRequest.bind(this)); + } + /** + * Create the roles and permission groups the scenario shipped with. + * + * Roles which already exist by name are reused and only gain the seed + * permissions, groups which already exist by name are reset to the seed. + */ + async handleSeedRequest() { + const rolesPlugin = this.controller.plugins.get("exp_roles") as RolesPlugin | undefined; + const groupsPlugin = this.controller.plugins.get("exp_groups") as GroupsPlugin | undefined; + if (!rolesPlugin || !groupsPlugin) { + throw new lib.RequestError("Seeding requires the exp_roles and exp_groups plugins"); + } + + const roleIds = new Map(); + for (const [index, seedRole] of seedRoles.entries()) { + const role = this.seedRole(seedRole); + if (!role) { + continue; + } + + roleIds.set(seedRole.name, role.id); + rolesPlugin.roleMeta.set(new RoleMetaRecord( + role.id, + index + 1, + seedRole.priority ?? 0, + seedRole.shortHand, + "", + seedRole.color, + seedRole.autoAssignHours === undefined ? null : seedRole.autoAssignHours * 3600000, + seedRole.blockAutoAssign ?? false, + )); + } + + const groupIds = new Map(); + for (const seedGroup of seedGroups) { + groupIds.set(seedGroup.name, this.seedGroup(groupsPlugin, seedGroup).id); + } + + this.seedRoleMappings(groupsPlugin, roleIds, groupIds); + this.logger.info(`Seeded ${roleIds.size} roles and ${groupIds.size} permission groups`); + } + + /** Find or create the clusterio role for a seed role, returns undefined if it has no role to use. */ + seedRole(seedRole: SeedRole) { + const roles = this.controller.roles; + if (seedRole.isAdmin) { + return roles.get(lib.Role.DefaultAdminRoleId); + } + if (seedRole.isDefault) { + const defaultRoleId = this.controller.config.get("controller.default_role_id"); + return defaultRoleId !== null ? roles.get(defaultRoleId) : undefined; + } + + const permissions = flattenSeedPermissions(seedRole); + for (const permission of permissions) { + if (!lib.permissions.has(permission)) { + this.logger.warn(`Seed role ${seedRole.name} grants unknown permission ${permission}`); + } + } + + let role = [...roles.valuesMutable()].find(other => other.name === seedRole.name); + if (role) { + for (const permission of permissions) { + role.permissions.add(permission); + } + } else { + const id = Math.max(5, ...[...roles.keys()].map(other => other + 1)); + role = new lib.Role(id, seedRole.name, "", permissions); + this.logger.info(`Created role ${seedRole.name}`); + } + roles.set(role); + return role; + } + + /** Find or create the permission group for a seed group. */ + seedGroup(groupsPlugin: GroupsPlugin, seedGroup: SeedGroup) { + const permissions = new GroupPermissions(seedGroup.isBlacklist, [...seedGroup.inputActions]); + const existing = [...groupsPlugin.groups.values()].find(other => other.name === seedGroup.name); + const group = new GroupRecord(existing?.id ?? newId(groupsPlugin.groups), seedGroup.name, permissions); + if (!existing) { + this.logger.info(`Created permission group ${seedGroup.name}`); + } + groupsPlugin.groups.set(group); + return group; + } + + /** + * Map each seed role onto its seed group. + * + * The mapping with the highest priority decides a player's group, so the + * priorities follow how exp_roles picks a player's highest role: role + * priority first, then the role order. + */ + seedRoleMappings(groupsPlugin: GroupsPlugin, roleIds: Map, groupIds: Map) { + // Lowest role first, so the mapping priority rises with the role + const ranked = seedRoles + .filter(seedRole => seedRole.group !== undefined && roleIds.has(seedRole.name)) + .reverse() + .sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0)); + const seededRoleIds = new Set(ranked.map(seedRole => roleIds.get(seedRole.name)!)); + + // A mapping of a single seed role is reused, every other mapping keeps its priority + const existing = new Map(); + const taken = new Set(); + for (const mapping of groupsPlugin.roleMappings.values()) { + const [roleId] = mapping.roleIds; + if (mapping.roleIds.size === 1 && seededRoleIds.has(roleId)) { + existing.set(roleId, mapping); + } else { + taken.add(mapping.priority); + } + } + + const mappings = []; + let priority = 0; + for (const seedRole of ranked) { + priority += 1; + while (taken.has(priority)) { + priority += 1; + } + + const roleId = roleIds.get(seedRole.name)!; + mappings.push(new RoleMappingRecord( + existing.get(roleId)?.id ?? newId(groupsPlugin.roleMappings), + new Set([roleId]), + groupIds.get(seedRole.group!)!, + priority, + true, + )); + } + + groupsPlugin.roleMappings.setMany(mappings); + return mappings; + } +} + +function newId(datastore: { has(id: number): boolean }) { + let id = Math.random() * 2 ** 31 | 0; + while (datastore.has(id)) { + id = Math.random() * 2 ** 31 | 0; + } + return id; } diff --git a/exp_scenario/index.ts b/exp_scenario/index.ts index b3e4bd81b6..f09c3c6fc1 100644 --- a/exp_scenario/index.ts +++ b/exp_scenario/index.ts @@ -1,5 +1,5 @@ import * as lib from "@clusterio/lib"; -// import * as Messages from "./messages"; +import * as messages from "./messages"; // Defines a permission for every in game action and role flag used by the scenario import "./permissions"; @@ -16,6 +16,12 @@ lib.definePermission({ description: "Edit the config for all submodules of ExpScenario", }); +lib.definePermission({ + name: "exp_scenario.seed", + title: "Seed ExpScenario roles and groups", + description: "Create the roles and permission groups the scenario shipped with", +}); + declare module "@clusterio/lib" { } @@ -27,13 +33,9 @@ export const plugin: lib.PluginDeclaration = { controllerEntrypoint: "./dist/node/controller", instanceEntrypoint: "./dist/node/instance", - /* messages: [ + messages.SeedRequest, ], webEntrypoint: "./web", - routes: [ - "/exp_scenario", - ], - */ }; diff --git a/exp_scenario/messages.ts b/exp_scenario/messages.ts index 1eb520574e..9c77845639 100644 --- a/exp_scenario/messages.ts +++ b/exp_scenario/messages.ts @@ -1,95 +1,11 @@ -import { plainJson, jsonArray, JsonBoolean, JsonNumber, JsonString, StringEnum } from "@clusterio/lib"; -import { Type, Static } from "@sinclair/typebox"; - -export class PluginExampleEvent { - declare ["constructor"]: typeof PluginExampleEvent; - static type = "event" as const; - static src = ["host", "control"] as const; - static dst = ["controller", "host", "instance"] as const; +/** Create the roles and permission groups the scenario shipped with, see seed.ts. */ +export class SeedRequest { + declare ["constructor"]: typeof SeedRequest; static plugin = "exp_scenario" as const; - static permission = "exp_scenario.example.permission.event"; - - constructor( - public myString: string, - public myNumberArray: number[], - ) { - } - - static jsonSchema = Type.Object({ - "myString": Type.String(), - "myNumberArray": Type.Array(Type.Number()), - }); - - static fromJSON(json: Static) { - return new PluginExampleEvent(json.myString, json.myNumberArray); - } -} - -export class PluginExampleRequest { - declare ["constructor"]: typeof PluginExampleRequest; static type = "request" as const; - static src = ["host", "control"] as const; - static dst = ["controller", "host", "instance"] as const; - static plugin = "exp_scenario" as const; - static permission = "exp_scenario.example.permission.request"; - - constructor( - public myString: string, - public myNumberArray: number[], - ) { - } - - static jsonSchema = Type.Object({ - "myString": Type.String(), - "myNumberArray": Type.Array(Type.Number()), - }); - - static fromJSON(json: Static) { - return new PluginExampleRequest(json.myString, json.myNumberArray); - } - - static Response = plainJson(Type.Object({ - "myResponseString": Type.String(), - "myResponseNumbers": Type.Array(Type.Number()), - })); -} - -export class ExampleSubscribableValue { - constructor( - public id: string, - public updatedAtMs: number, - public isDeleted: boolean, - ) { - } - - static jsonSchema = Type.Object({ - id: Type.String(), - updatedAtMs: Type.Number(), - isDeleted: Type.Boolean(), - }); - - static fromJSON(json: Static) { - return new this(json.id, json.updatedAtMs, json.isDeleted); - } -} - -export class ExampleSubscribableUpdate { - declare ["constructor"]: typeof ExampleSubscribableUpdate; - static type = "event" as const; - static src = "controller" as const; - static dst = "control" as const; - static plugin = "exp_scenario" as const; - static permission = "exp_scenario.example.permission.subscribe"; - - constructor( - public updates: ExampleSubscribableValue[], - ) { } - - static jsonSchema = Type.Object({ - "updates": Type.Array(ExampleSubscribableValue.jsonSchema), - }); + static src = "control" as const; + static dst = "controller" as const; + static permission = "exp_scenario.seed" as const; - static fromJSON(json: Static) { - return new this(json.updates.map(update => ExampleSubscribableValue.fromJSON(update))); - } + constructor() {} } diff --git a/exp_scenario/module/commands/_rcon.lua b/exp_scenario/module/commands/_rcon.lua index ef3febbc26..7db7ad1fca 100644 --- a/exp_scenario/module/commands/_rcon.lua +++ b/exp_scenario/module/commands/_rcon.lua @@ -7,7 +7,6 @@ local add_static, add_dynamic = Commands.add_rcon_static, Commands.add_rcon_dyna add_static("Gui", require("modules/exp_gui")) -add_static("Group", require("modules.exp_legacy.expcore.permission_groups")) add_static("Roles", require("modules/exp_roles")) add_static("Datastore", require("modules.exp_legacy.expcore.datastore")) add_static("External", require("modules.exp_legacy.expcore.external")) diff --git a/exp_scenario/package.json b/exp_scenario/package.json index 8a00f8b41f..1e4d9d655b 100644 --- a/exp_scenario/package.json +++ b/exp_scenario/package.json @@ -7,15 +7,19 @@ "repository": "explosivegaming/ExpCluster", "main": "dist/node/index.js", "scripts": { - "prepare": "tsc --build && webpack-cli --env production" + "prepare": "tsc --build && webpack-cli --env production", + "test": "tap --disable-coverage --allow-empty-coverage test/*.test.js", + "coverage": "tap --coverage-report=text test/*.test.js" }, "engines": { "node": ">=18" }, "peerDependencies": { + "@clusterio/controller": "workspace:^", "@clusterio/lib": "workspace:^" }, "devDependencies": { + "@clusterio/controller": "workspace:^", "@clusterio/lib": "workspace:^", "@clusterio/web_ui": "workspace:^", "@types/node": "catalog:", @@ -23,6 +27,7 @@ "antd": "catalog:", "react": "catalog:", "react-dom": "catalog:", + "tap": "^21.1.0", "typescript": "catalog:", "webpack": "catalog:", "webpack-cli": "catalog:", @@ -32,6 +37,8 @@ "@expcluster/lib_commands": "workspace:^", "@expcluster/lib_util": "workspace:^", "@expcluster/lib_gui": "workspace:^", + "@expcluster/permission-groups": "workspace:^", + "@expcluster/roles": "workspace:^", "@sinclair/typebox": "catalog:" }, "publishConfig": { diff --git a/exp_roles/seed.ts b/exp_scenario/seed.ts similarity index 76% rename from exp_roles/seed.ts rename to exp_scenario/seed.ts index b028314a4a..62cc2ab638 100644 --- a/exp_roles/seed.ts +++ b/exp_scenario/seed.ts @@ -1,4 +1,4 @@ -import { RoleColor } from "./messages"; +import { RoleColor } from "@expcluster/roles/dist/node/messages"; /** * A role created by the seed, as the scenario defined it before roles moved to @@ -20,6 +20,22 @@ export interface SeedRole { /** Name of the role whose permissions are also granted, applied recursively. */ parent?: string; permissions: string[]; + /** + * Name of the seed group holders are placed in by their highest role. + * Without one the holders stay in Factorio's Default group. + */ + group?: string; +} + +/** + * A Factorio permission group created by the seed, as the scenario defined it + * before groups moved to the controller. + */ +export interface SeedGroup { + name: string; + /** When true the input actions are the only ones disallowed, otherwise the only ones allowed. */ + isBlacklist: boolean; + inputActions: string[]; } export const seedRoles: SeedRole[] = [ @@ -32,6 +48,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Senior Administrator", + group: "Admin", shortHand: "SAdmin", color: new RoleColor(233, 63, 233), parent: "Administrator", @@ -44,6 +61,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Administrator", + group: "Admin", shortHand: "Admin", color: new RoleColor(233, 63, 233), parent: "Moderator", @@ -55,6 +73,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Moderator", + group: "Admin", shortHand: "Mod", color: new RoleColor(0, 170, 0), parent: "Trainee", @@ -89,6 +108,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Trainee", + group: "Admin", shortHand: "TrMod", color: new RoleColor(0, 170, 0), parent: "Veteran", @@ -120,6 +140,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Board Member", + group: "Trusted", shortHand: "Board", color: new RoleColor(247, 246, 54), parent: "Sponsor", @@ -133,6 +154,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Senior Backer", + group: "Trusted", shortHand: "Backer", color: new RoleColor(238, 172, 44), parent: "Sponsor", @@ -140,6 +162,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Sponsor", + group: "Trusted", shortHand: "Spon", color: new RoleColor(238, 172, 44), parent: "Supporter", @@ -158,6 +181,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Supporter", + group: "Trusted", shortHand: "Sup", color: new RoleColor(230, 99, 34), parent: "Veteran", @@ -172,6 +196,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Partner", + group: "Trusted", shortHand: "Part", color: new RoleColor(140, 120, 200), parent: "Veteran", @@ -183,6 +208,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Veteran", + group: "Trusted", shortHand: "Vet", color: new RoleColor(140, 120, 200), parent: "Member", @@ -196,6 +222,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Member", + group: "Standard", shortHand: "Mem", color: new RoleColor(24, 172, 188), parent: "Regular", @@ -217,6 +244,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Regular", + group: "Standard", shortHand: "Reg", color: new RoleColor(79, 155, 163), autoAssignHours: 3, @@ -232,6 +260,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Jail", + group: "Restricted", shortHand: "Jail", color: new RoleColor(50, 50, 50), priority: 1, @@ -240,6 +269,7 @@ export const seedRoles: SeedRole[] = [ }, { name: "Guest", + group: "Guest", shortHand: "", color: new RoleColor(185, 187, 160), isDefault: true, @@ -247,6 +277,56 @@ export const seedRoles: SeedRole[] = [ }, ]; +const adminDisallowed = [ + "add_permission_group", + "delete_permission_group", + "edit_permission_group", + "import_permissions_string", + "map_editor_action", + "toggle_map_editor", + "change_multiplayer_config", + "set_heat_interface_mode", + "set_heat_interface_temperature", + "set_infinity_container_filter_item", + "set_infinity_container_remove_unfiltered_items", + "set_infinity_pipe_filter", +]; + +const trustedDisallowed = [ + ...adminDisallowed, + "admin_action", +]; + +const standardDisallowed = [ + ...trustedDisallowed, + "change_programmable_speaker_alert_parameters", + "drop_item", + "open_new_platform_button_from_rocket_silo", + "set_rocket_silo_send_to_orbit_automated_mode", +]; + +const guestDisallowed = [ + ...standardDisallowed, + "change_programmable_speaker_parameters", + "change_train_stop_station", + "remove_cables", + "remove_train_station", + "reset_assembling_machine", + "rotate_entity", + "launch_rocket", + "cancel_research", + "flush_opened_entity_fluid", + "flush_opened_entity_specific_fluid", +]; + +export const seedGroups: SeedGroup[] = [ + { name: "Admin", isBlacklist: true, inputActions: adminDisallowed }, + { name: "Trusted", isBlacklist: true, inputActions: trustedDisallowed }, + { name: "Standard", isBlacklist: true, inputActions: standardDisallowed }, + { name: "Guest", isBlacklist: true, inputActions: guestDisallowed }, + { name: "Restricted", isBlacklist: false, inputActions: ["write_to_console"] }, +]; + /** The permissions a seed role grants, including those of its parents. */ export function flattenSeedPermissions(role: SeedRole, roles = seedRoles) { const permissions = new Set(); diff --git a/exp_scenario/test/controller.test.js b/exp_scenario/test/controller.test.js new file mode 100644 index 0000000000..a963b15764 --- /dev/null +++ b/exp_scenario/test/controller.test.js @@ -0,0 +1,139 @@ +"use strict"; +const t = require("tap"); +const lib = require("@clusterio/lib"); +const { Controller } = require("@clusterio/controller"); +const { ControllerPlugin } = require("../dist/node/controller"); +const messages = require("../dist/node/messages"); +const { seedRoles, seedGroups } = require("../dist/node/seed"); +const roles = require("@expcluster/roles/dist/node"); +const groups = require("@expcluster/permission-groups/dist/node"); +const { ControllerPlugin: RolesPlugin } = require("@expcluster/roles/dist/node/controller"); +const { ControllerPlugin: GroupsPlugin } = require("@expcluster/permission-groups/dist/node/controller"); +const { GroupRecord, GroupPermissions, RoleMappingRecord } = require("@expcluster/permission-groups/dist/node/messages"); + +// Importing this defines the permissions the seed grants +require("../dist/node/permissions"); + +// The controller validates message classes against the link registry +for (const Message of [messages.SeedRequest, ...roles.plugin.messages, ...groups.plugin.messages]) { + lib.Link.register(Message); +} + +const logger = { child: () => logger, info: () => {}, warn: () => {}, error: () => {}, verbose: () => {} }; + +// Silence the singleton logger to avoid polluting the test output +lib.logger.silent = true; +t.after(() => { + lib.logger.silent = false; +}); + +/** Build the plugin around a real controller, which is side effect free while not started. */ +async function startPlugin(t2, { withPlugins = true } = {}) { + const controllerConfig = new lib.ControllerConfig("controller", { + "controller.database_directory": t2.testdir(), + "controller.default_role_id": lib.Role.DefaultPlayerRoleId, + }); + const controller = new Controller(logger, [], controllerConfig); + controller.roles.set(new lib.Role(0, "Cluster Admin", "", new Set(["core.admin"]))); + controller.roles.set(new lib.Role(1, "Player", "", new Set())); + + if (withPlugins) { + for (const [name, Plugin] of [["exp_roles", RolesPlugin], ["exp_groups", GroupsPlugin]]) { + const plugin = new Plugin({ name }, controller, undefined, logger); + await plugin.init(); + controller.plugins.set(name, plugin); + } + } + + const plugin = new ControllerPlugin({ name: "exp_scenario" }, controller, undefined, logger); + await plugin.init(); + return { plugin, controller }; +} + +const byName = (datastore, name) => [...datastore.values()].find(other => other.name === name); + +t.test("class ControllerPlugin", t2 => { + t2.test(".handleSeedRequest() throws when the seeded plugins are missing", async t3 => { + const { plugin } = await startPlugin(t3, { withPlugins: false }); + await t3.rejects( + plugin.handleSeedRequest(), + { message: "Seeding requires the exp_roles and exp_groups plugins" }, + "the request is rejected", + ); + }); + + t2.test(".handleSeedRequest() creates the roles and reuses them by name", async t3 => { + const { plugin, controller } = await startPlugin(t3); + const rolesPlugin = controller.plugins.get("exp_roles"); + + await plugin.handleSeedRequest(); + t3.strictSame(controller.roles.size, seedRoles.length, "every seed role exists"); + + const moderator = byName(controller.roles, "Moderator"); + t3.ok(moderator.permissions.has("exp_scenario.command.jail"), "parent permissions are flattened in"); + t3.strictSame(rolesPlugin.roleMeta.get(moderator.id).shortHand, "Mod", "the role properties match the seed"); + + await plugin.handleSeedRequest(); + t3.strictSame(controller.roles.size, seedRoles.length, "seeding again reuses the roles"); + }); + + t2.test(".handleSeedRequest() creates the groups and resets them by name", async t3 => { + const { plugin, controller } = await startPlugin(t3); + const groupsPlugin = controller.plugins.get("exp_groups"); + + await plugin.handleSeedRequest(); + t3.strictSame(groupsPlugin.groups.size, seedGroups.length, "every seed group exists"); + + const restricted = byName(groupsPlugin.groups, "Restricted"); + t3.strictSame(restricted.permissions.isBlacklist, false, "the group only allows the listed actions"); + t3.strictSame(restricted.permissions.permissions, ["write_to_console"], "the actions match the seed"); + + const admin = byName(groupsPlugin.groups, "Admin"); + groupsPlugin.groups.set(new GroupRecord(admin.id, admin.name, new GroupPermissions(true, []))); + await plugin.handleSeedRequest(); + t3.strictSame(groupsPlugin.groups.size, seedGroups.length, "seeding again reuses the groups"); + t3.ok( + byName(groupsPlugin.groups, "Admin").permissions.permissions.includes("toggle_map_editor"), + "seeding again resets the actions", + ); + }); + + t2.test(".handleSeedRequest() maps each role onto its group by rank", async t3 => { + const { plugin, controller } = await startPlugin(t3); + const groupsPlugin = controller.plugins.get("exp_groups"); + + await plugin.handleSeedRequest(); + const mapped = seedRoles.filter(role => role.group !== undefined); + t3.strictSame(groupsPlugin.roleMappings.size, mapped.length, "every role with a group is mapped"); + + const mappingOf = name => [...groupsPlugin.roleMappings.values()] + .find(mapping => mapping.roleIds.has(byName(controller.roles, name).id)); + const groupOf = name => groupsPlugin.groups.get(mappingOf(name).groupId).name; + t3.strictSame(groupOf("Player"), "Guest", "the default role maps to the guest group"); + t3.strictSame(groupOf("Jail"), "Restricted", "jail maps to the restricted group"); + t3.ok(mappingOf("Jail").priority > mappingOf("Senior Administrator").priority, "jail outranks every role"); + t3.ok(mappingOf("Moderator").priority > mappingOf("Veteran").priority, "the role order sets the rank"); + t3.ok(mappingOf("Veteran").priority > mappingOf("Player").priority, "the default role ranks lowest"); + t3.strictSame(mappingOf("Cluster Admin"), undefined, "the admin role keeps the factorio default group"); + + const priorities = [...groupsPlugin.roleMappings.values()].map(mapping => mapping.priority); + t3.strictSame(priorities.length, new Set(priorities).size, "priorities are unique"); + }); + + t2.test(".handleSeedRequest() reuses mappings and keeps clear of other mappings", async t3 => { + const { plugin, controller } = await startPlugin(t3); + const groupsPlugin = controller.plugins.get("exp_groups"); + + groupsPlugin.roleMappings.set(new RoleMappingRecord(99, new Set([0, 1]), 5, 2, true)); + await plugin.handleSeedRequest(); + const first = new Map([...groupsPlugin.roleMappings.values()].map(mapping => [mapping.id, mapping.priority])); + + await plugin.handleSeedRequest(); + const second = new Map([...groupsPlugin.roleMappings.values()].map(mapping => [mapping.id, mapping.priority])); + t3.strictSame(second, first, "seeding again reuses the mappings"); + t3.strictSame(groupsPlugin.roleMappings.get(99).priority, 2, "other mappings are left alone"); + t3.notOk([...first.values()].filter(priority => priority === 2).length > 1, "seed priorities skip taken ones"); + }); + + t2.end(); +}); diff --git a/exp_roles/test/seed.test.js b/exp_scenario/test/seed.test.js similarity index 67% rename from exp_roles/test/seed.test.js rename to exp_scenario/test/seed.test.js index adcb098701..87e198a66d 100644 --- a/exp_roles/test/seed.test.js +++ b/exp_scenario/test/seed.test.js @@ -1,10 +1,10 @@ "use strict"; const t = require("tap"); const lib = require("@clusterio/lib"); -const { seedRoles, flattenSeedPermissions } = require("../dist/node/seed"); +const { seedRoles, seedGroups, flattenSeedPermissions } = require("../dist/node/seed"); -// Importing this defines the exp_scenario permissions the seed grants -require("@expcluster/scenario/dist/node/permissions"); +// Importing this defines the permissions the seed grants +require("../dist/node/permissions"); t.test("seedRoles[] grant only defined permissions", t2 => { for (const role of seedRoles) { @@ -23,6 +23,29 @@ t.test("seedRoles[] are unique with one default and one admin", t2 => { t2.end(); }); +t.test("seedRoles[] are placed in defined groups", t2 => { + const groupNames = new Set(seedGroups.map(group => group.name)); + for (const role of seedRoles) { + if (role.group !== undefined) { + t2.ok(groupNames.has(role.group), `${role.name} is placed in defined group ${role.group}`); + } + } + t2.end(); +}); + +t.test("seedGroups[] are unique", t2 => { + const names = seedGroups.map(group => group.name); + t2.strictSame(names.length, new Set(names).size, "names are unique"); + for (const group of seedGroups) { + t2.strictSame( + group.inputActions.length, + new Set(group.inputActions).size, + `${group.name} lists each input action once`, + ); + } + t2.end(); +}); + t.test("flattenSeedPermissions() inherits permissions from parent roles", t2 => { const byName = new Map(seedRoles.map(role => [role.name, role])); for (const role of seedRoles) { diff --git a/exp_scenario/web/components/Seed.tsx b/exp_scenario/web/components/Seed.tsx new file mode 100644 index 0000000000..619d79dfae --- /dev/null +++ b/exp_scenario/web/components/Seed.tsx @@ -0,0 +1,33 @@ +import React, { useContext, useState } from "react"; +import { Button, Popconfirm } from "antd"; + +import { ControlContext, SectionHeader, useAccount, notifyErrorHandler } from "@clusterio/web_ui"; + +import { SeedRequest } from "../../messages"; + +/** Button on the roles page which creates the roles and permission groups the scenario shipped with. */ +export default function Seed() { + const control = useContext(ControlContext); + const account = useAccount(); + const [seeding, setSeeding] = useState(false); + + if (!account.hasPermission("exp_scenario.seed")) { + return null; + } + + return { + setSeeding(true); + control.send(new SeedRequest()) + .catch(notifyErrorHandler("Error seeding roles and groups")) + .finally(() => setSeeding(false)); + }} + > + + } + />; +} diff --git a/exp_scenario/web/index.tsx b/exp_scenario/web/index.tsx index e69de29bb2..cecbd7f0ce 100644 --- a/exp_scenario/web/index.tsx +++ b/exp_scenario/web/index.tsx @@ -0,0 +1,11 @@ +import { BaseWebPlugin } from "@clusterio/web_ui"; + +import Seed from "./components/Seed"; + +export class WebPlugin extends BaseWebPlugin { + async init() { + this.componentExtra = { + RolesPage: Seed, + }; + } +}