Files
DarkflameServer/dDashboardServer/auth/AuthTokenHandler.cpp
Aaron Kimbrell 821b7c8767 feat(dashboard): permission grants count in every dashboard permission check
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>
2026-09-29 01:16:38 -05:00

119 lines
4.5 KiB
C++

#include "AuthTokenHandler.h"
#include "PermissionGrantsLoader.h"
#include "ApiKeyService.h"
#include "DashboardAuthService.h"
#include "Game.h"
#include "Logger.h"
#include "HTTPContext.h"
#include "Web.h"
std::string AuthTokenHandler::ExtractCookie(const std::string& cookieHeader, const std::string& name) {
// Match whole cookie names only so "xdashboardToken" does not satisfy "dashboardToken"
size_t pos = 0;
while (pos < cookieHeader.size()) {
while (pos < cookieHeader.size() && (cookieHeader[pos] == ' ' || cookieHeader[pos] == ';')) pos++;
const size_t end = std::min(cookieHeader.find(';', pos), cookieHeader.size());
const std::string_view pair(cookieHeader.data() + pos, end - pos);
const size_t eq = pair.find('=');
if (eq != std::string_view::npos && pair.substr(0, eq) == name) {
const auto value = pair.substr(eq + 1);
std::string decoded;
decoded.reserve(value.size());
for (size_t i = 0; i < value.size(); ++i) {
if (value[i] == '%' && i + 2 < value.size()) {
char* endptr = nullptr;
const std::string hex(value.substr(i + 1, 2));
const auto code = std::strtol(hex.c_str(), &endptr, 16);
if (endptr == hex.c_str() + 2) {
decoded += static_cast<char>(code);
i += 2;
continue;
}
}
decoded += value[i];
}
return decoded;
}
pos = end + 1;
}
return "";
}
std::string AuthTokenHandler::ExtractTokenFromAuthHeader(const std::string& authHeader) {
if (authHeader.starts_with("Bearer ")) return authHeader.substr(7);
if (authHeader.starts_with("Token ")) return authHeader.substr(6);
return "";
}
std::string AuthTokenHandler::ExtractToken(const std::string& cookieHeader, const std::string& authHeader, eTokenSource& source) {
auto token = ExtractTokenFromAuthHeader(authHeader);
if (!token.empty()) {
source = eTokenSource::HEADER;
return token;
}
token = ExtractCookie(cookieHeader, COOKIE_NAME);
source = token.empty() ? eTokenSource::NONE : eTokenSource::COOKIE;
return token;
}
AuthTokenHandler::TokenValidationResult AuthTokenHandler::ValidateToken(const std::string& token) {
TokenValidationResult result;
if (token.empty()) {
result.errorMessage = "No token provided";
return result;
}
if (!DashboardAuthService::VerifyToken(token, result.username, result.gmLevel, result.accountId)) {
result.errorMessage = "Invalid or expired token";
return result;
}
result.isValid = true;
return result;
}
bool AuthTokenHandler::ProcessHTTPContext(HTTPContext& context, HTTPReply& reply) {
eTokenSource source = eTokenSource::NONE;
const auto token = ExtractToken(context.GetHeader("Cookie"), context.GetHeader("Authorization"), source);
if (token.empty()) return true;
// API keys: their scope and limits are checked here; a key over its limits is refused outright
if (source == eTokenSource::HEADER && ApiKeyService::LooksLikeKey(token)) {
const auto keyResult = ApiKeyService::Authenticate(token, context, reply);
if (keyResult == ApiKeyService::eResult::INVALID) LOG_DEBUG("API key validation failed from %s", context.clientIP.c_str());
if (context.isAuthenticated) context.grants = PermissionGrants::Load(context.accountId);
return keyResult != ApiKeyService::eResult::REFUSED;
}
const auto result = ValidateToken(token);
if (!result.isValid) {
LOG_DEBUG("Authentication token validation failed: %s", result.errorMessage.c_str());
return true; // Let routes decide if auth is required
}
context.isAuthenticated = true;
context.authenticatedUser = result.username;
context.accountId = result.accountId;
context.gmLevel = result.gmLevel;
// Read on every request like the GM level, so a grant given or taken away applies at once
context.grants = PermissionGrants::Load(result.accountId);
context.userData["auth_source"] = source == eTokenSource::COOKIE ? "cookie" : "header";
if (DashboardAuthService::NeedsTwoFactorSetup(result.accountId, result.gmLevel)) context.userData["needs_2fa"] = "1";
return true;
}
std::string AuthTokenHandler::BuildSessionCookie(const std::string& token, bool rememberMe, bool secure) {
std::string cookie = std::string("Set-Cookie: ") + COOKIE_NAME + "=" + token + "; Path=/; HttpOnly; SameSite=Strict";
if (rememberMe) cookie += "; Max-Age=" + std::to_string(30 * 24 * 60 * 60);
if (secure) cookie += "; Secure";
return cookie;
}
std::string AuthTokenHandler::BuildClearSessionCookie(bool secure) {
std::string cookie = std::string("Set-Cookie: ") + COOKIE_NAME + "=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0";
if (secure) cookie += "; Secure";
return cookie;
}