Files
DarkflameServer/dCommon/AccountRules.h
Aaron Kimbrell 821b7c8767 feat(dashboard): permission grants count in every dashboard permission check
What someone may do on the dashboard is now their GM level's permissions plus the grants on their account, minus its
denies (PermissionGrants.h). A deny beats a grant; denies never apply to GM 9, and settings and permissions_manage stay
GM 9 only. The account's grants are read with every request (like its GM level), so a change applies at once, and
they are passed through every check: RouteUtils::Can, CanViewCharacter, the rank rules (self_* and manage_equal_rank),
routes guarded by a permission, the templates' `can`, the API documentation, API access, API key scopes (a key never
does more than its owner may now) and WebSocket subscriptions.

New permission grants_manage (GM 9 by default) and the API to manage grants: GET /api/grants/catalog, GET /api/grants,
POST /api/grants, POST /api/grants/:id/remove. Nobody grants or takes away what they don't hold themselves (a
permission, every permission of a group, a command they may use, every command up to their own GM level), and only on
accounts the rank rules let them manage (their own with self_moderation). Commands with a fixed level or a floor
above GM 1 (/execute) can't be granted. Every change goes in the audit log (grant_permission, deny_permission,
remove_grant). Also: the Showcase gate and the traffic subscription now check their permission by name.

Check: grant a GM 2 account accounts_ban (it can ban, and the Ban button shows); deny a GM 8 account accounts_view (the
accounts list is refused); give an expiry a minute ahead and see it stop; try to grant a permission your account
doesn't have (refused); dWebTests PermissionGrantsTests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 01:16:38 -05:00

117 lines
6.1 KiB
C++

#pragma once
#include <cstdint>
#include <string>
namespace ApiKeys { struct Scope; }
namespace PermissionGrants { struct Held; }
/**
* Who staff may use their tools on: the self and rank rules shared by the dashboard (RouteUtils) and the in-game slash
* commands (SlashCommandHandler). The functions here are pure; Permissions.h decides the self_* and manage_equal_rank
* levels they are given.
*/
namespace AccountRules {
// GM 9: may do anything to anyone, including themselves and other GM 9s
constexpr uint8_t OPERATOR_LEVEL = 9;
/**
* What a staff action on an account does. It decides which permission a staff member below GM 9 needs to use the
* tool on their own account or characters (they still need the tool's own permission too).
*/
enum class eAccountAction : uint8_t {
TOOLS, // nothing is gained: rescue or move a character, kick, sign out everywhere, email a reset link (self_tools)
ITEMS, // gives items, coins or progress: edit or restore characters, missions, mail items (self_items)
MODERATION, // changes a moderation record or account security: ban, mute, lock, strikes, GM level, password, delete (self_moderation)
};
// The permission that lets staff below GM 9 do this kind of action to their own account
inline const char* SelfPermission(eAccountAction action) {
switch (action) {
case eAccountAction::TOOLS: return "self_tools";
case eAccountAction::ITEMS: return "self_items";
default: return "self_moderation";
}
}
// The permission that lets staff below GM 9 act on accounts with their own GM level
constexpr const char* EQUAL_RANK_PERMISSION = "manage_equal_rank";
// Why an actor may not act on an account (NONE: they may)
enum class eManageDenial : uint8_t { NONE, SELF, EQUAL_RANK, HIGHER_RANK };
/**
* Whether an actor may act on a target account. GM 9 may act on anyone, themselves included. Below GM 9: their own
* account only with selfAllowed (the self_* permission for the action), an account at their own level only with
* equalAllowed (manage_equal_rank), and never an account above their own level, whatever the permissions say.
*/
inline eManageDenial ManageDenial(uint8_t actorLevel, uint32_t actorAccountId, uint8_t targetLevel, uint32_t targetAccountId, bool selfAllowed, bool equalAllowed) {
if (actorLevel >= OPERATOR_LEVEL) return eManageDenial::NONE;
if (actorAccountId != 0 && actorAccountId == targetAccountId) return selfAllowed ? eManageDenial::NONE : eManageDenial::SELF;
if (targetLevel > actorLevel) return eManageDenial::HIGHER_RANK;
if (targetLevel == actorLevel && !equalAllowed) return eManageDenial::EQUAL_RANK;
return eManageDenial::NONE;
}
inline bool CanManageAccount(uint8_t actorLevel, uint32_t actorAccountId, uint8_t targetLevel, uint32_t targetAccountId, bool selfAllowed, bool equalAllowed) {
return ManageDenial(actorLevel, actorAccountId, targetLevel, targetAccountId, selfAllowed, equalAllowed) == eManageDenial::NONE;
}
/**
* Safety rail for the server, not a limit on the operator: demoting, banning, locking or deleting a GM 9 account is
* refused when no other GM 9 account that can still sign in (not banned or locked) would be left.
*/
inline bool RemovesLastOperator(uint8_t targetLevel, uint32_t otherActiveOperators) {
return targetLevel >= OPERATOR_LEVEL && otherActiveOperators == 0;
}
// Whether an actor may grant a GM level: never above their own, and only operators may create peers
inline bool CanGrantGmLevel(uint8_t actorLevel, uint8_t newLevel) {
if (newLevel > OPERATOR_LEVEL) return false;
return actorLevel >= OPERATOR_LEVEL || newLevel < actorLevel;
}
// Why an action was refused, for the person who tried it (empty for NONE)
inline std::string DenialMessage(eManageDenial denial, eAccountAction action) {
switch (denial) {
case eManageDenial::SELF:
return std::string("Your GM level may not do this to your own account or characters (the ") + SelfPermission(action) + " permission)";
case eManageDenial::EQUAL_RANK:
return std::string("You cannot manage an account with the same GM level as yours (the ") + EQUAL_RANK_PERMISSION + " permission)";
case eManageDenial::HIGHER_RANK:
return "You cannot manage an account with a higher GM level than yours";
default:
return "";
}
}
// The last-operator refusal, e.g. what = "banned"
inline std::string LastOperatorMessage(const std::string& what) {
return "This is the last GM 9 account that can sign in, so it can't be " + what +
". Make another GM 9 account first, so the server always has someone who can manage it.";
}
/**
* ManageDenial with the self_* and manage_equal_rank levels as the Permissions page (or the config) sets them now.
* Used by the dashboard and the world servers alike.
*/
eManageDenial ManageDenialNow(uint8_t actorLevel, uint32_t actorAccountId, uint8_t targetLevel, uint32_t targetAccountId, eAccountAction action);
/**
* The self and rank rules for an API key, on top of what its owner may do (ownerDenial, from ManageDenial). The key
* needs the self_* permission in its scope to act on its owner's own account, and manage_equal_rank to act on an
* account at (or, for GM 9, also at) the owner's level - even when the owner is GM 9, who needs neither.
*/
inline eManageDenial ScopedManageDenial(eManageDenial ownerDenial, uint8_t actorLevel, uint32_t actorAccountId, uint8_t targetLevel, uint32_t targetAccountId,
bool scopeSelf, bool scopeEqual) {
if (ownerDenial != eManageDenial::NONE) return ownerDenial;
if (actorAccountId != 0 && actorAccountId == targetAccountId) return scopeSelf ? eManageDenial::NONE : eManageDenial::SELF;
if (targetLevel >= actorLevel && !scopeEqual) return eManageDenial::EQUAL_RANK;
return eManageDenial::NONE;
}
// ManageDenialNow for a request made with an API key (scope nullptr: a browser session, the plain rules). grants: the
// actor's permission grants (PermissionGrants.h), which count for self_* and manage_equal_rank; nullptr: the level alone.
eManageDenial ManageDenialNow(uint8_t actorLevel, uint32_t actorAccountId, uint8_t targetLevel, uint32_t targetAccountId, eAccountAction action, const ApiKeys::Scope* scope, const PermissionGrants::Held* grants = nullptr);
}