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:
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,53 @@
|
||||
// ── Admin · Rust · Permissions ────────────────────────────────────────────
|
||||
//
|
||||
// Mounted under the admin tier's `/rust` prefix, so every path here is
|
||||
// `/api/v1/admin/rust/permissions…`. It is a second router rather than more
|
||||
// routes on `rust.router.js` because it is a second subject: that one configures
|
||||
// the bridge, this one authors privilege inside somebody's game.
|
||||
// `/api/v1/admin/rust/permissions…`. The permission manager (PLAN_REDESIGNS §1):
|
||||
// the site owns every permission and group on every server (D160), a server at a
|
||||
// time on the screen (D162), with each server's policy for a change made in the
|
||||
// game (D161).
|
||||
//
|
||||
// **Every route is `requireRole('admin')`.** The admin tier's own gate admits
|
||||
// editors and moderators, and a moderator being able to grant themselves
|
||||
// `kits.admin` on six servers is the whole of R1's "a weak link is now a
|
||||
// privilege-escalation path" arriving through the front door instead. The tier
|
||||
// gate is not re-implemented; this is one gate on top of it, exactly as the
|
||||
// server-configuration routes do it.
|
||||
//
|
||||
// There is no module-declared site permission to gate these more finely with —
|
||||
// `MODULE_API.md` has no such member at 1.10.0 — so role is the whole of the
|
||||
// available vocabulary, and `admin` is the honest choice within it.
|
||||
// privilege-escalation path" arriving through the front door instead.
|
||||
|
||||
const core = require('../../core')
|
||||
|
||||
const express = core.express
|
||||
const permissions = require('./permissions.controller')
|
||||
const { requireRole, validate } = core.middleware
|
||||
const { body, param } = core.validator
|
||||
const { body, param, query } = core.validator
|
||||
|
||||
const permissionsRouter = express.Router()
|
||||
|
||||
/** A permission or group name, as both mod frameworks store them. */
|
||||
const NAME = /^[a-z0-9][a-z0-9._-]{0,127}$/i
|
||||
|
||||
/** A Steam id: seventeen digits in practice, digits always. */
|
||||
const STEAM = /^\d{5,20}$/
|
||||
|
||||
const serverParam = param('serverId').isString().isLength({ min: 1, max: 64 })
|
||||
const groupParam = param('id').isInt({ min: 1 }).toInt()
|
||||
|
||||
/** A subject: a Steam id, a website account id, or a username. */
|
||||
const subject = [
|
||||
body('steamId').optional().isString().matches(STEAM),
|
||||
body('userId').optional().isInt({ min: 1 }).toInt(),
|
||||
body('username').optional().isString().trim().isLength({ min: 1, max: 64 }),
|
||||
]
|
||||
|
||||
/** "Here only" on a shared group: split this server's copy off first (D190). */
|
||||
const hereOnly = [
|
||||
body('onlyHere').optional().isBoolean().toBoolean(),
|
||||
body('serverId').optional().isString().isLength({ min: 1, max: 64 }),
|
||||
]
|
||||
|
||||
permissionsRouter.get(
|
||||
'/',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'The whole permission model'
|
||||
// #swagger.description = 'Groups with their permissions, members and BetterChat style (`chat`, or null), direct grants, the drift each server reported, the option source of registered permission names, and the sync state of every configured server. A drift row of kind `chat-field` is a style field somebody changed in game: `subject` is the group, `object` the field and `detail` what the game holds. `chatFields` lists the twelve BetterChat fields with their types and defaults, for the style editor.'
|
||||
/* #swagger.responses[200] = { description: 'The authored model and what each game reported', content: { "application/json": { schema: { $ref: "#/components/schemas/RustPermissionModel" } } } } */
|
||||
// #swagger.summary = 'The permission manager: servers, policies and what waits for a person'
|
||||
// #swagger.description = 'Every configured server with its policy for a change made in the game (`auto-adopt`, `adopt`, `revoke`; D161) and its sync state, and every row waiting for a person: each change under `adopt`, an event’s grant removed in the game, a hand-edited style field, and notices of a shared group split off for one server (D190). `chatFields` lists the twelve BetterChat fields for the style editor.'
|
||||
/* #swagger.responses[200] = { description: 'The overview', content: { "application/json": { schema: { $ref: "#/components/schemas/RustPermissionOverview" } } } } */
|
||||
requireRole('admin'),
|
||||
permissions.overview,
|
||||
)
|
||||
@@ -42,118 +56,259 @@ permissionsRouter.get(
|
||||
'/catalogue',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Permission names the servers have registered'
|
||||
// #swagger.description = 'What the loaded plugins on each configured server have registered, cached from the last sync. It is the option source for the authoring form: a permission no server knows cannot be granted, because `GrantUserPermission` silently does nothing for an unregistered name.'
|
||||
/* #swagger.responses[200] = { description: 'Every registered name, and which servers know it', content: { "application/json": { schema: { $ref: "#/components/schemas/RustPermissionCatalogue" } } } } */
|
||||
// #swagger.description = 'Every permission name the loaded plugins on each server registered, from the last inventory, with the plugin that registered it (`owner`) — null for a name no plugin owns, which on Carbon is its built-in modules’.'
|
||||
/* #swagger.responses[200] = { description: 'Every registered name, which servers know it, and who registered it', content: { "application/json": { schema: { $ref: "#/components/schemas/RustPermissionCatalogue" } } } } */
|
||||
requireRole('admin'),
|
||||
permissions.catalogue,
|
||||
)
|
||||
|
||||
permissionsRouter.put(
|
||||
'/groups/:name',
|
||||
permissionsRouter.get(
|
||||
'/servers/:serverId',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Create or update a permission group'
|
||||
// #swagger.description = 'Writes the group and the permissions it carries in one request, because they are one idea on the form. `scope` is a server id or `*` for the whole fleet. The group is mirrored into each in-scope game as a real group, so third-party plugins that read group membership see it. `chat` is the group’s BetterChat style: all twelve fields (`Priority`, `Title`, `TitleColor`, `TitleSize`, `TitleHidden`, `TitleHiddenIfNotPrimary`, `UsernameColor`, `UsernameSize`, `MessageColor`, `MessageSize`, `ChatFormat`, `ConsoleFormat`), each as text; `null` removes the style, which removes the group from BetterChat on the next sync; absent leaves it alone. A format must hold `{Message}` exactly once. A 400 carries one sentence per problem in `errors`.'
|
||||
/* #swagger.responses[204] = { description: 'Saved' } */
|
||||
/* #swagger.responses[400] = { description: 'Invalid body, or a scope naming no configured server' } */
|
||||
// #swagger.summary = 'One server’s permissions, as the screen shows them'
|
||||
// #swagger.description = 'Plugins grouped by the plugin that registered each permission, never by prefix; the groups on the server and where else each one is (D189); every player holding anything there, named by linked account and in-game name, or Steam id (D163); what the site wants and why, what has landed, and what the last report said did not — the facts every toggle’s state is read from (U-1).'
|
||||
/* #swagger.responses[200] = { description: 'The server view', content: { "application/json": { schema: { $ref: "#/components/schemas/RustPermissionServer" } } } } */
|
||||
/* #swagger.responses[404] = { description: 'No such server' } */
|
||||
requireRole('admin'),
|
||||
param('name').matches(NAME).withMessage('a group name is letters, digits, dots, dashes and underscores'),
|
||||
body('title').optional().isString().trim().isLength({ max: 120 }),
|
||||
body('rank').optional().isInt({ min: -1000, max: 1000 }).toInt(),
|
||||
body('scope').optional().isString().isLength({ min: 1, max: 64 }),
|
||||
body('permissions').optional().isArray({ max: 500 }),
|
||||
body('permissions.*').isString().matches(NAME),
|
||||
// Shape only; the twelve fields and their rules are `chatStyle.validateStyle`'s,
|
||||
// in the controller, so the form gets one sentence per problem.
|
||||
body('chat').optional({ values: 'null' }).isObject().withMessage('chat is an object of BetterChat fields, or null'),
|
||||
serverParam,
|
||||
validate,
|
||||
permissions.putGroup,
|
||||
permissions.server,
|
||||
)
|
||||
|
||||
permissionsRouter.get(
|
||||
'/servers/:serverId/players',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Find a player seen on a server'
|
||||
// #swagger.description = 'By in-game name, Steam id or linked account name, among the players this server has seen — to grant to somebody who holds nothing yet. At most 25, newest first.'
|
||||
/* #swagger.parameters['q'] = { in: 'query', description: 'Part of a name, Steam id or account name', type: 'string' } */
|
||||
/* #swagger.responses[200] = { description: 'Matching players' } */
|
||||
/* #swagger.responses[404] = { description: 'No such server' } */
|
||||
requireRole('admin'),
|
||||
serverParam,
|
||||
query('q').optional().isString().isLength({ max: 64 }),
|
||||
validate,
|
||||
permissions.players,
|
||||
)
|
||||
|
||||
permissionsRouter.put(
|
||||
'/servers/:serverId/policy',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Set what a change made in the game becomes'
|
||||
// #swagger.description = '`auto-adopt` (the default) makes it the site’s own for that server; `adopt` puts each one to a person; `revoke` undoes it (D161).'
|
||||
/* #swagger.responses[204] = { description: 'Saved' } */
|
||||
/* #swagger.responses[400] = { description: 'Not a policy' } */
|
||||
/* #swagger.responses[404] = { description: 'No such server' } */
|
||||
requireRole('admin'),
|
||||
serverParam,
|
||||
body('policy').isString().isIn(['auto-adopt', 'adopt', 'revoke']),
|
||||
validate,
|
||||
permissions.setPolicy,
|
||||
)
|
||||
|
||||
permissionsRouter.post(
|
||||
'/servers/:serverId/grant',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Grant permissions to one player (a toggle, or Grant all)'
|
||||
// #swagger.description = 'On this server, or with `everywhere` on every server. A linked Steam id is granted as its website account and reaches every account the person links (D28); an unlinked one as itself (D188). A grant kept off this server by an exception has the exception removed instead.'
|
||||
/* #swagger.responses[200] = { description: 'How many were granted, and how many exceptions removed' } */
|
||||
/* #swagger.responses[400] = { description: 'No subject named' } */
|
||||
/* #swagger.responses[404] = { description: 'No such server' } */
|
||||
requireRole('admin'),
|
||||
serverParam,
|
||||
...subject,
|
||||
body('permissions').isArray({ min: 1, max: 500 }),
|
||||
body('permissions.*').isString().matches(NAME),
|
||||
body('everywhere').optional().isBoolean().toBoolean(),
|
||||
validate,
|
||||
permissions.grant,
|
||||
)
|
||||
|
||||
permissionsRouter.post(
|
||||
'/servers/:serverId/revoke',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Revoke permissions from one player (a toggle, or Revoke all)'
|
||||
// #swagger.description = 'Every direct grant that puts the permission on this server stops doing so: one scoped to this server is deleted; one that reaches further is deleted with `everywhere`, and otherwise gains an exception for this server (D190). What the player holds through a group or from an event is reported back in `untouched`, not changed.'
|
||||
/* #swagger.responses[200] = { description: 'How many grants changed, and what could not be' } */
|
||||
/* #swagger.responses[400] = { description: 'No subject named' } */
|
||||
/* #swagger.responses[404] = { description: 'No such server' } */
|
||||
requireRole('admin'),
|
||||
serverParam,
|
||||
...subject,
|
||||
body('permissions').isArray({ min: 1, max: 500 }),
|
||||
body('permissions.*').isString().matches(NAME),
|
||||
body('everywhere').optional().isBoolean().toBoolean(),
|
||||
validate,
|
||||
permissions.revoke,
|
||||
)
|
||||
|
||||
permissionsRouter.post(
|
||||
'/servers/:serverId/groups',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Create a group on one server'
|
||||
// #swagger.description = 'A group belongs to one server unless an admin shares it (D189). A server cannot have two groups of one name.'
|
||||
/* #swagger.responses[201] = { description: 'Created; the body carries its id' } */
|
||||
/* #swagger.responses[404] = { description: 'No such server' } */
|
||||
/* #swagger.responses[409] = { description: 'This server already has a group of that name' } */
|
||||
requireRole('admin'),
|
||||
serverParam,
|
||||
body('name').isString().matches(NAME).withMessage('a group name is letters, digits, dots, dashes and underscores'),
|
||||
body('title').optional().isString().isLength({ max: 120 }),
|
||||
body('rank').optional().isInt({ min: -1000, max: 1000 }).toInt(),
|
||||
body('parent').optional().isString().isLength({ max: 64 }),
|
||||
validate,
|
||||
permissions.createGroup,
|
||||
)
|
||||
|
||||
permissionsRouter.patch(
|
||||
'/groups/:id',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Change a group’s title, rank, parent or chat style'
|
||||
// #swagger.description = 'The title is kept verbatim. `chat` is the group’s BetterChat style — all twelve fields as text; `null` removes it; absent leaves it. With `onlyHere` and `serverId` on a shared group, that server’s copy is split off first and only it changes (D190).'
|
||||
/* #swagger.responses[200] = { description: 'Saved; `id` is the group that changed, a new copy if it was split' } */
|
||||
/* #swagger.responses[400] = { description: 'An invalid style' } */
|
||||
/* #swagger.responses[404] = { description: 'No such group' } */
|
||||
requireRole('admin'),
|
||||
groupParam,
|
||||
body('title').optional().isString().isLength({ max: 120 }),
|
||||
body('rank').optional().isInt({ min: -1000, max: 1000 }).toInt(),
|
||||
body('parent').optional().isString().isLength({ max: 64 }),
|
||||
body('chat').optional({ values: 'null' }).isObject().withMessage('chat is an object of BetterChat fields, or null'),
|
||||
...hereOnly,
|
||||
validate,
|
||||
permissions.updateGroup,
|
||||
)
|
||||
|
||||
permissionsRouter.delete(
|
||||
'/groups/:name',
|
||||
'/groups/:id',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Delete a permission group'
|
||||
// #swagger.description = 'Removes the group, its permission list and its membership from the site. The next sync retires the group from every server it had been pushed to — a group the site authored and has withdrawn is removed from the game, unlike one somebody created by hand.'
|
||||
// #swagger.summary = 'Delete a group'
|
||||
// #swagger.description = 'From the site and, at the next sync, from every server it is on. A built-in group (`default`, `admin`, Carbon’s `moderator`) is never removed from a game.'
|
||||
/* #swagger.responses[204] = { description: 'Deleted' } */
|
||||
/* #swagger.responses[404] = { description: 'No such group' } */
|
||||
requireRole('admin'),
|
||||
param('name').isString().isLength({ min: 1, max: 64 }),
|
||||
groupParam,
|
||||
validate,
|
||||
permissions.deleteGroup,
|
||||
)
|
||||
|
||||
permissionsRouter.post(
|
||||
'/groups/:name/members',
|
||||
permissionsRouter.put(
|
||||
'/groups/:id/permissions',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Put an account in a group'
|
||||
// #swagger.description = 'Membership is authored against a website user and reaches every Steam account they have linked. A member who has never connected to a server cannot be placed in its store yet — the sync reports them as pending and the membership lands on their first connection.'
|
||||
/* #swagger.responses[204] = { description: 'Added' } */
|
||||
// #swagger.summary = 'Replace what a group carries'
|
||||
// #swagger.description = 'The whole list: the screen’s toggles, Grant all and Revoke all each send it. With `onlyHere` and `serverId` on a shared group, that server’s copy is split off first (D190).'
|
||||
/* #swagger.responses[200] = { description: 'Saved; `id` is the group that changed' } */
|
||||
/* #swagger.responses[404] = { description: 'No such group' } */
|
||||
requireRole('admin'),
|
||||
param('name').isString().isLength({ min: 1, max: 64 }),
|
||||
// Either identifier: the screen sends a name, the panel inside core's own user
|
||||
// page already holds an id.
|
||||
body('userId').optional().isInt({ min: 1 }).toInt(),
|
||||
body('username').optional().isString().trim().isLength({ min: 1, max: 64 }),
|
||||
groupParam,
|
||||
body('permissions').isArray({ max: 2000 }),
|
||||
body('permissions.*').isString().matches(NAME),
|
||||
...hereOnly,
|
||||
validate,
|
||||
permissions.setGroupPermissions,
|
||||
)
|
||||
|
||||
permissionsRouter.put(
|
||||
'/groups/:id/servers',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Share a group, or stop sharing it'
|
||||
// #swagger.description = '`allServers` puts it on every server, including servers added later; otherwise `servers` lists them (D189). A chosen server that already has its own group of this name answers 409 with each one’s permissions; repeat with `replace` listing the ids to replace.'
|
||||
/* #swagger.responses[200] = { description: 'Saved' } */
|
||||
/* #swagger.responses[404] = { description: 'No such group' } */
|
||||
/* #swagger.responses[409] = { description: 'A chosen server has its own group of this name' } */
|
||||
requireRole('admin'),
|
||||
groupParam,
|
||||
body('allServers').optional().isBoolean().toBoolean(),
|
||||
body('servers').optional().isArray({ max: 200 }),
|
||||
body('servers.*').isString().isLength({ min: 1, max: 64 }),
|
||||
body('replace').optional().isArray({ max: 200 }),
|
||||
body('replace.*').isInt({ min: 1 }).toInt(),
|
||||
validate,
|
||||
permissions.setGroupServers,
|
||||
)
|
||||
|
||||
permissionsRouter.post(
|
||||
'/groups/:id/split',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Give one server its own copy of a shared group'
|
||||
// #swagger.description = 'The copy carries the same permissions, members and style, on that server only, and the shared group stops covering it (D190).'
|
||||
/* #swagger.responses[200] = { description: 'Split; `id` is the copy' } */
|
||||
/* #swagger.responses[404] = { description: 'No such group' } */
|
||||
/* #swagger.responses[409] = { description: 'That group is not on that server' } */
|
||||
requireRole('admin'),
|
||||
groupParam,
|
||||
body('serverId').isString().isLength({ min: 1, max: 64 }),
|
||||
validate,
|
||||
permissions.splitGroup,
|
||||
)
|
||||
|
||||
permissionsRouter.post(
|
||||
'/groups/:id/members',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Put a player in a group'
|
||||
// #swagger.description = 'A Steam id is a member as itself (D188); a website account reaches every Steam id it links (D28). A member who has never connected to a server is pending there until their first connection.'
|
||||
/* #swagger.responses[204] = { description: 'Added' } */
|
||||
/* #swagger.responses[400] = { description: 'No subject named' } */
|
||||
/* #swagger.responses[404] = { description: 'No such group' } */
|
||||
requireRole('admin'),
|
||||
groupParam,
|
||||
...subject,
|
||||
...hereOnly,
|
||||
validate,
|
||||
permissions.addMember,
|
||||
)
|
||||
|
||||
permissionsRouter.delete(
|
||||
'/groups/:name/members/:userId',
|
||||
permissionsRouter.post(
|
||||
'/groups/:id/members/remove',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Take an account out of a group'
|
||||
// #swagger.summary = 'Take a player out of a group'
|
||||
// #swagger.description = 'Both ways they can be in it: as the Steam id, and as the website account it is linked to.'
|
||||
/* #swagger.responses[204] = { description: 'Removed' } */
|
||||
/* #swagger.responses[404] = { description: 'No such group, or that account is not in it' } */
|
||||
/* #swagger.responses[404] = { description: 'No such group' } */
|
||||
requireRole('admin'),
|
||||
param('name').isString().isLength({ min: 1, max: 64 }),
|
||||
param('userId').isInt({ min: 1 }).toInt(),
|
||||
groupParam,
|
||||
...subject,
|
||||
...hereOnly,
|
||||
validate,
|
||||
permissions.removeMember,
|
||||
)
|
||||
|
||||
permissionsRouter.post(
|
||||
'/grants',
|
||||
'/groups/:id/members/clear',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Grant one permission to one person'
|
||||
// #swagger.description = 'A direct grant, authored against a website user and pushed to every Steam account they have linked. Unlike group membership it reaches a player who has never connected to the server, which is what an entitlement earned on the website has to do.'
|
||||
/* #swagger.responses[201] = { description: 'Granted' } */
|
||||
/* #swagger.responses[200] = { description: 'They already held it; nothing changed' } */
|
||||
/* #swagger.responses[400] = { description: 'Invalid body, or a scope naming no configured server' } */
|
||||
/* #swagger.responses[404] = { description: 'No account on this site has that name' } */
|
||||
// #swagger.summary = 'Remove every member of a group'
|
||||
/* #swagger.responses[204] = { description: 'Emptied' } */
|
||||
/* #swagger.responses[404] = { description: 'No such group' } */
|
||||
requireRole('admin'),
|
||||
body('userId').optional().isInt({ min: 1 }).toInt(),
|
||||
body('username').optional().isString().trim().isLength({ min: 1, max: 64 }),
|
||||
body('permission').isString().matches(NAME),
|
||||
body('scope').optional().isString().isLength({ min: 1, max: 64 }),
|
||||
body('note').optional().isString().isLength({ max: 255 }),
|
||||
groupParam,
|
||||
...hereOnly,
|
||||
validate,
|
||||
permissions.addGrant,
|
||||
permissions.clearMembers,
|
||||
)
|
||||
|
||||
permissionsRouter.delete(
|
||||
'/grants/:id',
|
||||
'/exceptions/:id',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Remove a grant'
|
||||
// #swagger.description = 'The next sync revokes it in every in-scope game. A player who has already used what it allowed keeps what they did with it — the grant is the entitlement, not the consumption.'
|
||||
// #swagger.summary = 'Give a grant back to the one server it was kept off'
|
||||
/* #swagger.responses[204] = { description: 'Removed' } */
|
||||
/* #swagger.responses[404] = { description: 'No such grant' } */
|
||||
/* #swagger.responses[404] = { description: 'No such exception' } */
|
||||
requireRole('admin'),
|
||||
param('id').isInt({ min: 1 }).toInt(),
|
||||
validate,
|
||||
permissions.removeGrant,
|
||||
permissions.removeException,
|
||||
)
|
||||
|
||||
const driftId = param('id').isInt({ min: 1 }).toInt()
|
||||
|
||||
permissionsRouter.post(
|
||||
'/drift/:id/adopt',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Adopt a hand edit'
|
||||
// #swagger.description = 'Records a grant or membership somebody made in game as one the site authors, so it stops being reported and starts being maintained. It needs a website account holding that Steam id; without one there is nobody to author it against, and the answer is to revoke it or to ask the player to link. For a `chat-field` row it copies the game’s value into the group’s style — which every server in the group’s scope is then pushed — and answers 409 when the group has no style or the value is not one the site accepts.'
|
||||
// #swagger.summary = 'Adopt a change made in the game'
|
||||
// #swagger.description = 'An addition (or a changed group) becomes the site’s own for that server — the same write auto-adopt makes. For a `chat-field` row, the game’s value becomes the group’s style; 409 when the group has no style or the value is not one the site accepts.'
|
||||
/* #swagger.responses[204] = { description: 'Adopted' } */
|
||||
/* #swagger.responses[400] = { description: 'That kind of drift cannot be adopted' } */
|
||||
/* #swagger.responses[409] = { description: 'That Steam account is linked to nobody on this site' } */
|
||||
/* #swagger.responses[400] = { description: 'That change was a removal' } */
|
||||
/* #swagger.responses[404] = { description: 'No such change' } */
|
||||
requireRole('admin'),
|
||||
param('id').isInt({ min: 1 }).toInt(),
|
||||
driftId,
|
||||
validate,
|
||||
permissions.adoptDrift,
|
||||
)
|
||||
@@ -161,21 +316,63 @@ permissionsRouter.post(
|
||||
permissionsRouter.post(
|
||||
'/drift/:id/revoke',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Revoke a hand edit'
|
||||
// #swagger.description = 'Queues the removal rather than performing it: a server that is down keeps the instruction until it comes back. This is the only way the site removes something it did not put there — a sync never does it on its own. For a `chat-field` row it puts the site’s value back over the hand edit on the next sync.'
|
||||
// #swagger.summary = 'Undo an addition made in the game'
|
||||
// #swagger.description = 'Queued: a server that is down keeps the instruction until it comes back. For a `chat-field` row it puts the site’s value back.'
|
||||
/* #swagger.responses[202] = { description: 'Queued for the next sync' } */
|
||||
/* #swagger.responses[404] = { description: 'No such drift' } */
|
||||
/* #swagger.responses[400] = { description: 'Only an addition can be revoked' } */
|
||||
/* #swagger.responses[404] = { description: 'No such change' } */
|
||||
requireRole('admin'),
|
||||
param('id').isInt({ min: 1 }).toInt(),
|
||||
driftId,
|
||||
validate,
|
||||
permissions.revokeDrift,
|
||||
)
|
||||
|
||||
permissionsRouter.post(
|
||||
'/drift/:id/accept',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Accept a removal made in the game'
|
||||
// #swagger.description = 'The site stops giving it on that server: deleted when it was that server’s alone, an exception when it reached further, a split when it was a shared group’s (D190).'
|
||||
/* #swagger.responses[204] = { description: 'Accepted' } */
|
||||
/* #swagger.responses[400] = { description: 'Only a removal can be accepted' } */
|
||||
/* #swagger.responses[404] = { description: 'No such change' } */
|
||||
requireRole('admin'),
|
||||
driftId,
|
||||
validate,
|
||||
permissions.acceptDrift,
|
||||
)
|
||||
|
||||
permissionsRouter.post(
|
||||
'/drift/:id/restore',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Put back what the game removed or changed'
|
||||
// #swagger.description = 'The next sync pushes the site’s version again.'
|
||||
/* #swagger.responses[202] = { description: 'Queued for the next sync' } */
|
||||
/* #swagger.responses[400] = { description: 'Only a removal or a changed group can be put back' } */
|
||||
/* #swagger.responses[404] = { description: 'No such change' } */
|
||||
requireRole('admin'),
|
||||
driftId,
|
||||
validate,
|
||||
permissions.restoreDrift,
|
||||
)
|
||||
|
||||
permissionsRouter.post(
|
||||
'/drift/:id/dismiss',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Dismiss a notice'
|
||||
// #swagger.description = 'A split notice (D190) or an event’s grant that was pushed back. Nothing in any game changes.'
|
||||
/* #swagger.responses[204] = { description: 'Dismissed' } */
|
||||
/* #swagger.responses[404] = { description: 'No such notice' } */
|
||||
requireRole('admin'),
|
||||
driftId,
|
||||
validate,
|
||||
permissions.dismissDrift,
|
||||
)
|
||||
|
||||
permissionsRouter.post(
|
||||
'/sync',
|
||||
// #swagger.tags = ['Admin · Rust']
|
||||
// #swagger.summary = 'Push the permission set now'
|
||||
// #swagger.description = 'Runs the reconciliation loop’s pass immediately, for one server or for all of them, and answers with what each one reported. The loop does this on its own; the button exists so an operator who has just changed something can see it land, and finds out at once when a server is unreachable.'
|
||||
// #swagger.summary = 'Read, reconcile and push now'
|
||||
// #swagger.description = 'Runs the loop’s pass immediately for one server or all of them — read the store, settle what changed in the game by the server’s policy, push — and answers with each server’s state.'
|
||||
/* #swagger.responses[200] = { description: 'The state of every server after the pass', content: { "application/json": { schema: { $ref: "#/components/schemas/RustPermissionSyncResult" } } } } */
|
||||
/* #swagger.responses[404] = { description: 'No such server' } */
|
||||
requireRole('admin'),
|
||||
|
||||
@@ -83,32 +83,41 @@ async function listPermissions(req, res) {
|
||||
const userId = Number(req.params.id)
|
||||
|
||||
try {
|
||||
const [groups, groupPermissions, members, grants, allLinks] = await Promise.all([
|
||||
const [groups, groupServers, groupPermissions, members, grants, allLinks] = await Promise.all([
|
||||
permissionsDb.listGroups(),
|
||||
permissionsDb.listGroupServers(),
|
||||
permissionsDb.listGroupPermissions(),
|
||||
permissionsDb.listGroupMembers(),
|
||||
permissionsDb.listGrants({ userId }),
|
||||
permissionsDb.listLinks(),
|
||||
])
|
||||
|
||||
const theirs = new Set(
|
||||
members.filter((row) => row.userId === userId).map((row) => row.groupName),
|
||||
)
|
||||
const theirs = new Set(members.filter((row) => row.userId === userId).map((row) => row.groupId))
|
||||
|
||||
const carried = new Map()
|
||||
for (const row of groupPermissions) {
|
||||
if (!carried.has(row.groupName)) carried.set(row.groupName, [])
|
||||
carried.get(row.groupName).push(row.permission)
|
||||
if (!carried.has(row.groupId)) carried.set(row.groupId, [])
|
||||
carried.get(row.groupId).push(row.permission)
|
||||
}
|
||||
|
||||
// A group is on one server unless it is shared (D189): `scope` says `*`
|
||||
// for every server, or lists the servers it is on.
|
||||
const on = new Map()
|
||||
for (const row of groupServers) {
|
||||
if (!row.included) continue
|
||||
if (!on.has(row.groupId)) on.set(row.groupId, [])
|
||||
on.get(row.groupId).push(row.serverId)
|
||||
}
|
||||
|
||||
res.json({
|
||||
groups: groups
|
||||
.filter((group) => theirs.has(group.name))
|
||||
.filter((group) => theirs.has(group.id))
|
||||
.map((group) => ({
|
||||
id: group.id,
|
||||
name: group.name,
|
||||
title: group.title,
|
||||
scope: group.scope,
|
||||
permissions: carried.get(group.name) || [],
|
||||
scope: group.allServers ? permissions.FLEET : (on.get(group.id) || []).join(','),
|
||||
permissions: carried.get(group.id) || [],
|
||||
})),
|
||||
grants: permissions.collapseGrants(grants).map((grant) => ({
|
||||
id: grant.id,
|
||||
|
||||
Reference in New Issue
Block a user