// ── Admin · Rust · Permissions ──────────────────────────────────────────── // // Mounted under the admin tier's `/rust` prefix, so every path here is // `/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. const core = require('../../core') const express = core.express const permissions = require('./permissions.controller') const { requireRole, validate } = core.middleware 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 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, ) permissionsRouter.get( '/catalogue', // #swagger.tags = ['Admin · Rust'] // #swagger.summary = 'Permission names the servers have registered' // #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.get( '/servers/:serverId', // #swagger.tags = ['Admin · Rust'] // #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'), serverParam, validate, 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/:id', // #swagger.tags = ['Admin · Rust'] // #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'), groupParam, validate, permissions.deleteGroup, ) permissionsRouter.put( '/groups/:id/permissions', // #swagger.tags = ['Admin · Rust'] // #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'), 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.post( '/groups/:id/members/remove', // #swagger.tags = ['Admin · Rust'] // #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' } */ requireRole('admin'), groupParam, ...subject, ...hereOnly, validate, permissions.removeMember, ) permissionsRouter.post( '/groups/:id/members/clear', // #swagger.tags = ['Admin · Rust'] // #swagger.summary = 'Remove every member of a group' /* #swagger.responses[204] = { description: 'Emptied' } */ /* #swagger.responses[404] = { description: 'No such group' } */ requireRole('admin'), groupParam, ...hereOnly, validate, permissions.clearMembers, ) permissionsRouter.delete( '/exceptions/:id', // #swagger.tags = ['Admin · Rust'] // #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 exception' } */ requireRole('admin'), param('id').isInt({ min: 1 }).toInt(), validate, permissions.removeException, ) const driftId = param('id').isInt({ min: 1 }).toInt() permissionsRouter.post( '/drift/:id/adopt', // #swagger.tags = ['Admin · Rust'] // #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 change was a removal' } */ /* #swagger.responses[404] = { description: 'No such change' } */ requireRole('admin'), driftId, validate, permissions.adoptDrift, ) permissionsRouter.post( '/drift/:id/revoke', // #swagger.tags = ['Admin · Rust'] // #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[400] = { description: 'Only an addition can be revoked' } */ /* #swagger.responses[404] = { description: 'No such change' } */ requireRole('admin'), 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 = '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'), body('serverId').optional().isString().isLength({ min: 1, max: 64 }), validate, permissions.syncNow, ) module.exports = permissionsRouter