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
@@ -10,9 +10,11 @@ use sqlx::{
use aether_data_contracts::repository::provider_catalog::{
ProviderCatalogKeyAdaptiveStateUpdate, ProviderCatalogKeyAdminCasUpdate,
ProviderCatalogKeyHealthStateUpdate, ProviderCatalogKeyListOrder, ProviderCatalogKeyListQuery,
ProviderCatalogKeyCredentialsCasUpdate, ProviderCatalogKeyHealthStateUpdate,
ProviderCatalogKeyListOrder, ProviderCatalogKeyListQuery,
ProviderCatalogKeyOAuthCredentialCasDelete, ProviderCatalogKeyOAuthRuntimeStateCasUpdate,
ProviderCatalogKeyRuntimeMetadataUpdate, ProviderCatalogKeyStatusSnapshotUpdate,
ProviderCatalogProviderConfigCasUpdate, ProviderCatalogProxyCasUpdate,
ProviderCatalogReadRepository, ProviderCatalogUpstreamMetadataNamespaceUpdate,
ProviderCatalogWriteRepository, StoredProviderCatalogEndpoint, StoredProviderCatalogKey,
StoredProviderCatalogKeyMaintenanceSummary, StoredProviderCatalogKeyPage,
@@ -273,6 +275,7 @@ SELECT
is_active,
api_formats,
NULL::jsonb AS auth_type_by_format,
NULL::jsonb AS allow_auth_channel_mismatch_formats,
'summary' AS api_key,
CASE
WHEN auth_config IS NULL THEN NULL
@@ -966,56 +969,11 @@ impl SqlxProviderCatalogReadRepository {
.await
}
pub async fn update_key_oauth_credentials(
&self,
key_id: &str,
encrypted_api_key: &str,
encrypted_auth_config: Option<&str>,
expires_at_unix_secs: Option<u64>,
) -> Result<bool, DataLayerError> {
if key_id.trim().is_empty() {
return Err(DataLayerError::InvalidInput(
"provider catalog key_id is empty".to_string(),
));
}
if encrypted_api_key.trim().is_empty() {
return Err(DataLayerError::InvalidInput(
"provider catalog oauth api_key is empty".to_string(),
));
}
let rows_affected = sqlx::query(
r#"
UPDATE provider_api_keys
SET
api_key = $2,
auth_config = $3,
expires_at = CASE
WHEN $4::double precision IS NULL THEN NULL
ELSE TO_TIMESTAMP($4::double precision)
END,
updated_at = NOW()
WHERE id = $1
"#,
)
.bind(key_id)
.bind(encrypted_api_key)
.bind(encrypted_auth_config)
.bind(expires_at_unix_secs.map(|value| value as f64))
.execute(&self.pool)
.await
.map_postgres_err()?
.rows_affected();
Ok(rows_affected > 0)
}
pub async fn update_key_oauth_runtime_state(
&self,
key_id: &str,
oauth_invalid_at_unix_secs: Option<u64>,
oauth_invalid_reason: Option<&str>,
encrypted_auth_config_update: Option<&str>,
updated_at_unix_secs: Option<u64>,
) -> Result<bool, DataLayerError> {
if key_id.trim().is_empty() {
@@ -1032,10 +990,9 @@ SET
ELSE TO_TIMESTAMP($2::double precision)
END,
oauth_invalid_reason = $3,
auth_config = COALESCE($4, auth_config),
updated_at = CASE
WHEN $5::double precision IS NULL THEN NOW()
ELSE TO_TIMESTAMP($5::double precision)
WHEN $4::double precision IS NULL THEN NOW()
ELSE TO_TIMESTAMP($4::double precision)
END
WHERE id = $1
"#,
@@ -1043,7 +1000,6 @@ WHERE id = $1
.bind(key_id)
.bind(oauth_invalid_at_unix_secs.map(|value| value as f64))
.bind(oauth_invalid_reason)
.bind(encrypted_auth_config_update)
.bind(updated_at_unix_secs.map(|value| value as f64))
.execute(&self.pool)
.await
@@ -1512,6 +1468,60 @@ WHERE id = $1
})
}
pub async fn compare_and_swap_provider_config(
&self,
update: &ProviderCatalogProviderConfigCasUpdate,
) -> Result<bool, DataLayerError> {
if update.provider_id.trim().is_empty() {
return Err(DataLayerError::InvalidInput(
"provider catalog provider_id is empty".to_string(),
));
}
let rows_affected = sqlx::query(
r#"
UPDATE providers
SET config = $3, updated_at = NOW()
WHERE id = $1
AND config::jsonb IS NOT DISTINCT FROM $2::jsonb
"#,
)
.bind(&update.provider_id)
.bind(&update.expected_config)
.bind(&update.config)
.execute(&self.pool)
.await
.map_postgres_err()?
.rows_affected();
Ok(rows_affected == 1)
}
pub async fn compare_and_swap_provider_proxy(
&self,
update: &ProviderCatalogProxyCasUpdate,
) -> Result<bool, DataLayerError> {
if update.record_id.trim().is_empty() {
return Err(DataLayerError::InvalidInput(
"provider catalog provider_id is empty".to_string(),
));
}
let rows_affected = sqlx::query(
r#"
UPDATE providers
SET proxy = $3, updated_at = NOW()
WHERE id = $1
AND proxy::jsonb IS NOT DISTINCT FROM $2::jsonb
"#,
)
.bind(&update.record_id)
.bind(&update.expected_proxy)
.bind(&update.proxy)
.execute(&self.pool)
.await
.map_postgres_err()?
.rows_affected();
Ok(rows_affected == 1)
}
pub async fn delete_provider(&self, provider_id: &str) -> Result<bool, DataLayerError> {
if provider_id.trim().is_empty() {
return Err(DataLayerError::InvalidInput(
@@ -2169,6 +2179,33 @@ WHERE id = $1
})
}
pub async fn compare_and_swap_endpoint_proxy(
&self,
update: &ProviderCatalogProxyCasUpdate,
) -> Result<bool, DataLayerError> {
if update.record_id.trim().is_empty() {
return Err(DataLayerError::InvalidInput(
"provider catalog endpoint_id is empty".to_string(),
));
}
let rows_affected = sqlx::query(
r#"
UPDATE provider_endpoints
SET proxy = $3, updated_at = NOW()
WHERE id = $1
AND proxy::jsonb IS NOT DISTINCT FROM $2::jsonb
"#,
)
.bind(&update.record_id)
.bind(&update.expected_proxy)
.bind(&update.proxy)
.execute(&self.pool)
.await
.map_postgres_err()?
.rows_affected();
Ok(rows_affected == 1)
}
pub async fn delete_endpoint(&self, endpoint_id: &str) -> Result<bool, DataLayerError> {
if endpoint_id.trim().is_empty() {
return Err(DataLayerError::InvalidInput(
@@ -2221,6 +2258,65 @@ WHERE id = $1
})
}
pub async fn compare_and_swap_key_proxy(
&self,
update: &ProviderCatalogProxyCasUpdate,
) -> Result<bool, DataLayerError> {
if update.record_id.trim().is_empty() {
return Err(DataLayerError::InvalidInput(
"provider catalog key_id is empty".to_string(),
));
}
let rows_affected = sqlx::query(
r#"
UPDATE provider_api_keys
SET proxy = $3, updated_at = NOW()
WHERE id = $1
AND proxy::jsonb IS NOT DISTINCT FROM $2::jsonb
"#,
)
.bind(&update.record_id)
.bind(&update.expected_proxy)
.bind(&update.proxy)
.execute(&self.pool)
.await
.map_postgres_err()?
.rows_affected();
Ok(rows_affected == 1)
}
pub async fn compare_and_swap_key_credentials(
&self,
update: &ProviderCatalogKeyCredentialsCasUpdate,
) -> Result<bool, DataLayerError> {
if update.key_id.trim().is_empty() || update.expected_provider_id.trim().is_empty() {
return Err(DataLayerError::InvalidInput(
"provider catalog key credential CAS requires key_id and provider_id".to_string(),
));
}
let rows_affected = sqlx::query(
r#"
UPDATE provider_api_keys
SET api_key = $5, encrypted_key = NULL, auth_config = $6
WHERE id = $1
AND provider_id = $2
AND COALESCE(api_key, encrypted_key) IS NOT DISTINCT FROM $3
AND auth_config IS NOT DISTINCT FROM $4
"#,
)
.bind(&update.key_id)
.bind(&update.expected_provider_id)
.bind(update.expected_encrypted_api_key.as_deref())
.bind(update.expected_encrypted_auth_config.as_deref())
.bind(update.encrypted_api_key.as_deref())
.bind(update.encrypted_auth_config.as_deref())
.execute(&self.pool)
.await
.map_postgres_err()?
.rows_affected();
Ok(rows_affected == 1)
}
pub async fn compare_and_update_key_admin_state(
&self,
update: &ProviderCatalogKeyAdminCasUpdate,
@@ -2934,6 +3030,20 @@ impl ProviderCatalogWriteRepository for SqlxProviderCatalogReadRepository {
Self::update_provider(self, provider).await
}
async fn compare_and_swap_provider_config(
&self,
update: &ProviderCatalogProviderConfigCasUpdate,
) -> Result<bool, DataLayerError> {
Self::compare_and_swap_provider_config(self, update).await
}
async fn compare_and_swap_provider_proxy(
&self,
update: &ProviderCatalogProxyCasUpdate,
) -> Result<bool, DataLayerError> {
Self::compare_and_swap_provider_proxy(self, update).await
}
async fn delete_provider(&self, provider_id: &str) -> Result<bool, DataLayerError> {
Self::delete_provider(self, provider_id).await
}
@@ -2969,6 +3079,13 @@ impl ProviderCatalogWriteRepository for SqlxProviderCatalogReadRepository {
self.update_endpoint(endpoint).await
}
async fn compare_and_swap_endpoint_proxy(
&self,
update: &ProviderCatalogProxyCasUpdate,
) -> Result<bool, DataLayerError> {
Self::compare_and_swap_endpoint_proxy(self, update).await
}
async fn delete_endpoint(&self, endpoint_id: &str) -> Result<bool, DataLayerError> {
Self::delete_endpoint(self, endpoint_id).await
}
@@ -2987,6 +3104,20 @@ impl ProviderCatalogWriteRepository for SqlxProviderCatalogReadRepository {
Self::update_key(self, key).await
}
async fn compare_and_swap_key_proxy(
&self,
update: &ProviderCatalogProxyCasUpdate,
) -> Result<bool, DataLayerError> {
Self::compare_and_swap_key_proxy(self, update).await
}
async fn compare_and_swap_key_credentials(
&self,
update: &ProviderCatalogKeyCredentialsCasUpdate,
) -> Result<bool, DataLayerError> {
Self::compare_and_swap_key_credentials(self, update).await
}
async fn compare_and_update_key_admin_state(
&self,
update: &ProviderCatalogKeyAdminCasUpdate,
@@ -3081,29 +3212,11 @@ impl ProviderCatalogWriteRepository for SqlxProviderCatalogReadRepository {
Self::clear_key_oauth_invalid_marker(self, key_id).await
}
async fn update_key_oauth_credentials(
&self,
key_id: &str,
encrypted_api_key: &str,
encrypted_auth_config: Option<&str>,
expires_at_unix_secs: Option<u64>,
) -> Result<bool, DataLayerError> {
Self::update_key_oauth_credentials(
self,
key_id,
encrypted_api_key,
encrypted_auth_config,
expires_at_unix_secs,
)
.await
}
async fn update_key_oauth_runtime_state(
&self,
key_id: &str,
oauth_invalid_at_unix_secs: Option<u64>,
oauth_invalid_reason: Option<&str>,
encrypted_auth_config_update: Option<&str>,
updated_at_unix_secs: Option<u64>,
) -> Result<bool, DataLayerError> {
Self::update_key_oauth_runtime_state(
@@ -3111,7 +3224,6 @@ impl ProviderCatalogWriteRepository for SqlxProviderCatalogReadRepository {
key_id,
oauth_invalid_at_unix_secs,
oauth_invalid_reason,
encrypted_auth_config_update,
updated_at_unix_secs,
)
.await
@@ -3236,6 +3348,16 @@ where
row.try_get(column).map_postgres_err()
}
fn optional_u64(value: Option<i64>, field_name: &str) -> Result<Option<u64>, DataLayerError> {
value
.map(|value| {
u64::try_from(value).map_err(|_| {
DataLayerError::UnexpectedValue(format!("invalid {field_name}: {value}"))
})
})
.transpose()
}
fn map_provider_row(row: &PgRow) -> Result<StoredProviderCatalogProvider, DataLayerError> {
let quota_reset_day = row_get::<Option<i32>>(row, "quota_reset_day")?
.map(|value| {
@@ -3276,8 +3398,14 @@ fn map_provider_row(row: &PgRow) -> Result<StoredProviderCatalogProvider, DataLa
row_get(row, "monthly_quota_usd")?,
row_get(row, "monthly_used_usd")?,
quota_reset_day,
row_get::<Option<i64>>(row, "quota_last_reset_at_unix_secs")?.map(|value| value as u64),
row_get::<Option<i64>>(row, "quota_expires_at_unix_secs")?.map(|value| value as u64),
optional_u64(
row_get(row, "quota_last_reset_at_unix_secs")?,
"providers.quota_last_reset_at",
)?,
optional_u64(
row_get(row, "quota_expires_at_unix_secs")?,
"providers.quota_expires_at",
)?,
)
.with_routing_fields(row_get(row, "provider_priority")?)
.with_transport_fields(
@@ -3394,6 +3522,10 @@ fn map_key_maintenance_summary_row(
}
fn map_key_row(row: &PgRow) -> Result<StoredProviderCatalogKey, DataLayerError> {
let expires_at_unix_secs = optional_u64(
row_get(row, "expires_at_unix_secs")?,
"provider_api_keys.expires_at",
)?;
let rpm_limit = row_get::<Option<i32>>(row, "rpm_limit")?
.map(|value| {
u32::try_from(value).map_err(|_| {
@@ -3544,6 +3676,9 @@ fn map_key_row(row: &PgRow) -> Result<StoredProviderCatalogKey, DataLayerError>
})
})
.transpose()?;
let auth_type_by_format: Option<serde_json::Value> = row_get(row, "auth_type_by_format")?;
let allow_auth_channel_mismatch_formats: Option<serde_json::Value> =
row_get(row, "allow_auth_channel_mismatch_formats")?;
StoredProviderCatalogKey::new(
row_get(row, "id")?,
@@ -3560,8 +3695,7 @@ fn map_key_row(row: &PgRow) -> Result<StoredProviderCatalogKey, DataLayerError>
row_get(row, "rate_multipliers")?,
row_get(row, "global_priority_by_format")?,
row_get(row, "allowed_models")?,
row_get::<Option<i64>>(row, "expires_at_unix_secs")?
.and_then(|value| u64::try_from(value).ok()),
expires_at_unix_secs,
row_get(row, "proxy")?,
row_get(row, "fingerprint")?,
)
@@ -3588,9 +3722,6 @@ fn map_key_row(row: &PgRow) -> Result<StoredProviderCatalogKey, DataLayerError>
row.try_get("circuit_breaker_by_format").ok(),
);
key.note = row.try_get("note").ok();
key.auth_type_by_format = row.try_get("auth_type_by_format").ok();
key.allow_auth_channel_mismatch_formats =
row.try_get("allow_auth_channel_mismatch_formats").ok();
key.internal_priority = row.try_get("internal_priority").unwrap_or(50);
key.cache_ttl_minutes = row.try_get("cache_ttl_minutes").unwrap_or(5);
key.max_probe_interval_minutes = row.try_get("max_probe_interval_minutes").unwrap_or(32);
@@ -3613,6 +3744,12 @@ fn map_key_row(row: &PgRow) -> Result<StoredProviderCatalogKey, DataLayerError>
key.updated_at_unix_secs = updated_at_unix_secs;
key
})
.and_then(|key| {
key.with_auth_channel_policy_fields(
auth_type_by_format,
allow_auth_channel_mismatch_formats,
)
})
}
#[cfg(test)]
@@ -3620,7 +3757,7 @@ mod tests {
use aether_data_contracts::repository::provider_catalog::ProviderCatalogKeyRuntimeMetadataUpdate;
use serde_json::json;
use super::SqlxProviderCatalogReadRepository;
use super::{optional_u64, SqlxProviderCatalogReadRepository};
use crate::{PostgresPoolConfig, PostgresPoolFactory};
#[tokio::test]
@@ -3642,6 +3779,16 @@ mod tests {
let _ = repository.pool();
}
#[test]
fn provider_catalog_negative_security_timestamps_fail_closed() {
assert!(optional_u64(Some(-1), "provider_api_keys.expires_at").is_err());
assert_eq!(
optional_u64(None, "provider_api_keys.expires_at")
.expect("SQL NULL should remain optional"),
None
);
}
#[test]
fn key_queries_include_usage_totals() {
for sql in [
@@ -3710,7 +3857,7 @@ mod tests {
);
assert!(source.contains("QueryBuilder::<Postgres>::new(select_prefix_for_in("));
assert!(source.contains(".bind(&key.allow_auth_channel_mismatch_formats)"));
assert!(source.contains("row.try_get(\"allow_auth_channel_mismatch_formats\").ok()"));
assert!(source.contains("row_get(row, \"allow_auth_channel_mismatch_formats\")?"));
}
#[test]
@@ -3903,6 +4050,13 @@ VALUES ($1, $2, $3, 0, 0, $4::jsonb)
assert!(sql.contains("auth_config is not distinct from $6"));
}
#[test]
fn credential_cas_migrates_legacy_encrypted_key_with_null_safe_fence() {
let source = include_str!("provider_catalog.rs");
assert!(source.contains("SET api_key = $5, encrypted_key = NULL, auth_config = $6"));
assert!(source.contains("AND COALESCE(api_key, encrypted_key) IS NOT DISTINCT FROM $3"));
}
#[test]
fn admin_credential_cas_has_atomic_rotation_guards() {
let source = include_str!("provider_catalog.rs");