Move the seed into exp_scenario and seed the permission groups (#457)

The seed roles were owned by exp_roles, but they describe the scenario, so
they now live in exp_scenario/seed.ts next to the permissions they grant.
Each seed role names the permission group its holders belong to, and the
five groups the legacy config defined (Admin, Trusted, Standard, Guest,
Restricted) are seeded through exp_groups along with one role mapping per
role. Mapping priorities follow how exp_roles ranks a player's highest
role, so Jail lands in Restricted regardless of other roles.

SeedRolesRequest becomes exp_scenario's SeedRequest behind a new
exp_scenario.seed permission, and the button moves to the scenario's web
plugin, which gets a web entrypoint for it. exp_roles no longer depends on
exp_scenario, and exp_scenario gains controller tests around the seed.

With groups seeded from the controller the legacy expcore.permission_groups
module and its config are removed, along with the Group rcon static.
This commit is contained in:
Bastiaan
2026-09-08 11:20:44 +01:00
committed by GitHub
parent f805a1e760
commit 0854a2b7d5
20 changed files with 464 additions and 738 deletions
-22
View File
@@ -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();
});
-62
View File
@@ -1,62 +0,0 @@
"use strict";
const t = require("tap");
const lib = require("@clusterio/lib");
const { seedRoles, flattenSeedPermissions } = require("../dist/node/seed");
// Importing this defines the exp_scenario permissions the seed grants
require("@expcluster/scenario/dist/node/permissions");
t.test("seedRoles[] grant only defined permissions", t2 => {
for (const role of seedRoles) {
for (const permission of role.permissions) {
t2.ok(lib.permissions.has(permission), `${role.name} grants defined permission ${permission}`);
}
}
t2.end();
});
t.test("seedRoles[] are unique with one default and one admin", t2 => {
const names = seedRoles.map(role => role.name);
t2.strictSame(names.length, new Set(names).size, "names are unique");
t2.strictSame(seedRoles.filter(role => role.isDefault).length, 1, "one default role");
t2.strictSame(seedRoles.filter(role => role.isAdmin).length, 1, "one admin role");
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) {
if (role.parent === undefined) {
continue;
}
const parent = byName.get(role.parent);
t2.ok(parent, `${role.name} has parent ${role.parent}`);
const flattened = flattenSeedPermissions(role);
for (const permission of flattenSeedPermissions(parent)) {
t2.ok(flattened.has(permission), `${role.name} inherits ${permission}`);
}
}
t2.end();
});
t.test("flattenSeedPermissions() does not inherit permissions when parent is undefined", t2 => {
for (const role of seedRoles) {
if (role.parent !== undefined) {
continue;
}
const flattened = flattenSeedPermissions(role);
for (const permission of role.permissions) {
t2.ok(flattened.has(permission), `${role.name} contains ${permission}`);
}
t2.equal(
flattened.size,
role.permissions.length,
`${role.name} has no inherited permissions`,
);
}
t2.end();
});