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
@@ -6,9 +6,10 @@ use async_trait::async_trait;
use crate::DataLayerError;
use aether_data_contracts::repository::management_tokens::{
CreateManagementTokenRecord, ManagementTokenListQuery, ManagementTokenReadRepository,
ManagementTokenWriteRepository, RegenerateManagementTokenSecret, StoredManagementToken,
StoredManagementTokenListPage, StoredManagementTokenWithUser, UpdateManagementTokenRecord,
ActivateManagementTokenIfMatches, CreateManagementTokenRecord, ManagementTokenListQuery,
ManagementTokenReadRepository, ManagementTokenWriteRepository, RegenerateManagementTokenSecret,
StoredManagementToken, StoredManagementTokenListPage, StoredManagementTokenWithUser,
UpdateManagementTokenRecord,
};
#[derive(Debug, Default)]
@@ -49,6 +50,189 @@ impl InMemoryManagementTokenRepository {
fn remove_hash_for_token(hashes: &mut BTreeMap<String, String>, token_id: &str) {
hashes.retain(|_, existing_token_id| existing_token_id != token_id);
}
fn update_management_token_scoped(
&self,
record: &UpdateManagementTokenRecord,
expected_user_id: Option<&str>,
) -> Result<Option<StoredManagementToken>, DataLayerError> {
record.validate()?;
let mut items = self
.items
.write()
.expect("management token repository lock");
let Some(index) = items.iter().position(|item| {
item.token.id == record.token_id
&& expected_user_id
.map(|user_id| item.token.user_id == user_id)
.unwrap_or(true)
}) else {
return Ok(None);
};
if let Some(name) = &record.name {
if items.iter().enumerate().any(|(position, item)| {
position != index
&& item.token.user_id == items[index].token.user_id
&& item.token.name == *name
}) {
return Err(DataLayerError::InvalidInput(format!(
"已存在名为 '{}' 的 Token",
name
)));
}
items[index].token.name = name.clone();
}
if record.clear_description {
items[index].token.description = None;
} else if let Some(description) = &record.description {
items[index].token.description = Some(description.clone());
}
if record.clear_allowed_ips {
items[index].token.allowed_ips = None;
} else if let Some(allowed_ips) = &record.allowed_ips {
items[index].token.allowed_ips = Some(allowed_ips.clone());
}
if let Some(permissions) = &record.permissions {
items[index].token.permissions = Some(permissions.clone());
}
if record.clear_expires_at {
items[index].token.expires_at_unix_secs = None;
} else if let Some(expires_at_unix_secs) = record.expires_at_unix_secs {
items[index].token.expires_at_unix_secs = Some(expires_at_unix_secs);
}
if let Some(is_active) = record.is_active {
items[index].token.is_active = is_active;
}
items[index].token.updated_at_unix_secs = Self::now_unix_secs();
Ok(Some(items[index].token.clone()))
}
fn delete_management_token_scoped(
&self,
token_id: &str,
expected_user_id: Option<&str>,
) -> bool {
let mut items = self
.items
.write()
.expect("management token repository lock");
let mut hashes = self
.hashes
.write()
.expect("management token repository lock");
let original_len = items.len();
items.retain(|item| {
item.token.id != token_id
|| expected_user_id
.map(|user_id| item.token.user_id != user_id)
.unwrap_or(false)
});
if items.len() != original_len {
Self::remove_hash_for_token(&mut hashes, token_id);
return true;
}
false
}
fn set_management_token_active_scoped(
&self,
token_id: &str,
expected_user_id: Option<&str>,
is_active: bool,
) -> Option<StoredManagementToken> {
let mut items = self
.items
.write()
.expect("management token repository lock");
let item = items.iter_mut().find(|item| {
item.token.id == token_id
&& expected_user_id
.map(|user_id| item.token.user_id == user_id)
.unwrap_or(true)
})?;
item.token.is_active = is_active;
item.token.updated_at_unix_secs = Self::now_unix_secs();
Some(item.token.clone())
}
fn activate_management_token_if_matches_inner(
&self,
mutation: &ActivateManagementTokenIfMatches,
) -> Result<bool, DataLayerError> {
mutation.validate()?;
// The in-memory token store does not own the independently stored user row and cannot
// atomically verify role/status/security_version with this mutation. Pretending that the
// user summary cached beside the token is authoritative would recreate the TOCTOU, so
// one-time install activation is intentionally unavailable on this backend.
Ok(false)
}
fn delete_inactive_management_token_if_matches_inner(
&self,
mutation: &ActivateManagementTokenIfMatches,
) -> Result<bool, DataLayerError> {
mutation.validate()?;
let mut items = self
.items
.write()
.expect("management token repository lock");
let mut hashes = self
.hashes
.write()
.expect("management token repository lock");
if hashes.get(&mutation.token_hash).map(String::as_str)
!= Some(mutation.expected_token.id.as_str())
{
return Ok(false);
}
let Some(index) = items.iter().position(|item| {
mutation.matches_locked_token_snapshot(&item.token, &mutation.token_hash)
}) else {
return Ok(false);
};
items.remove(index);
Self::remove_hash_for_token(&mut hashes, &mutation.expected_token.id);
Ok(true)
}
fn regenerate_management_token_secret_scoped(
&self,
mutation: &RegenerateManagementTokenSecret,
expected_user_id: Option<&str>,
) -> Result<Option<StoredManagementToken>, DataLayerError> {
mutation.validate()?;
let mut items = self
.items
.write()
.expect("management token repository lock");
let mut hashes = self
.hashes
.write()
.expect("management token repository lock");
let Some(item) = items.iter_mut().find(|item| {
item.token.id == mutation.token_id
&& expected_user_id
.map(|user_id| item.token.user_id == user_id)
.unwrap_or(true)
}) else {
return Ok(None);
};
Self::remove_hash_for_token(&mut hashes, &mutation.token_id);
hashes.insert(mutation.token_hash.clone(), mutation.token_id.clone());
item.token.token_prefix = mutation.token_prefix.clone();
item.token.updated_at_unix_secs = Self::now_unix_secs();
Ok(Some(item.token.clone()))
}
}
#[async_trait]
@@ -167,76 +351,27 @@ impl ManagementTokenWriteRepository for InMemoryManagementTokenRepository {
&self,
record: &UpdateManagementTokenRecord,
) -> Result<Option<StoredManagementToken>, DataLayerError> {
record.validate()?;
self.update_management_token_scoped(record, None)
}
let mut items = self
.items
.write()
.expect("management token repository lock");
let Some(index) = items
.iter()
.position(|item| item.token.id == record.token_id)
else {
return Ok(None);
};
if let Some(name) = &record.name {
if items.iter().enumerate().any(|(position, item)| {
position != index
&& item.token.user_id == items[index].token.user_id
&& item.token.name == *name
}) {
return Err(DataLayerError::InvalidInput(format!(
"已存在名为 '{}' 的 Token",
name
)));
}
items[index].token.name = name.clone();
}
if record.clear_description {
items[index].token.description = None;
} else if let Some(description) = &record.description {
items[index].token.description = Some(description.clone());
}
if record.clear_allowed_ips {
items[index].token.allowed_ips = None;
} else if let Some(allowed_ips) = &record.allowed_ips {
items[index].token.allowed_ips = Some(allowed_ips.clone());
}
if let Some(permissions) = &record.permissions {
items[index].token.permissions = Some(permissions.clone());
}
if record.clear_expires_at {
items[index].token.expires_at_unix_secs = None;
} else if let Some(expires_at_unix_secs) = record.expires_at_unix_secs {
items[index].token.expires_at_unix_secs = Some(expires_at_unix_secs);
}
if let Some(is_active) = record.is_active {
items[index].token.is_active = is_active;
}
items[index].token.updated_at_unix_secs = Self::now_unix_secs();
Ok(Some(items[index].token.clone()))
async fn update_management_token_for_user(
&self,
record: &UpdateManagementTokenRecord,
user_id: &str,
) -> Result<Option<StoredManagementToken>, DataLayerError> {
self.update_management_token_scoped(record, Some(user_id))
}
async fn delete_management_token(&self, token_id: &str) -> Result<bool, DataLayerError> {
let mut items = self
.items
.write()
.expect("management token repository lock");
let mut hashes = self
.hashes
.write()
.expect("management token repository lock");
let original_len = items.len();
items.retain(|item| item.token.id != token_id);
Self::remove_hash_for_token(&mut hashes, token_id);
Ok(items.len() != original_len)
Ok(self.delete_management_token_scoped(token_id, None))
}
async fn delete_management_token_for_user(
&self,
token_id: &str,
user_id: &str,
) -> Result<bool, DataLayerError> {
Ok(self.delete_management_token_scoped(token_id, Some(user_id)))
}
async fn set_management_token_active(
@@ -244,43 +379,45 @@ impl ManagementTokenWriteRepository for InMemoryManagementTokenRepository {
token_id: &str,
is_active: bool,
) -> Result<Option<StoredManagementToken>, DataLayerError> {
let mut items = self
.items
.write()
.expect("management token repository lock");
let Some(item) = items.iter_mut().find(|item| item.token.id == token_id) else {
return Ok(None);
};
item.token.is_active = is_active;
item.token.updated_at_unix_secs = Self::now_unix_secs();
Ok(Some(item.token.clone()))
Ok(self.set_management_token_active_scoped(token_id, None, is_active))
}
async fn set_management_token_active_for_user(
&self,
token_id: &str,
user_id: &str,
is_active: bool,
) -> Result<Option<StoredManagementToken>, DataLayerError> {
Ok(self.set_management_token_active_scoped(token_id, Some(user_id), is_active))
}
async fn activate_management_token_if_matches(
&self,
mutation: &ActivateManagementTokenIfMatches,
) -> Result<bool, DataLayerError> {
self.activate_management_token_if_matches_inner(mutation)
}
async fn delete_inactive_management_token_if_matches(
&self,
mutation: &ActivateManagementTokenIfMatches,
) -> Result<bool, DataLayerError> {
self.delete_inactive_management_token_if_matches_inner(mutation)
}
async fn regenerate_management_token_secret(
&self,
mutation: &RegenerateManagementTokenSecret,
) -> Result<Option<StoredManagementToken>, DataLayerError> {
mutation.validate()?;
self.regenerate_management_token_secret_scoped(mutation, None)
}
let mut items = self
.items
.write()
.expect("management token repository lock");
let mut hashes = self
.hashes
.write()
.expect("management token repository lock");
let Some(item) = items
.iter_mut()
.find(|item| item.token.id == mutation.token_id)
else {
return Ok(None);
};
Self::remove_hash_for_token(&mut hashes, &mutation.token_id);
hashes.insert(mutation.token_hash.clone(), mutation.token_id.clone());
item.token.token_prefix = mutation.token_prefix.clone();
item.token.updated_at_unix_secs = Self::now_unix_secs();
Ok(Some(item.token.clone()))
async fn regenerate_management_token_secret_for_user(
&self,
mutation: &RegenerateManagementTokenSecret,
user_id: &str,
) -> Result<Option<StoredManagementToken>, DataLayerError> {
self.regenerate_management_token_secret_scoped(mutation, Some(user_id))
}
async fn record_management_token_usage(
@@ -307,10 +444,10 @@ impl ManagementTokenWriteRepository for InMemoryManagementTokenRepository {
mod tests {
use super::InMemoryManagementTokenRepository;
use crate::repository::management_tokens::{
CreateManagementTokenRecord, ManagementTokenListQuery, ManagementTokenReadRepository,
ManagementTokenWriteRepository, RegenerateManagementTokenSecret, StoredManagementToken,
StoredManagementTokenUserSummary, StoredManagementTokenWithUser,
UpdateManagementTokenRecord,
ActivateManagementTokenIfMatches, CreateManagementTokenRecord, ManagementTokenListQuery,
ManagementTokenReadRepository, ManagementTokenWriteRepository,
RegenerateManagementTokenSecret, StoredManagementToken, StoredManagementTokenUserSummary,
StoredManagementTokenWithUser, UpdateManagementTokenRecord,
};
fn sample_token(id: &str, user_id: &str, is_active: bool) -> StoredManagementTokenWithUser {
@@ -456,4 +593,108 @@ mod tests {
.expect("hash lookup should succeed");
assert!(deleted_by_hash.is_none());
}
#[tokio::test]
async fn owner_scoped_mutations_never_cross_user_boundaries() {
let repository = InMemoryManagementTokenRepository::seed_with_hashes(
vec![sample_token("token-1", "user-1", true)],
vec![("hash-1".to_string(), "token-1".to_string())],
);
let update = UpdateManagementTokenRecord {
token_id: "token-1".to_string(),
name: Some("hijacked".to_string()),
description: None,
clear_description: false,
allowed_ips: None,
clear_allowed_ips: false,
permissions: None,
expires_at_unix_secs: None,
clear_expires_at: false,
is_active: None,
};
assert!(repository
.update_management_token_for_user(&update, "user-2")
.await
.expect("scoped update should execute")
.is_none());
assert!(repository
.set_management_token_active_for_user("token-1", "user-2", false)
.await
.expect("scoped toggle should execute")
.is_none());
assert!(repository
.regenerate_management_token_secret_for_user(
&RegenerateManagementTokenSecret {
token_id: "token-1".to_string(),
token_hash: "hash-hijacked".to_string(),
token_prefix: Some("ae_hijacked".to_string()),
},
"user-2",
)
.await
.expect("scoped regeneration should execute")
.is_none());
assert!(!repository
.delete_management_token_for_user("token-1", "user-2")
.await
.expect("scoped delete should execute"));
let unchanged = repository
.get_management_token_with_user_by_hash("hash-1")
.await
.expect("original hash lookup should succeed")
.expect("token should remain");
assert_eq!(unchanged.token.name, "token-1");
assert!(unchanged.token.is_active);
assert!(repository
.get_management_token_with_user_by_hash("hash-hijacked")
.await
.expect("replacement hash lookup should succeed")
.is_none());
}
#[tokio::test]
async fn install_activation_fails_closed_without_atomic_user_state() {
let mut pending = sample_token("token-1", "user-1", false);
pending.token.allowed_ips = Some(serde_json::json!(["127.0.0.1"]));
pending.token.permissions = Some(serde_json::json!(["admin:proxy_nodes:write"]));
pending.token.expires_at_unix_secs = Some(1_800_000_000);
let expected_token = pending.token.clone();
let repository = InMemoryManagementTokenRepository::seed_with_hashes(
[pending],
[("hash-1".to_string(), "token-1".to_string())],
);
let expected = ActivateManagementTokenIfMatches {
expected_token,
token_hash: "hash-1".to_string(),
expected_user_security_version: 4,
now_unix_secs: 1_700_000_000,
};
let mut mismatched = expected.clone();
mismatched.expected_token.permissions =
Some(serde_json::json!(["admin:proxy_nodes:admin"]));
assert!(!repository
.activate_management_token_if_matches(&mismatched)
.await
.expect("mismatched activation should execute"));
assert!(!repository
.activate_management_token_if_matches(&expected)
.await
.expect("memory activation should fail closed"));
assert!(
!repository
.get_management_token_with_user("token-1")
.await
.expect("token lookup should execute")
.expect("token should remain")
.token
.is_active
);
assert!(repository
.delete_inactive_management_token_if_matches(&expected)
.await
.expect("exact inactive snapshot cleanup should execute"));
}
}
@@ -1,10 +1,10 @@
mod memory;
pub use aether_data_contracts::repository::management_tokens::{
CreateManagementTokenRecord, ManagementTokenListQuery, ManagementTokenReadRepository,
ManagementTokenWriteRepository, RegenerateManagementTokenSecret, StoredManagementToken,
StoredManagementTokenListPage, StoredManagementTokenUserSummary, StoredManagementTokenWithUser,
UpdateManagementTokenRecord,
ActivateManagementTokenIfMatches, CreateManagementTokenRecord, ManagementTokenListQuery,
ManagementTokenReadRepository, ManagementTokenWriteRepository, RegenerateManagementTokenSecret,
StoredManagementToken, StoredManagementTokenListPage, StoredManagementTokenUserSummary,
StoredManagementTokenWithUser, UpdateManagementTokenRecord,
};
#[cfg(feature = "mysql")]
pub use aether_data_mysql::MysqlManagementTokenRepository;