feat(rust): the permission manager — the site owns the whole store (D160-D163, D188-D198)
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:
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user