feat(rust): the permission manager — the site owns the whole store (D160-D163, D188-D198)
All checks were successful
PR Checks / client-build (pull_request) Successful in 21s
PR Checks / frozen-manifest (pull_request) Successful in 43s
PR Checks / server-tests (pull_request) Successful in 7m58s

PLAN_REDESIGNS section 1.

- Every sync reads the store (perm.inventory), reconciles it against the
  site's record and its ledger, and pushes. A change made in the game is
  settled by the server's policy (D161): auto-adopt (default), adopt, or
  revoke. The first read of a server imports everything (D198).
- Groups belong to one server unless an admin shares them (D189), in new
  id-keyed tables; the old ones are copied once at boot and left unread.
  Holders may be a Steam account nobody linked (D188).
- An in-game change affects that server only (D190): a grant that reaches
  further gains an exception, a shared group is split.
- Never judged: a permission the server does not register right now (an
  unloaded plugin is not a revocation), and a pair an event lease holds.
- A new admin API (server view, grant/revoke with everywhere-or-here,
  groups by id, share/split, members, drift answers) and a screen on
  PermissionsManager's flow with a state on every toggle (D162, D163, U-1).
- The announcement voice names a group by id; old name settings still read.

Walked on both rigs against the walk core: import on an existing install,
auto-adopt of a grant and a revoke, a fleet grant's exception, Kits
unloaded without loss, a shared group split, adopt and revoke policies.
Server 420/420, client 58/58, swagger, imports and route manifest current.

Refs #21

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-28 06:58:59 -05:00
parent 77c90db338
commit e0d13e73db
24 changed files with 5873 additions and 2589 deletions

View File

@@ -383,90 +383,30 @@ module.exports = {
},
},
},
RustPermissionModel: {
RustPermissionOverview: {
type: 'object',
description:
'The whole permission model (GET /admin/rust/permissions): what the site authors, what each game reported back, and the names a grant may use.',
'The permission manager’s front page (GET /admin/rust/permissions): every server with its policy and sync state, and every change made in a game that waits for a person (PLAN_REDESIGNS §1).',
properties: {
groups: {
type: 'array',
description: 'Groups the site authors, mirrored into each in-scope game as a real group.',
items: {
type: 'object',
properties: {
name: { type: 'string', example: 'vip' },
title: { type: 'string', example: 'VIP' },
rank: { type: 'integer', example: 10 },
scope: {
type: 'string',
description: 'A server id, or `*` for every server.',
example: '*',
},
permissions: { type: 'array', items: { type: 'string', example: 'kits.vip' } },
chat: {
type: 'object',
nullable: true,
description: 'The group’s BetterChat style — all twelve fields as text — or null for a group without one (D138).',
additionalProperties: { type: 'string' },
example: { Title: '[VIP]', TitleColor: '#ffaa55', ChatFormat: '{Title} {Username}: {Message}' },
},
members: {
type: 'array',
items: {
type: 'object',
properties: {
userId: { type: 'integer', example: 42 },
username: { type: 'string', example: 'wanderer' },
steamId: {
type: 'string',
nullable: true,
description: 'Null when this account has linked no Steam id, in which case the membership reaches nobody yet.',
example: '76561198000000000',
},
playerName: { type: 'string', nullable: true, example: 'Wanderer' },
},
},
},
},
},
},
grants: {
type: 'array',
description: 'Permissions held by one person without a group. Unlike membership, a direct grant reaches a player who has never connected.',
items: {
type: 'object',
properties: {
id: { type: 'integer', example: 7 },
userId: { type: 'integer', example: 42 },
username: { type: 'string', example: 'wanderer' },
permission: { type: 'string', example: 'kits.gold' },
scope: { type: 'string', example: 'main' },
source: {
type: 'string',
description: 'What authored it — `admin`, `adopted`, or a later phase’s own writer.',
example: 'admin',
},
note: { type: 'string', nullable: true, example: null },
grantedAt: { type: 'string', format: 'date-time' },
accounts: {
type: 'array',
description: 'The Steam accounts this grant reaches. Empty means it reaches nobody yet.',
items: {
type: 'object',
properties: {
steamId: { type: 'string', example: '76561198000000000' },
name: { type: 'string', nullable: true, example: 'Wanderer' },
},
},
},
},
},
},
servers: {
type: 'array',
description: 'The state of the mirror, per configured server.',
items: { $ref: '#/components/schemas/RustPermissionSyncState' },
items: {
type: 'object',
properties: {
id: { type: 'string', example: 'rust-oxide' },
name: { type: 'string', example: 'Oxide rig' },
policy: {
type: 'string',
enum: ['auto-adopt', 'adopt', 'revoke'],
description: 'What a change made in the game becomes (D161): the site’s own, a question for a person, or undone.',
example: 'auto-adopt',
},
sync: { $ref: '#/components/schemas/RustPermissionSyncState' },
},
},
},
drift: { type: 'array', items: { $ref: '#/components/schemas/RustPermissionDrift' } },
policies: { type: 'array', items: { type: 'string' }, example: ['auto-adopt', 'adopt', 'revoke'] },
chatFields: {
type: 'array',
description: 'The twelve BetterChat group fields a style carries, with each one’s type and BetterChat’s default, for the style editor.',
@@ -479,49 +419,101 @@ module.exports = {
},
},
},
drift: {
},
},
RustPermissionDrift: {
type: 'object',
description: 'A change made in a game that waits for a person: every change under the `adopt` policy, an event’s grant removed in the game, a hand-edited style field, or a notice that a shared group was split (D190).',
properties: {
id: { type: 'integer', example: 3 },
serverId: { type: 'string', example: 'rust-oxide' },
kind: { type: 'string', description: '`grant`, `member`, `group-permission`, `group`, or `chat-field`.', example: 'grant' },
direction: { type: 'string', enum: ['added', 'removed', 'changed', 'split'], example: 'added' },
subject: { type: 'string', description: 'A Steam id, or a group name.', example: '76561198000000000' },
object: { type: 'string', description: 'A permission or group name, a style field, or empty.', example: 'kits.vip' },
detail: { type: 'string', nullable: true, description: 'What the game holds now (a style value, a group’s title, rank and parent), or a notice’s sentence.', example: null },
username: { type: 'string', nullable: true, example: 'wanderer' },
playerName: { type: 'string', nullable: true, example: 'Wanderer' },
firstSeen: { type: 'string', format: 'date-time' },
},
},
RustPermissionServer: {
type: 'object',
description:
'One server as the permission screen shows it (GET /admin/rust/permissions/servers/{serverId}), following uMod PermissionsManager’s flow (D162): plugins grouped by the plugin that registered each permission, the groups on the server (D189), every player holding anything there named by account and in-game name (D163), and the facts each toggle’s state is read from.',
properties: {
server: { type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' } } },
servers: { type: 'array', items: { type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' } } } },
policy: { type: 'string', example: 'auto-adopt' },
sync: { $ref: '#/components/schemas/RustPermissionSyncState' },
plugins: {
type: 'array',
description: 'What a game holds that the site did not author. Reported, never undone.',
items: {
type: 'object',
properties: {
id: { type: 'integer', example: 3 },
serverId: { type: 'string', example: 'main' },
kind: {
type: 'string',
description: 'One of `grant`, `member`, `group-permission`, or `chat-field` for a style field changed in game.',
example: 'grant',
},
detail: {
type: 'string',
nullable: true,
description: 'For `chat-field`, the value the game holds now. Null for every other kind.',
example: '#ff0000',
},
subject: {
type: 'string',
description: 'A Steam id, or a group name.',
example: '76561198000000000',
},
object: {
type: 'string',
description: 'A permission name, or a group name.',
example: 'kits.admin',
},
username: {
type: 'string',
nullable: true,
description: 'The website account holding that Steam id, when there is one. Without it the drift cannot be adopted, only revoked.',
example: 'wanderer',
},
firstSeen: { type: 'string', format: 'date-time' },
key: { type: 'string', example: 'plugin:ZoneManager' },
label: { type: 'string', example: 'ZoneManager' },
registered: { type: 'boolean', description: 'False for a name no plugin owns (Carbon’s built-in modules), grouped by its prefix.', example: true },
permissions: { type: 'array', items: { type: 'string', example: 'zonemanager.ignoreflag.nokits' } },
},
},
},
catalogue: {
groups: {
type: 'array',
items: { $ref: '#/components/schemas/RustPermissionCatalogueEntry' },
items: {
type: 'object',
properties: {
id: { type: 'integer', example: 12 },
name: { type: 'string', example: 'vip' },
title: { type: 'string', example: 'VIP' },
rank: { type: 'integer', example: 10 },
parent: { type: 'string', example: 'default' },
source: { type: 'string', example: 'imported' },
builtin: { type: 'boolean', example: false },
allServers: { type: 'boolean', example: false },
shared: { type: 'boolean', description: 'On more than one server; a change to it asks whether to change it everywhere or split this server off.', example: false },
servers: { type: 'array', items: { type: 'string', example: 'rust-oxide' } },
permissions: { type: 'array', items: { type: 'string', example: 'kits.vip' } },
members: { type: 'array', items: { type: 'object', properties: { userId: { type: 'integer' }, username: { type: 'string' }, steamIds: { type: 'array', items: { type: 'string' } } } } },
steamMembers: { type: 'array', items: { type: 'string', example: '76561198000000000' } },
chat: { type: 'object', nullable: true, additionalProperties: { type: 'string' } },
},
},
},
players: {
type: 'array',
items: {
type: 'object',
properties: {
steamId: { type: 'string', example: '76561198000000000' },
name: { type: 'string', nullable: true, example: 'Wanderer' },
account: { type: 'object', nullable: true, properties: { userId: { type: 'integer' }, username: { type: 'string' } } },
grants: {
type: 'array',
items: {
type: 'object',
properties: {
permission: { type: 'string', example: 'kits.vip' },
sources: { type: 'array', description: 'What puts it there: `userGrant`, `steamGrant` or `runGrant`, with its id and scope.', items: { type: 'object' } },
},
},
},
groups: { type: 'array', items: { type: 'string', example: 'vip' } },
},
},
},
excepted: { type: 'array', description: 'Grants that reach every server but this one (D190).', items: { type: 'object' } },
landed: { type: 'array', description: 'What has landed on this server: `grant <steamId> <permission>` and `member <steamId> <group>`.', items: { type: 'string' } },
report: {
type: 'object',
nullable: true,
properties: {
unresolved: { type: 'array', items: { type: 'string' } },
pending: { type: 'array', items: { type: 'string' } },
notLanded: { type: 'array', items: { type: 'string' } },
},
},
drift: { type: 'array', items: { $ref: '#/components/schemas/RustPermissionDrift' } },
},
},
RustPermissionSyncState: {
@@ -542,6 +534,7 @@ module.exports = {
dirty: { type: 'boolean', example: false },
lastAttemptAt: { type: 'string', format: 'date-time', nullable: true },
lastOkAt: { type: 'string', format: 'date-time', nullable: true },
importedAt: { type: 'string', format: 'date-time', nullable: true, description: 'When this server’s store was first imported (D198). Null until then; until then every sync imports.' },
error: {
type: 'string',
nullable: true,
@@ -593,6 +586,7 @@ module.exports = {
properties: {
permission: { type: 'string', example: 'kits.vip' },
servers: { type: 'array', items: { type: 'string', example: 'main' } },
owner: { type: 'string', nullable: true, description: 'The plugin that registered it, from the inventory (PLAN_REDESIGNS §0.1).', example: 'Kits' },
},
},
RustPermissionSyncResult: {
@@ -600,7 +594,7 @@ module.exports = {
description: 'What a forced sync produced (POST /admin/rust/permissions/sync).',
properties: {
servers: { type: 'array', items: { $ref: '#/components/schemas/RustPermissionSyncState' } },
drift: { type: 'array', items: { type: 'object' } },
drift: { type: 'array', items: { $ref: '#/components/schemas/RustPermissionDrift' } },
},
},
RustUserPermissions: {