feat(security): harden gateway boundaries and usage policies

Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change.

Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
elky
2026-09-04 03:45:52 +08:00
parent ddcbeb3ae9
commit 579f2c7cc1
1019 changed files with 190437 additions and 26080 deletions
@@ -2,10 +2,12 @@ use super::{
AdminBillingCollectorRecord, AdminBillingCollectorWriteInput, AdminBillingMutationOutcome,
AdminBillingPresetApplyResult, AdminBillingRuleRecord, AdminBillingRuleWriteInput, AppState,
BillingPlanRecord, BillingPlanWriteInput, GatewayError, LocalMutationOutcome,
PaymentGatewayConfigRecord, PaymentGatewayConfigWriteInput, UserDailyQuotaAvailabilityRecord,
UserPlanEntitlementRecord,
PaymentGatewayConfigCasWriteInput, PaymentGatewayConfigRecord, PaymentGatewayConfigWriteInput,
PaymentGatewaySecretCasUpdate, UserDailyQuotaAvailabilityRecord, UserPlanEntitlementRecord,
};
const PAYMENT_GATEWAY_SECRET_MIGRATION_MAX_ATTEMPTS: usize = 8;
fn data_error(err: impl ToString) -> GatewayError {
GatewayError::Internal(err.to_string())
}
@@ -446,9 +448,73 @@ impl AppState {
&self,
provider: &str,
) -> Result<Option<PaymentGatewayConfigRecord>, GatewayError> {
self.data
.find_payment_gateway_config(provider)
let provider = provider.trim().to_ascii_lowercase();
let mut record = self
.data
.find_payment_gateway_config(&provider)
.await
.map_err(data_error)?;
for _ in 0..PAYMENT_GATEWAY_SECRET_MIGRATION_MAX_ATTEMPTS {
let Some(mut current) = record else {
return Ok(None);
};
let Some(observed) = current.merchant_key_encrypted.as_deref() else {
return Ok(Some(current));
};
let projection = crate::handlers::shared::open_payment_gateway_secret(
self,
&crate::handlers::shared::PaymentGatewaySecretBinding::from_record(&current)
.map_err(|detail| {
GatewayError::Internal(format!(
"payment gateway secret binding is invalid for {}: {detail}",
current.provider
))
})?,
observed,
)
.map_err(|detail| {
GatewayError::Internal(format!(
"payment gateway secret integrity check failed for {}: {detail}",
current.provider
))
})?;
if !projection.migration_required {
return Ok(Some(current));
}
let update = PaymentGatewaySecretCasUpdate {
provider: current.provider.clone(),
expected_merchant_key_encrypted: observed.to_string(),
merchant_key_encrypted: projection.protected.clone(),
};
if self
.data
.compare_and_swap_payment_gateway_secret(&update)
.await
.map_err(data_error)?
{
current.merchant_key_encrypted = Some(projection.protected);
return Ok(Some(current));
}
record = self
.data
.find_payment_gateway_config_strong(&provider)
.await
.map_err(data_error)?;
}
Err(GatewayError::Internal(format!(
"payment gateway secret migration did not converge for {provider}"
)))
}
pub(crate) async fn compare_and_swap_payment_gateway_config(
&self,
input: &PaymentGatewayConfigCasWriteInput,
) -> Result<LocalMutationOutcome<PaymentGatewayConfigRecord>, GatewayError> {
self.data
.compare_and_swap_payment_gateway_config(input)
.await
.map(local_mutation_outcome)
.map_err(data_error)
}
@@ -608,13 +674,20 @@ impl AppState {
#[cfg(test)]
mod tests {
use std::sync::Arc;
use std::time::Duration;
use aether_crypto::{encrypt_python_fernet_plaintext, DEVELOPMENT_ENCRYPTION_KEY};
use aether_data::repository::billing::InMemoryBillingReadRepository;
use aether_data_contracts::repository::billing::{
BillingReadRepository, PaymentGatewayConfigWriteInput,
};
use serde_json::json;
use super::{
AdminBillingCollectorWriteInput, AdminBillingRuleWriteInput, AppState, LocalMutationOutcome,
};
use crate::data::GatewayDataState;
const CACHE_KEY: &str = "billing-mutation-test";
@@ -736,4 +809,59 @@ mod tests {
.get(&CACHE_KEY.to_string(), Duration::from_secs(60),)
.is_some());
}
#[tokio::test]
async fn gateway_lookup_lazily_migrates_legacy_secret_without_touching_other_fields() {
let repository = Arc::new(InMemoryBillingReadRepository::default());
let legacy = encrypt_python_fernet_plaintext(
DEVELOPMENT_ENCRYPTION_KEY,
r#"{"secret_key":"legacy-value"}"#,
)
.expect("legacy secret should encrypt");
repository
.upsert_payment_gateway_config(&PaymentGatewayConfigWriteInput {
provider: "stripe".to_string(),
enabled: true,
endpoint_url: "https://api.stripe.com".to_string(),
callback_base_url: Some("https://example.com".to_string()),
merchant_id: "merchant".to_string(),
merchant_key_encrypted: Some(legacy),
preserve_existing_secret: false,
pay_currency: "USD".to_string(),
usd_exchange_rate: 1.0,
min_recharge_usd: 1.0,
channels_json: json!({"channels": []}),
})
.await
.expect("gateway seed should succeed");
let before = repository
.find_payment_gateway_config("stripe")
.await
.expect("gateway lookup should succeed")
.expect("gateway should exist");
let data = GatewayDataState::with_billing_reader_for_tests(repository.clone())
.with_encryption_key_for_tests(DEVELOPMENT_ENCRYPTION_KEY);
let state = AppState::new()
.expect("app state should build")
.with_data_state_for_tests(data);
let migrated = state
.find_payment_gateway_config("stripe")
.await
.expect("gateway migration should succeed")
.expect("gateway should exist");
assert!(migrated
.merchant_key_encrypted
.as_deref()
.is_some_and(|value| value.starts_with("aether-payment-gateway-secret-v3:")));
let stored = repository
.find_payment_gateway_config("stripe")
.await
.expect("gateway lookup should succeed")
.expect("gateway should exist");
assert_eq!(stored, migrated);
assert_eq!(stored.updated_at_unix_secs, before.updated_at_unix_secs);
assert_eq!(stored.endpoint_url, before.endpoint_url);
assert_eq!(stored.channels_json, before.channels_json);
}
}
@@ -74,7 +74,7 @@ impl AppState {
let effective_status = if order.status == "pending"
&& order
.expires_at_unix_secs
.is_some_and(|value| value < now_unix_secs)
.is_some_and(|value| value <= now_unix_secs)
{
"expired"
} else {
@@ -2,8 +2,8 @@ use super::super::{
AdminBillingCollectorRecord, AdminBillingCollectorWriteInput, AdminBillingMutationOutcome,
AdminBillingPresetApplyResult, AdminBillingRuleRecord, AdminBillingRuleWriteInput, AppState,
BillingPlanRecord, BillingPlanWriteInput, GatewayError, LocalMutationOutcome,
PaymentGatewayConfigRecord, PaymentGatewayConfigWriteInput, UserDailyQuotaAvailabilityRecord,
UserPlanEntitlementRecord,
PaymentGatewayConfigCasWriteInput, PaymentGatewayConfigRecord, PaymentGatewayConfigWriteInput,
PaymentGatewaySecretCasUpdate, UserDailyQuotaAvailabilityRecord, UserPlanEntitlementRecord,
};
mod admin;