Files
DarkflameServer/dCommon/AccountRules.h
Aaron Kimbrell 0794cf48dd feat(permissions): API key scopes that narrow permission and rank checks
A key's effective permission is its scope AND its owner's current
permission. Scoped variants of Allowed, CanViewCharacter, ForLevel and
ManageDenialNow; keys need self_* and manage_equal_rank in their scope
to act on their owner or equal ranks, even for GM 9 owners. Adds the
api_keys_manage permission and NotGrantable for key creation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 22:31:02 -05:00

115 lines
5.9 KiB
C++

#pragma once
#include <cstdint>
#include <string>
namespace ApiKeys { struct Scope; }
/**
* 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)
eManageDenial ManageDenialNow(uint8_t actorLevel, uint32_t actorAccountId, uint8_t targetLevel, uint32_t targetAccountId, eAccountAction action, const ApiKeys::Scope* scope);
}