mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
refactor: 拆分 gateway 单体为独立 crate,新增 systemd 部署方案
将 gateway 内部的 model-fetch、provider-transport、scheduler-core、 usage-runtime、video-tasks-core 模块提取为独立 crate;重构 gateway 内部模块结构(state/router/cache/data/query 等);移除大量遗留模块 文件;新增 systemd 二进制部署骨架及相关文档;更新前端 usage 相关 API 和组件。
This commit is contained in:
@@ -5,7 +5,7 @@ use axum::http::Uri;
|
||||
use sha2::{Digest, Sha256};
|
||||
use url::form_urlencoded;
|
||||
|
||||
use crate::gateway::headers::{header_value_str, is_json_request};
|
||||
use crate::headers::{header_value_str, is_json_request};
|
||||
|
||||
use super::super::GatewayControlDecision;
|
||||
use super::types::{
|
||||
@@ -68,7 +68,7 @@ pub(super) fn extract_request_credentials(
|
||||
}
|
||||
|
||||
fn has_trusted_gateway_marker(headers: &http::HeaderMap) -> bool {
|
||||
header_value_str(headers, crate::gateway::constants::GATEWAY_HEADER)
|
||||
header_value_str(headers, crate::constants::GATEWAY_HEADER)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_ascii_lowercase()
|
||||
@@ -128,23 +128,23 @@ fn extract_trusted_auth_headers(headers: &http::HeaderMap) -> Option<GatewayTrus
|
||||
}
|
||||
let user_id = header_value_str(
|
||||
headers,
|
||||
crate::gateway::constants::TRUSTED_AUTH_USER_ID_HEADER,
|
||||
crate::constants::TRUSTED_AUTH_USER_ID_HEADER,
|
||||
)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
let api_key_id = header_value_str(
|
||||
headers,
|
||||
crate::gateway::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
|
||||
crate::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
|
||||
)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
let balance_remaining = header_value_str(
|
||||
headers,
|
||||
crate::gateway::constants::TRUSTED_AUTH_BALANCE_HEADER,
|
||||
crate::constants::TRUSTED_AUTH_BALANCE_HEADER,
|
||||
)
|
||||
.as_deref()
|
||||
.and_then(parse_f64_header);
|
||||
let access_allowed = header_value_str(
|
||||
headers,
|
||||
crate::gateway::constants::TRUSTED_AUTH_ACCESS_ALLOWED_HEADER,
|
||||
crate::constants::TRUSTED_AUTH_ACCESS_ALLOWED_HEADER,
|
||||
)
|
||||
.as_deref()
|
||||
.and_then(parse_bool_header);
|
||||
@@ -165,7 +165,7 @@ pub(super) fn extract_trusted_admin_headers(
|
||||
}
|
||||
let user_id = header_value_str(
|
||||
headers,
|
||||
crate::gateway::constants::TRUSTED_ADMIN_USER_ID_HEADER,
|
||||
crate::constants::TRUSTED_ADMIN_USER_ID_HEADER,
|
||||
)?
|
||||
.trim()
|
||||
.to_string();
|
||||
@@ -174,7 +174,7 @@ pub(super) fn extract_trusted_admin_headers(
|
||||
}
|
||||
let user_role = header_value_str(
|
||||
headers,
|
||||
crate::gateway::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
|
||||
crate::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
|
||||
)?
|
||||
.trim()
|
||||
.to_string();
|
||||
@@ -183,13 +183,13 @@ pub(super) fn extract_trusted_admin_headers(
|
||||
}
|
||||
let session_id = header_value_str(
|
||||
headers,
|
||||
crate::gateway::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
|
||||
crate::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
|
||||
)
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
let management_token_id = header_value_str(
|
||||
headers,
|
||||
crate::gateway::constants::TRUSTED_ADMIN_MANAGEMENT_TOKEN_ID_HEADER,
|
||||
crate::constants::TRUSTED_ADMIN_MANAGEMENT_TOKEN_ID_HEADER,
|
||||
)
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty());
|
||||
@@ -519,23 +519,23 @@ mod tests {
|
||||
fn extracts_trusted_auth_headers() {
|
||||
let mut headers = http::HeaderMap::new();
|
||||
headers.insert(
|
||||
crate::gateway::constants::GATEWAY_HEADER,
|
||||
crate::constants::GATEWAY_HEADER,
|
||||
"rust-phase3b".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_AUTH_USER_ID_HEADER,
|
||||
crate::constants::TRUSTED_AUTH_USER_ID_HEADER,
|
||||
"user-1".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
|
||||
crate::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
|
||||
"key-1".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_AUTH_BALANCE_HEADER,
|
||||
crate::constants::TRUSTED_AUTH_BALANCE_HEADER,
|
||||
"1.5".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_AUTH_ACCESS_ALLOWED_HEADER,
|
||||
crate::constants::TRUSTED_AUTH_ACCESS_ALLOWED_HEADER,
|
||||
"true".parse().unwrap(),
|
||||
);
|
||||
|
||||
@@ -557,11 +557,11 @@ mod tests {
|
||||
fn ignores_trusted_auth_headers_without_gateway_marker() {
|
||||
let mut headers = http::HeaderMap::new();
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_AUTH_USER_ID_HEADER,
|
||||
crate::constants::TRUSTED_AUTH_USER_ID_HEADER,
|
||||
"user-1".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
|
||||
crate::constants::TRUSTED_AUTH_API_KEY_ID_HEADER,
|
||||
"key-1".parse().unwrap(),
|
||||
);
|
||||
|
||||
@@ -574,19 +574,19 @@ mod tests {
|
||||
fn extracts_trusted_admin_headers() {
|
||||
let mut headers = http::HeaderMap::new();
|
||||
headers.insert(
|
||||
crate::gateway::constants::GATEWAY_HEADER,
|
||||
crate::constants::GATEWAY_HEADER,
|
||||
"rust-phase3b".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_ADMIN_USER_ID_HEADER,
|
||||
crate::constants::TRUSTED_ADMIN_USER_ID_HEADER,
|
||||
"admin-user-1".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
|
||||
crate::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
|
||||
"admin".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
|
||||
crate::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
|
||||
"sess-1".parse().unwrap(),
|
||||
);
|
||||
|
||||
@@ -610,15 +610,15 @@ mod tests {
|
||||
fn ignores_trusted_admin_headers_without_gateway_marker() {
|
||||
let mut headers = http::HeaderMap::new();
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_ADMIN_USER_ID_HEADER,
|
||||
crate::constants::TRUSTED_ADMIN_USER_ID_HEADER,
|
||||
"admin-user-1".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
|
||||
crate::constants::TRUSTED_ADMIN_USER_ROLE_HEADER,
|
||||
"admin".parse().unwrap(),
|
||||
);
|
||||
headers.insert(
|
||||
crate::gateway::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
|
||||
crate::constants::TRUSTED_ADMIN_SESSION_ID_HEADER,
|
||||
"sess-1".parse().unwrap(),
|
||||
);
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ pub(crate) fn should_buffer_request_for_local_auth(
|
||||
};
|
||||
decision.route_class.as_deref() == Some("ai_public")
|
||||
&& decision.route_kind.as_deref() != Some("files")
|
||||
&& crate::gateway::headers::is_json_request(headers)
|
||||
&& crate::headers::is_json_request(headers)
|
||||
}
|
||||
|
||||
pub(crate) fn request_model_local_rejection(
|
||||
|
||||
@@ -35,7 +35,7 @@ pub(super) fn derive_principal_candidate(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{derive_principal_candidate, GatewayPrincipalCandidate};
|
||||
use crate::gateway::control::auth::types::{
|
||||
use crate::control::auth::types::{
|
||||
GatewayCredentialBundle, GatewayCredentialCarrier, GatewayExtractedCredentials,
|
||||
GatewayPrimaryCredential, GatewayTrustedAuthHeaders,
|
||||
};
|
||||
|
||||
@@ -5,8 +5,12 @@ use base64::Engine as _;
|
||||
use hmac::Mac;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::gateway::{AppState, GatewayError};
|
||||
use crate::wallet_runtime::{
|
||||
local_rejection_from_wallet_access, resolve_wallet_auth_gate,
|
||||
};
|
||||
use crate::{AppState, GatewayError};
|
||||
|
||||
use super::super::GatewayControlDecision;
|
||||
use super::credentials::{
|
||||
@@ -16,8 +20,7 @@ use super::credentials::{
|
||||
use super::gate::GatewayLocalAuthRejection;
|
||||
use super::principal::derive_principal_candidate;
|
||||
use super::types::{GatewayPrincipalCandidate, GatewayTrustedAuthHeaders};
|
||||
use crate::gateway::headers::header_value_str;
|
||||
use crate::gateway::{local_rejection_from_wallet_access, resolve_wallet_auth_gate};
|
||||
use crate::headers::header_value_str;
|
||||
|
||||
const AUTH_CONTEXT_CACHE_TTL: Duration = Duration::from_secs(60);
|
||||
const AUTH_CONTEXT_CACHE_MAX_ENTRIES: usize = 256;
|
||||
@@ -56,11 +59,13 @@ pub(in super::super) async fn resolve_control_decision_auth(
|
||||
state: &AppState,
|
||||
headers: &http::HeaderMap,
|
||||
uri: &Uri,
|
||||
trace_id: &str,
|
||||
mut decision: GatewayControlDecision,
|
||||
) -> Result<ControlDecisionAuthResolution, GatewayError> {
|
||||
if let Some(admin_principal) =
|
||||
resolve_trusted_admin_principal(headers, decision.auth_endpoint_signature.as_deref())
|
||||
{
|
||||
log_admin_principal_resolution(trace_id, &decision, "trusted_headers", &admin_principal);
|
||||
decision.admin_principal = Some(admin_principal);
|
||||
} else if let Some(admin_principal) = resolve_local_admin_principal(
|
||||
state,
|
||||
@@ -70,6 +75,7 @@ pub(in super::super) async fn resolve_control_decision_auth(
|
||||
)
|
||||
.await?
|
||||
{
|
||||
log_admin_principal_resolution(trace_id, &decision, "local_session", &admin_principal);
|
||||
decision.admin_principal = Some(admin_principal);
|
||||
}
|
||||
|
||||
@@ -81,6 +87,7 @@ pub(in super::super) async fn resolve_control_decision_auth(
|
||||
)
|
||||
.await?
|
||||
{
|
||||
log_auth_context_resolution(trace_id, &decision, &auth_context);
|
||||
decision.local_auth_rejection = auth_context.local_rejection.clone();
|
||||
if !auth_context.user_id.is_empty() && !auth_context.api_key_id.is_empty() {
|
||||
if let Some(cache_key) = decision
|
||||
@@ -95,6 +102,7 @@ pub(in super::super) async fn resolve_control_decision_auth(
|
||||
}
|
||||
|
||||
if decision.local_auth_rejection.is_some() {
|
||||
log_local_auth_rejection(trace_id, &decision);
|
||||
return Ok(ControlDecisionAuthResolution::Resolved(decision));
|
||||
}
|
||||
|
||||
@@ -113,6 +121,93 @@ pub(in super::super) async fn resolve_control_decision_auth(
|
||||
Ok(ControlDecisionAuthResolution::Resolved(decision))
|
||||
}
|
||||
|
||||
fn log_admin_principal_resolution(
|
||||
trace_id: &str,
|
||||
decision: &GatewayControlDecision,
|
||||
resolution: &'static str,
|
||||
admin_principal: &GatewayAdminPrincipalContext,
|
||||
) {
|
||||
debug!(
|
||||
event_name = "admin_principal_resolved",
|
||||
log_type = "debug",
|
||||
debug_context = "control_auth",
|
||||
trace_id = %trace_id,
|
||||
route_class = decision.route_class.as_deref().unwrap_or("unknown"),
|
||||
route_family = decision.route_family.as_deref().unwrap_or("unknown"),
|
||||
route_kind = decision.route_kind.as_deref().unwrap_or("unknown"),
|
||||
resolution,
|
||||
admin_user_id = admin_principal.user_id.as_str(),
|
||||
admin_user_role = admin_principal.user_role.as_str(),
|
||||
admin_session_id = admin_principal.session_id.as_deref().unwrap_or("-"),
|
||||
admin_management_token_id = admin_principal.management_token_id.as_deref().unwrap_or("-"),
|
||||
"resolved admin principal for control decision"
|
||||
);
|
||||
}
|
||||
|
||||
fn log_auth_context_resolution(
|
||||
trace_id: &str,
|
||||
decision: &GatewayControlDecision,
|
||||
auth_context: &GatewayControlAuthContext,
|
||||
) {
|
||||
info!(
|
||||
event_name = "auth_context_resolved",
|
||||
log_type = "event",
|
||||
status = if auth_context.access_allowed {
|
||||
"allowed"
|
||||
} else {
|
||||
"blocked"
|
||||
},
|
||||
trace_id = %trace_id,
|
||||
route_class = decision.route_class.as_deref().unwrap_or("unknown"),
|
||||
route_family = decision.route_family.as_deref().unwrap_or("unknown"),
|
||||
route_kind = decision.route_kind.as_deref().unwrap_or("unknown"),
|
||||
user_id = auth_context.user_id.as_str(),
|
||||
api_key_id = auth_context.api_key_id.as_str(),
|
||||
access_allowed = auth_context.access_allowed,
|
||||
api_key_is_standalone = auth_context.api_key_is_standalone,
|
||||
has_local_rejection = auth_context.local_rejection.is_some(),
|
||||
"resolved data-backed auth context for control decision"
|
||||
);
|
||||
}
|
||||
|
||||
fn log_local_auth_rejection(trace_id: &str, decision: &GatewayControlDecision) {
|
||||
let Some(rejection) = decision.local_auth_rejection.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let (rejection_kind, rejection_detail) = match rejection {
|
||||
GatewayLocalAuthRejection::InvalidApiKey => ("invalid_api_key", "-".to_string()),
|
||||
GatewayLocalAuthRejection::LockedApiKey => ("locked_api_key", "-".to_string()),
|
||||
GatewayLocalAuthRejection::WalletUnavailable => ("wallet_unavailable", "-".to_string()),
|
||||
GatewayLocalAuthRejection::BalanceDenied { remaining } => (
|
||||
"balance_denied",
|
||||
remaining
|
||||
.map(|value| format!("remaining_usd={value:.4}"))
|
||||
.unwrap_or_else(|| "remaining_usd=unknown".to_string()),
|
||||
),
|
||||
GatewayLocalAuthRejection::ProviderNotAllowed { provider } => {
|
||||
("provider_not_allowed", provider.clone())
|
||||
}
|
||||
GatewayLocalAuthRejection::ApiFormatNotAllowed { api_format } => {
|
||||
("api_format_not_allowed", api_format.clone())
|
||||
}
|
||||
GatewayLocalAuthRejection::ModelNotAllowed { model } => {
|
||||
("model_not_allowed", model.clone())
|
||||
}
|
||||
};
|
||||
info!(
|
||||
event_name = "local_auth_rejected",
|
||||
log_type = "event",
|
||||
status = "rejected",
|
||||
trace_id = %trace_id,
|
||||
route_class = decision.route_class.as_deref().unwrap_or("unknown"),
|
||||
route_family = decision.route_family.as_deref().unwrap_or("unknown"),
|
||||
route_kind = decision.route_kind.as_deref().unwrap_or("unknown"),
|
||||
rejection_kind,
|
||||
rejection_detail = %rejection_detail,
|
||||
"rejected local control request during auth gate resolution"
|
||||
);
|
||||
}
|
||||
|
||||
fn allows_missing_data_backed_auth_context(decision: &GatewayControlDecision) -> bool {
|
||||
matches!(
|
||||
decision.route_kind.as_deref(),
|
||||
@@ -463,7 +558,7 @@ async fn resolve_trusted_auth_context(
|
||||
}
|
||||
|
||||
fn build_data_backed_auth_context(
|
||||
snapshot: crate::gateway::gateway_data::StoredGatewayAuthApiKeySnapshot,
|
||||
snapshot: crate::data::auth::GatewayAuthApiKeySnapshot,
|
||||
auth_endpoint_signature: &str,
|
||||
header_access_allowed: Option<bool>,
|
||||
balance_remaining: Option<f64>,
|
||||
@@ -551,8 +646,9 @@ mod tests {
|
||||
use axum::http::{HeaderMap, Uri};
|
||||
|
||||
use super::resolve_data_backed_auth_context;
|
||||
use crate::gateway::control::auth::credentials::hash_api_key;
|
||||
use crate::gateway::{AppState, GatewayDataState};
|
||||
use crate::control::auth::credentials::hash_api_key;
|
||||
use crate::data::GatewayDataState;
|
||||
use crate::AppState;
|
||||
|
||||
fn sample_snapshot(api_key_id: &str, user_id: &str) -> StoredAuthApiKeySnapshot {
|
||||
StoredAuthApiKeySnapshot::new(
|
||||
|
||||
Reference in New Issue
Block a user