mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-05 12:23:44 +00:00
feat(dashboard): scoped API keys with rate limits and quotas
Bearer keys (dlk_...) are checked per request against their owner's current account (ban, lock, demotion, sign out everywhere stop or narrow them at once) and their scope: permission routes need the permission in scope, read-only keys only read, level-only routes need an all-permission key, and session-only paths (sign-in, password, 2FA, key management) are never reachable with a key. Per-key rate limit and daily quota with 429 and X-RateLimit/X-Quota/Retry-After headers; usage is written in batches every minute. WebSocket subscriptions honour the scope too. Routes to list, make, rotate and revoke keys; staff with api_keys_manage can see and revoke others' keys under the rank rules. POST /api/auth/token now makes an all-permission key. Audit entries for create/rotate/revoke/denied, and actions done with a key are attributed to "user (key name)". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,15 @@ namespace {
|
||||
|
||||
std::function<bool(uint8_t)> g_ApiAccessAllowed;
|
||||
std::function<void(const HTTPContext&, HTTPReply&)> g_ForbiddenPage;
|
||||
std::function<void(const HTTPContext&, const std::string&)> g_ApiKeyDenied;
|
||||
|
||||
bool RefuseApiKey(const HTTPContext& context, HTTPReply& reply, const std::string& reason, const std::string& message) {
|
||||
if (g_ApiKeyDenied) g_ApiKeyDenied(context, reason);
|
||||
reply.status = eHTTPStatusCode::FORBIDDEN;
|
||||
reply.message = nlohmann::json{ {"success", false}, {"error", message}, {"code", "api_key_scope"} }.dump();
|
||||
reply.contentType = eContentType::APPLICATION_JSON;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsSafeMethod(const HTTPContext& context) {
|
||||
return context.method == "GET" || context.method == "HEAD" || context.method == "OPTIONS";
|
||||
@@ -37,6 +46,13 @@ RequireAuthMiddleware::RequireAuthMiddleware(uint8_t minGmLevel) : requiredLevel
|
||||
|
||||
RequireAuthMiddleware::RequireAuthMiddleware(std::function<uint8_t()> requiredLevel) : requiredLevel(std::move(requiredLevel)) {}
|
||||
|
||||
RequireAuthMiddleware::RequireAuthMiddleware(std::function<uint8_t()> requiredLevel, std::string permission)
|
||||
: requiredLevel(std::move(requiredLevel)), permission(std::move(permission)) {}
|
||||
|
||||
void RequireAuthMiddleware::SetApiKeyDeniedHook(std::function<void(const HTTPContext& context, const std::string& reason)> hook) {
|
||||
g_ApiKeyDenied = std::move(hook);
|
||||
}
|
||||
|
||||
bool RequireAuthMiddleware::Process(HTTPContext& context, HTTPReply& reply) {
|
||||
if (!context.isAuthenticated) {
|
||||
LOG_DEBUG("Unauthorized access attempt to %s from %s", context.path.c_str(), context.clientIP.c_str());
|
||||
@@ -105,5 +121,21 @@ bool RequireAuthMiddleware::Process(HTTPContext& context, HTTPReply& reply) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// An API key can only narrow what its owner may do: read-only keys make no changes, and a route guarded by a
|
||||
// permission needs that permission in the key's scope. Routes guarded only by a GM level above 0 have no
|
||||
// permission to scope them by, so only keys with all of the owner's permissions reach them.
|
||||
if (context.apiKey) {
|
||||
const auto& key = *context.apiKey;
|
||||
if (key.readOnly && !readsOnly && !IsSafeMethod(context)) {
|
||||
return RefuseApiKey(context, reply, context.method + " " + context.path + " with a read-only key", "This API key is read-only");
|
||||
}
|
||||
if (!permission.empty() && !key.Has(permission)) {
|
||||
return RefuseApiKey(context, reply, "no " + permission + " permission for " + context.path, "This API key doesn't have the " + permission + " permission");
|
||||
}
|
||||
if (permission.empty() && minGmLevel > 0 && !key.allPermissions) {
|
||||
return RefuseApiKey(context, reply, context.path + " needs a key with all permissions", "This needs an API key with all of your permissions");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user