mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-30 16:37:14 +00:00
fix(dashboard): DataTables queries count as reads for read-only API keys
POST /api/tables/... only reads, so read-only keys may use it (and the API docs list it for them). Keys limited to some addresses can't open the WebSocket, whose address isn't checked, nor keys whose allowed paths leave out /ws. Refusals are audited without an account target. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -109,14 +109,13 @@ namespace {
|
||||
if (!context.apiKey) return true;
|
||||
const auto& key = *context.apiKey;
|
||||
if (ApiKeyService::SessionOnlyPath(doc.path)) return false;
|
||||
if (key.readOnly && doc.method != "GET") return false;
|
||||
if (key.readOnly && !doc.reads) return false;
|
||||
return doc.permission.empty() ? (doc.minGmLevel <= 0 || key.allPermissions) : key.Has(doc.permission);
|
||||
}
|
||||
|
||||
// Register a DataTables endpoint. The fetcher returns the DB layer's JSON string. Access: a GM level or a Perm.
|
||||
template<typename Access>
|
||||
void TableRoute(const std::string& path, const Access& access, const std::string& description, TableFetcher fetcher) {
|
||||
ReadRoutes reads; // the query is a POST body, but it only reads
|
||||
Route(eHTTPMethod::POST, path, access, description, [fetcher = std::move(fetcher)](HTTPReply& reply, const HTTPContext& context) {
|
||||
const auto request = ParseDataTablesRequest(context.body);
|
||||
const auto body = ParseBody(context);
|
||||
|
||||
@@ -279,7 +279,7 @@ namespace ApiKeyRoutes {
|
||||
|
||||
// Refusals of keys (their scope, read-only, addresses, paths, limits) go to the audit log, a minute apart at most
|
||||
ApiKeyService::SetDeniedHook([](const HTTPContext& context, const std::string& reason) {
|
||||
Audit(context, "api_key_denied", reason, AuditTarget::Account(context.accountId));
|
||||
Audit(context, "api_key_denied", reason);
|
||||
});
|
||||
RequireAuthMiddleware::SetApiKeyDeniedHook([](const HTTPContext& context, const std::string& reason) { ApiKeyService::NoteDenied(context, reason); });
|
||||
ApiKeyService::SetClientAddress([](const HTTPContext& context) { return ClientAddress(context); });
|
||||
|
||||
@@ -35,8 +35,12 @@ namespace RouteUtils {
|
||||
std::vector<RouteDoc> g_RouteDocs;
|
||||
int g_ReadRoutes = 0;
|
||||
|
||||
std::shared_ptr<RequireAuthMiddleware> MakeRequireAuth(std::shared_ptr<RequireAuthMiddleware> middleware) {
|
||||
if (g_ReadRoutes > 0) middleware->SetReadsOnly();
|
||||
bool Reads(eHTTPMethod method, const std::string& path) {
|
||||
return method == eHTTPMethod::GET || g_ReadRoutes > 0 || (method == eHTTPMethod::POST && path.starts_with("/api/tables/"));
|
||||
}
|
||||
|
||||
std::shared_ptr<RequireAuthMiddleware> MakeRequireAuth(std::shared_ptr<RequireAuthMiddleware> middleware, eHTTPMethod method, const std::string& path) {
|
||||
if (Reads(method, path)) middleware->SetReadsOnly();
|
||||
return middleware;
|
||||
}
|
||||
}
|
||||
@@ -63,16 +67,16 @@ namespace RouteUtils {
|
||||
|
||||
void Route(eHTTPMethod method, const std::string& path, int16_t minGmLevel, const std::string& description, Handler handler) {
|
||||
std::vector<MiddlewarePtr> middleware;
|
||||
if (minGmLevel >= 0) middleware.push_back(MakeRequireAuth(std::make_shared<RequireAuthMiddleware>(static_cast<uint8_t>(minGmLevel))));
|
||||
g_RouteDocs.push_back({ std::string(magic_enum::enum_name(method)), path, minGmLevel, description, "" });
|
||||
if (minGmLevel >= 0) middleware.push_back(MakeRequireAuth(std::make_shared<RequireAuthMiddleware>(static_cast<uint8_t>(minGmLevel)), method, path));
|
||||
g_RouteDocs.push_back({ std::string(magic_enum::enum_name(method)), path, minGmLevel, description, "", Reads(method, path) });
|
||||
Register(method, path, std::move(middleware), std::move(handler));
|
||||
}
|
||||
|
||||
void Route(eHTTPMethod method, const std::string& path, const Perm& permission, const std::string& description, Handler handler) {
|
||||
if (!Permissions::Find(permission.key)) LOG("Route %s uses unknown permission %s; nobody can use it", path.c_str(), permission.key.c_str());
|
||||
std::vector<MiddlewarePtr> middleware;
|
||||
middleware.push_back(MakeRequireAuth(std::make_shared<RequireAuthMiddleware>(std::function<uint8_t()>([key = permission.key] { return Permissions::Level(key); }), permission.key)));
|
||||
g_RouteDocs.push_back({ std::string(magic_enum::enum_name(method)), path, Permissions::Level(permission.key), description, permission.key });
|
||||
middleware.push_back(MakeRequireAuth(std::make_shared<RequireAuthMiddleware>(std::function<uint8_t()>([key = permission.key] { return Permissions::Level(key); }), permission.key), method, path));
|
||||
g_RouteDocs.push_back({ std::string(magic_enum::enum_name(method)), path, Permissions::Level(permission.key), description, permission.key, Reads(method, path) });
|
||||
Register(method, path, std::move(middleware), std::move(handler));
|
||||
}
|
||||
|
||||
|
||||
@@ -243,6 +243,7 @@ namespace RouteUtils {
|
||||
int16_t minGmLevel;
|
||||
std::string description;
|
||||
std::string permission; // set for routes guarded by a permission; minGmLevel is then its level at the time
|
||||
bool reads{}; // makes no changes (a GET, or a POST that only reads): read-only API keys may use it
|
||||
};
|
||||
|
||||
// A named permission (Permissions.h) guarding a route; its GM level can be changed while the server runs
|
||||
@@ -259,8 +260,8 @@ namespace RouteUtils {
|
||||
void Route(eHTTPMethod method, const std::string& path, int16_t minGmLevel, const std::string& description, Handler handler);
|
||||
void Route(eHTTPMethod method, const std::string& path, const Perm& permission, const std::string& description, Handler handler);
|
||||
|
||||
// Routes registered while one of these lives only read, even POSTs (DataTables and lookups send their query as a
|
||||
// body), so read-only API keys may use them
|
||||
// Routes registered while one of these lives only read, even POSTs (lookups that send their query as a body), so
|
||||
// read-only API keys may use them. POST /api/tables/... (DataTables) always counts as a read.
|
||||
struct ReadRoutes {
|
||||
ReadRoutes();
|
||||
~ReadRoutes();
|
||||
|
||||
Reference in New Issue
Block a user