Files
DarkflameServer/dDashboardServer/auth/ApiKeyService.h
Aaron Kimbrell fc4e4eda9f 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>
2026-09-27 09:33:12 -05:00

78 lines
2.9 KiB
C++

#pragma once
#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include "ApiKeyScope.h"
#include "IApiKeys.h"
struct HTTPContext;
struct HTTPReply;
/**
* Dashboard API keys: checking them on requests, their rate limits and quotas, and their usage counters.
* A key's effective permissions are its scope AND its owner's current permissions: the owner is looked up on every
* request, so a ban, lock, demotion or "sign out everywhere" narrows or stops the key at once.
* Runs on the web thread only.
*/
namespace ApiKeyService {
constexpr uint32_t DEFAULT_RATE_LIMIT = 120; // requests a minute, unless api_key_rate_limit says otherwise
constexpr uint32_t MAX_RATE_LIMIT = 6000;
constexpr uint32_t MAX_DAILY_QUOTA = 10'000'000;
struct NewSecret {
std::string token; // shown to the person once, never stored
std::string hash; // SHA-256 hex, stored
std::string prefix; // the start of the key, stored to tell keys apart
};
NewSecret GenerateSecret();
bool LooksLikeKey(std::string_view token);
// Paths a key may never use, whatever its scope: signing in, the account's own password, email, two-factor and
// sessions, and managing API keys (a leaked key must not be able to make itself new keys or lock its owner out)
bool SessionOnlyPath(std::string_view path);
enum class eResult : uint8_t {
INVALID, // unknown, revoked or expired key, or an owner who can't sign in: the request is unauthenticated
OK, // context filled in
REFUSED, // reply written (403 for the key's IP/path restrictions, 429 for its limits)
};
eResult Authenticate(const std::string& token, HTTPContext& context, HTTPReply& reply);
// For WebSocket connections: the owner and scope, without counting against the limits
struct Verified {
uint32_t accountId{};
std::string username;
uint8_t gmLevel{};
bool needsTwoFactorSetup{};
std::shared_ptr<const ApiKeys::Scope> scope;
};
std::optional<Verified> Verify(const std::string& token);
// Stop using what is cached about a key (after it is revoked or rotated)
void Forget(uint64_t keyId);
void ForgetAll();
// Requests since the last write, not yet in the database (for the key list)
std::optional<IApiKeys::ApiKeyUsage> PendingUsage(uint64_t keyId);
// Write the usage counters now and then (call every tick) and before shutting down
void Update();
void Flush();
// The address a request came from, for the key's allowed addresses (the dashboard's behind_proxy rule)
void SetClientAddress(std::function<std::string(const HTTPContext&)> resolve);
// Record that a key was refused something; at most once a minute per key and reason, so a busy script can't flood
// the audit log
void SetDeniedHook(std::function<void(const HTTPContext&, const std::string& reason)> hook);
void NoteDenied(const HTTPContext& context, const std::string& reason);
uint32_t DefaultRateLimit();
}