mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43: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>
119 lines
4.5 KiB
C++
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;
|
|
}
|