mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 10:53:44 +00:00
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>
152 lines
5.9 KiB
C++
152 lines
5.9 KiB
C++
#include "PermissionGrants.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "GeneralUtils.h"
|
|
#include "Permissions.h"
|
|
|
|
namespace {
|
|
using PermissionGrants::eKind;
|
|
using PermissionGrants::eMatch;
|
|
|
|
constexpr uint8_t STAFF_FLOOR = 1; // commands with a higher floor are never granted (SlashCommandLevels::STAFF_MIN_LEVEL)
|
|
|
|
// A deny anywhere beats every allow
|
|
eMatch Combine(eMatch a, eMatch b) {
|
|
if (a == eMatch::DENY || b == eMatch::DENY) return eMatch::DENY;
|
|
if (a == eMatch::ALLOW || b == eMatch::ALLOW) return eMatch::ALLOW;
|
|
return eMatch::NONE;
|
|
}
|
|
|
|
eMatch Of(const PermissionGrants::Rule& rule) {
|
|
return rule.deny ? eMatch::DENY : eMatch::ALLOW;
|
|
}
|
|
}
|
|
|
|
namespace PermissionGrants {
|
|
std::optional<eKind> ParseKind(std::string_view name) {
|
|
if (name == "permission") return eKind::PERMISSION;
|
|
if (name == "command") return eKind::COMMAND;
|
|
if (name == "permission_group") return eKind::PERMISSION_GROUP;
|
|
if (name == "command_group") return eKind::COMMAND_GROUP;
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::string_view KindName(eKind kind) {
|
|
switch (kind) {
|
|
case eKind::PERMISSION: return "permission";
|
|
case eKind::COMMAND: return "command";
|
|
case eKind::PERMISSION_GROUP: return "permission_group";
|
|
default: return "command_group";
|
|
}
|
|
}
|
|
|
|
void Held::Add(std::string_view kind, std::string name, bool deny, int64_t expiresAt) {
|
|
const auto parsed = ParseKind(kind);
|
|
if (parsed) rules.push_back({ *parsed, std::move(name), deny, expiresAt });
|
|
}
|
|
|
|
eMatch ForPermission(const Held& held, std::string_view key, int64_t now) {
|
|
const auto* permission = Permissions::Find(std::string(key));
|
|
if (!permission || permission->locked) return eMatch::NONE;
|
|
eMatch match = eMatch::NONE;
|
|
for (const auto& rule : held.rules) {
|
|
if (!InForce(rule, now)) continue;
|
|
if ((rule.kind == eKind::PERMISSION && rule.name == key) || (rule.kind == eKind::PERMISSION_GROUP && rule.name == permission->category)) {
|
|
match = Combine(match, Of(rule));
|
|
}
|
|
}
|
|
return match;
|
|
}
|
|
|
|
eMatch ForCommand(const Held& held, const Command& command, int64_t now) {
|
|
eMatch match = command.permission.empty() ? eMatch::NONE : ForPermission(held, command.permission, now);
|
|
for (const auto& rule : held.rules) {
|
|
if (!InForce(rule, now)) continue;
|
|
if (rule.kind == eKind::COMMAND && rule.name == command.name) match = Combine(match, Of(rule));
|
|
else if (rule.kind == eKind::COMMAND_GROUP) {
|
|
const auto level = CommandGroupLevel(rule.name);
|
|
if (level && command.level <= *level) match = Combine(match, Of(rule));
|
|
}
|
|
}
|
|
return match;
|
|
}
|
|
|
|
bool MayUseCommand(uint8_t playerLevel, uint8_t accountLevel, const Command& command, const Held* held, int64_t now) {
|
|
const bool byLevel = playerLevel >= command.level;
|
|
if (!held || command.fixed) return byLevel;
|
|
auto match = ForCommand(*held, command, now);
|
|
// A floor above GM 1 is a safety limit of the code (e.g. /execute): grants don't take anyone below it
|
|
if (match == eMatch::ALLOW && command.minLevel > STAFF_FLOOR && playerLevel < command.minLevel) match = eMatch::NONE;
|
|
return Decide(byLevel, match, accountLevel);
|
|
}
|
|
|
|
std::vector<std::string> PermissionGroups() {
|
|
std::vector<std::string> groups;
|
|
for (const auto& permission : Permissions::All()) {
|
|
if (std::ranges::find(groups, permission.category) == groups.end()) groups.push_back(permission.category);
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
std::vector<std::string> GroupPermissions(std::string_view category) {
|
|
std::vector<std::string> keys;
|
|
for (const auto& permission : Permissions::All()) {
|
|
if (permission.category == category && !permission.locked) keys.push_back(permission.key);
|
|
}
|
|
return keys;
|
|
}
|
|
|
|
std::optional<uint8_t> CommandGroupLevel(std::string_view name) {
|
|
if (name.size() != 1 || name[0] < '1' || name[0] > '9') return std::nullopt;
|
|
return static_cast<uint8_t>(name[0] - '0');
|
|
}
|
|
|
|
std::string Refusal(eKind kind, const std::string& name, uint8_t grantorLevel,
|
|
const std::function<bool(const std::string&)>& holdsPermission,
|
|
const std::function<std::optional<Command>(const std::string&)>& findCommand,
|
|
const std::function<bool(const Command&)>& holdsCommand) {
|
|
switch (kind) {
|
|
case eKind::PERMISSION: {
|
|
const auto* permission = Permissions::Find(name);
|
|
if (!permission) return "Unknown permission " + name;
|
|
if (permission->locked) return permission->key + " is always GM 9 only and can't be granted";
|
|
if (!holdsPermission(name)) return "You can't grant " + name + ": you don't have it yourself";
|
|
return "";
|
|
}
|
|
case eKind::PERMISSION_GROUP: {
|
|
const auto keys = GroupPermissions(name);
|
|
if (keys.empty()) return "Unknown permission group " + name;
|
|
for (const auto& key : keys) {
|
|
if (!holdsPermission(key)) return "You can't grant every " + name + " permission: you don't have " + key + " yourself";
|
|
}
|
|
return "";
|
|
}
|
|
case eKind::COMMAND: {
|
|
const auto command = findCommand(name);
|
|
if (!command) return "Unknown command " + name + " (the world servers list their commands when they start)";
|
|
if (command->fixed) return "/" + name + " has a fixed level; grants don't apply to it";
|
|
if (command->minLevel > STAFF_FLOOR) return "/" + name + " never goes below GM " + std::to_string(command->minLevel) + "; grants can't change that";
|
|
if (!holdsCommand(*command)) return "You can't grant /" + name + ": you may not use it yourself";
|
|
return "";
|
|
}
|
|
case eKind::COMMAND_GROUP: {
|
|
const auto level = CommandGroupLevel(name);
|
|
if (!level) return "A command group is a GM level from 1 to 9";
|
|
if (grantorLevel < *level) return "You can't grant every command up to GM " + name + ": your GM level is " + std::to_string(grantorLevel);
|
|
return "";
|
|
}
|
|
}
|
|
return "Unknown kind of grant";
|
|
}
|
|
|
|
std::string Describe(eKind kind, const std::string& name) {
|
|
switch (kind) {
|
|
case eKind::PERMISSION: return "the " + name + " permission";
|
|
case eKind::COMMAND: return "/" + name;
|
|
case eKind::PERMISSION_GROUP: return "every " + name + " permission";
|
|
default: return "every command up to GM " + name;
|
|
}
|
|
}
|
|
}
|