// ── 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. // // **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. const core = require('../../core') const express = core.express const permissions = require('./permissions.controller') const { requireRole, validate } = core.middleware const { body, param } = 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 permissionsRouter.get( '/', // #swagger.tags = ['Admin · Rust'] // #swagger.summary = 'The whole permission model' // #swagger.description = 'Groups with their permissions and members, direct grants, the drift each server reported, the option source of registered permission names, and the sync state of every configured server.' /* #swagger.responses[200] = { description: 'The authored model and what each game reported', content: { "application/json": { schema: { $ref: "#/components/schemas/RustPermissionModel" } } } } */ requireRole('admin'), permissions.overview, ) 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" } } } } */ requireRole('admin'), permissions.catalogue, ) permissionsRouter.put( '/groups/:name', // #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.' /* #swagger.responses[204] = { description: 'Saved' } */ /* #swagger.responses[400] = { description: 'Invalid body, or a scope naming no configured 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), validate, permissions.putGroup, ) permissionsRouter.delete( '/groups/:name', // #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.responses[204] = { description: 'Deleted' } */ /* #swagger.responses[404] = { description: 'No such group' } */ requireRole('admin'), param('name').isString().isLength({ min: 1, max: 64 }), validate, permissions.deleteGroup, ) permissionsRouter.post( '/groups/:name/members', // #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.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 }), validate, permissions.addMember, ) permissionsRouter.delete( '/groups/:name/members/:userId', // #swagger.tags = ['Admin · Rust'] // #swagger.summary = 'Take an account out of a group' /* #swagger.responses[204] = { description: 'Removed' } */ /* #swagger.responses[404] = { description: 'No such group, or that account is not in it' } */ requireRole('admin'), param('name').isString().isLength({ min: 1, max: 64 }), param('userId').isInt({ min: 1 }).toInt(), validate, permissions.removeMember, ) permissionsRouter.post( '/grants', // #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' } */ 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 }), validate, permissions.addGrant, ) permissionsRouter.delete( '/grants/: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.responses[204] = { description: 'Removed' } */ /* #swagger.responses[404] = { description: 'No such grant' } */ requireRole('admin'), param('id').isInt({ min: 1 }).toInt(), validate, permissions.removeGrant, ) 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.' /* #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' } */ requireRole('admin'), param('id').isInt({ min: 1 }).toInt(), validate, permissions.adoptDrift, ) 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.' /* #swagger.responses[202] = { description: 'Queued for the next sync' } */ /* #swagger.responses[404] = { description: 'No such drift' } */ requireRole('admin'), param('id').isInt({ min: 1 }).toInt(), validate, permissions.revokeDrift, ) 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.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'), body('serverId').optional().isString().isLength({ min: 1, max: 64 }), validate, permissions.syncNow, ) module.exports = permissionsRouter