mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-13 22:50:19 +08:00
fix(ci): align lint-safe security paths
This commit is contained in:
@@ -914,7 +914,7 @@ impl GatewayDataState {
|
|||||||
|
|
||||||
Ok(Some(LdapAuthProvisioningResult {
|
Ok(Some(LdapAuthProvisioningResult {
|
||||||
user: outcome.user,
|
user: outcome.user,
|
||||||
owned_wallet_id: initialized.created.then(|| initialized.wallet.id),
|
owned_wallet_id: initialized.created.then_some(initialized.wallet.id),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ fn smtp_read_response<T: std::io::BufRead>(reader: &mut T) -> Result<(u16, Strin
|
|||||||
if message
|
if message
|
||||||
.len()
|
.len()
|
||||||
.checked_add(additional)
|
.checked_add(additional)
|
||||||
.map_or(true, |length| length > SMTP_MAX_RESPONSE_BYTES)
|
.is_none_or(|length| length > SMTP_MAX_RESPONSE_BYTES)
|
||||||
{
|
{
|
||||||
return Err(GatewayError::Internal(
|
return Err(GatewayError::Internal(
|
||||||
"smtp response exceeds the allowed size".to_string(),
|
"smtp response exceeds the allowed size".to_string(),
|
||||||
@@ -476,7 +476,7 @@ fn read_smtp_response_line<T: std::io::BufRead>(
|
|||||||
if line
|
if line
|
||||||
.len()
|
.len()
|
||||||
.checked_add(take)
|
.checked_add(take)
|
||||||
.map_or(true, |length| length > SMTP_MAX_RESPONSE_LINE_BYTES)
|
.is_none_or(|length| length > SMTP_MAX_RESPONSE_LINE_BYTES)
|
||||||
{
|
{
|
||||||
return Err(std::io::Error::new(
|
return Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::InvalidData,
|
std::io::ErrorKind::InvalidData,
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ use aether_scheduler_core::SchedulerRequestCandidateStatusUpdate;
|
|||||||
use aether_usage_runtime::{
|
use aether_usage_runtime::{
|
||||||
build_lifecycle_usage_seed, build_stream_terminal_usage_payload_seed,
|
build_lifecycle_usage_seed, build_stream_terminal_usage_payload_seed,
|
||||||
build_terminal_usage_context_seed, stream_report_represents_failure,
|
build_terminal_usage_context_seed, stream_report_represents_failure,
|
||||||
DEFAULT_USAGE_RESPONSE_BODY_CAPTURE_LIMIT_BYTES,
|
|
||||||
};
|
};
|
||||||
use base64::Engine as _;
|
use base64::Engine as _;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -433,8 +432,7 @@ impl AttemptBodyCapture {
|
|||||||
if bytes.is_empty() || self.truncated {
|
if bytes.is_empty() || self.truncated {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let max_bytes = DEFAULT_USAGE_RESPONSE_BODY_CAPTURE_LIMIT_BYTES
|
let max_bytes = crate::execution_runtime::MAX_STREAM_BODY_CAPTURE_BYTES;
|
||||||
.min(crate::execution_runtime::MAX_STREAM_BODY_CAPTURE_BYTES);
|
|
||||||
if self.buffer.len() >= max_bytes {
|
if self.buffer.len() >= max_bytes {
|
||||||
self.truncated = true;
|
self.truncated = true;
|
||||||
return;
|
return;
|
||||||
@@ -448,10 +446,7 @@ impl AttemptBodyCapture {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn encode(&self) -> (Option<String>, Option<UsageBodyCaptureState>) {
|
pub(crate) fn encode(&self) -> (Option<String>, Option<UsageBodyCaptureState>) {
|
||||||
self.encode_with_limit(
|
self.encode_with_limit(crate::execution_runtime::MAX_STREAM_BODY_CAPTURE_BYTES)
|
||||||
DEFAULT_USAGE_RESPONSE_BODY_CAPTURE_LIMIT_BYTES
|
|
||||||
.min(crate::execution_runtime::MAX_STREAM_BODY_CAPTURE_BYTES),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_with_limit(
|
fn encode_with_limit(
|
||||||
@@ -1435,8 +1430,7 @@ mod stage_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn body_capture_encodes_inline_and_empty_states() {
|
fn body_capture_encodes_inline_and_empty_states() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
super::DEFAULT_USAGE_RESPONSE_BODY_CAPTURE_LIMIT_BYTES
|
crate::execution_runtime::MAX_STREAM_BODY_CAPTURE_BYTES,
|
||||||
.min(crate::execution_runtime::MAX_STREAM_BODY_CAPTURE_BYTES),
|
|
||||||
crate::execution_runtime::MAX_STREAM_BODY_CAPTURE_BYTES
|
crate::execution_runtime::MAX_STREAM_BODY_CAPTURE_BYTES
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -585,7 +585,7 @@ fn harden_execution_runtime_socket(
|
|||||||
// runners. The validated canonical parent and inode identity still
|
// runners. The validated canonical parent and inode identity still
|
||||||
// close the replacement window without relying on that differing
|
// close the replacement window without relying on that differing
|
||||||
// device number.
|
// device number.
|
||||||
|| (cfg!(target_os = "linux") && metadata.ino() != stat.st_ino as u64)
|
|| (cfg!(target_os = "linux") && metadata.ino() != stat.st_ino)
|
||||||
{
|
{
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::PermissionDenied,
|
io::ErrorKind::PermissionDenied,
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ use aether_usage_runtime::{
|
|||||||
build_lifecycle_usage_seed, build_stream_terminal_usage_payload_seed,
|
build_lifecycle_usage_seed, build_stream_terminal_usage_payload_seed,
|
||||||
build_sync_terminal_usage_payload_seed, build_terminal_usage_context_seed, LifecycleUsageSeed,
|
build_sync_terminal_usage_payload_seed, build_terminal_usage_context_seed, LifecycleUsageSeed,
|
||||||
SyncTerminalUsagePayloadSeed, TerminalUsageContextSeed, UsageRequestRecordLevel,
|
SyncTerminalUsagePayloadSeed, TerminalUsageContextSeed, UsageRequestRecordLevel,
|
||||||
DEFAULT_USAGE_RESPONSE_BODY_CAPTURE_LIMIT_BYTES,
|
|
||||||
};
|
};
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
use axum::body::{Body, Bytes};
|
use axum::body::{Body, Bytes};
|
||||||
@@ -399,8 +398,7 @@ fn direct_passthrough_mode() -> DirectPassthroughMode {
|
|||||||
fn stream_body_buffer_limit_for_record_level(record_level: UsageRequestRecordLevel) -> usize {
|
fn stream_body_buffer_limit_for_record_level(record_level: UsageRequestRecordLevel) -> usize {
|
||||||
match record_level {
|
match record_level {
|
||||||
UsageRequestRecordLevel::Basic => BASIC_STREAM_BODY_ANALYSIS_LIMIT_BYTES,
|
UsageRequestRecordLevel::Basic => BASIC_STREAM_BODY_ANALYSIS_LIMIT_BYTES,
|
||||||
UsageRequestRecordLevel::Full => DEFAULT_USAGE_RESPONSE_BODY_CAPTURE_LIMIT_BYTES
|
UsageRequestRecordLevel::Full => crate::execution_runtime::MAX_STREAM_BODY_CAPTURE_BYTES,
|
||||||
.min(crate::execution_runtime::MAX_STREAM_BODY_CAPTURE_BYTES),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -686,8 +686,9 @@ pub(crate) async fn read_admin_external_models_cache(
|
|||||||
match fetch_admin_external_models_from_source(state, request_id, proxy_node_id.as_deref()).await
|
match fetch_admin_external_models_from_source(state, request_id, proxy_node_id.as_deref()).await
|
||||||
{
|
{
|
||||||
Ok(payload) => {
|
Ok(payload) => {
|
||||||
if let Err(_) =
|
if store_admin_external_models_cache(state, proxy_node_id.as_deref(), &payload)
|
||||||
store_admin_external_models_cache(state, proxy_node_id.as_deref(), &payload).await
|
.await
|
||||||
|
.is_err()
|
||||||
{
|
{
|
||||||
warn!("failed to store fetched external models cache");
|
warn!("failed to store fetched external models cache");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ async fn release_codex_agent_identity_leases(
|
|||||||
leases: Vec<RuntimeLockLease>,
|
leases: Vec<RuntimeLockLease>,
|
||||||
) {
|
) {
|
||||||
for lease in leases {
|
for lease in leases {
|
||||||
if let Err(_) = state.runtime_state().lock_release(&lease).await {
|
if state.runtime_state().lock_release(&lease).await.is_err() {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
lock_key = %lease.key,
|
lock_key = %lease.key,
|
||||||
"gateway Agent Identity enrollment lock release failed"
|
"gateway Agent Identity enrollment lock release failed"
|
||||||
|
|||||||
@@ -552,7 +552,13 @@ pub(super) async fn seed_provider_oauth_pool_score(
|
|||||||
now_unix_secs,
|
now_unix_secs,
|
||||||
pool_config.score_rules,
|
pool_config.score_rules,
|
||||||
);
|
);
|
||||||
if let Err(_) = state.app().data.upsert_pool_member_score(upsert).await {
|
if state
|
||||||
|
.app()
|
||||||
|
.data
|
||||||
|
.upsert_pool_member_score(upsert)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
provider_id = %provider_id,
|
provider_id = %provider_id,
|
||||||
key_id = %key.id,
|
key_id = %key.id,
|
||||||
|
|||||||
@@ -7632,10 +7632,11 @@ impl<'a> AdminAppState<'a> {
|
|||||||
// Legacy uploads historically normalize an omitted rate limit to zero. Rollback
|
// Legacy uploads historically normalize an omitted rate limit to zero. Rollback
|
||||||
// checkpoints instead preserve the nullable database value exactly.
|
// checkpoints instead preserve the nullable database value exactly.
|
||||||
let rate_limit = imported_rate_limit.unwrap_or(0);
|
let rate_limit = imported_rate_limit.unwrap_or(0);
|
||||||
let rate_limit_value = mode
|
let rate_limit_value = if mode.is_rollback_checkpoint() {
|
||||||
.is_rollback_checkpoint()
|
imported_rate_limit
|
||||||
.then_some(imported_rate_limit)
|
} else {
|
||||||
.unwrap_or(Some(rate_limit));
|
Some(rate_limit)
|
||||||
|
};
|
||||||
let concurrent_limit = invalid_value!(imported_optional_i32(
|
let concurrent_limit = invalid_value!(imported_optional_i32(
|
||||||
key.get("concurrent_limit"),
|
key.get("concurrent_limit"),
|
||||||
"concurrent_limit"
|
"concurrent_limit"
|
||||||
@@ -8364,7 +8365,7 @@ impl<'a> AdminAppState<'a> {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
if let Some(wallet_id) = created_wallet_id {
|
if let Some(wallet_id) = created_wallet_id {
|
||||||
if let Some(journal) = mutation_journal.as_deref_mut() {
|
if let Some(journal) = mutation_journal {
|
||||||
journal
|
journal
|
||||||
.user_wallet_snapshots
|
.user_wallet_snapshots
|
||||||
.insert((user_id.to_string(), wallet_id), synced);
|
.insert((user_id.to_string(), wallet_id), synced);
|
||||||
@@ -8433,7 +8434,7 @@ impl<'a> AdminAppState<'a> {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
if let Some(wallet_id) = created_wallet_id {
|
if let Some(wallet_id) = created_wallet_id {
|
||||||
if let Some(journal) = mutation_journal.as_deref_mut() {
|
if let Some(journal) = mutation_journal {
|
||||||
journal
|
journal
|
||||||
.api_key_wallet_snapshots
|
.api_key_wallet_snapshots
|
||||||
.insert((api_key_id.to_string(), wallet_id), synced);
|
.insert((api_key_id.to_string(), wallet_id), synced);
|
||||||
|
|||||||
@@ -349,6 +349,9 @@ fn load_and_sanitize_update_history(path: &Path) -> (Vec<UpdateHistoryEntry>, bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sanitize_update_history_entries(entries: &mut Vec<UpdateHistoryEntry>) -> bool {
|
fn sanitize_update_history_entries(entries: &mut Vec<UpdateHistoryEntry>) -> bool {
|
||||||
|
// Deliberately use a non-short-circuiting fold: every historical entry
|
||||||
|
// must be sanitized even after one entry changes.
|
||||||
|
#[allow(clippy::unnecessary_fold)]
|
||||||
let mut changed = entries.iter_mut().fold(false, |changed, entry| {
|
let mut changed = entries.iter_mut().fold(false, |changed, entry| {
|
||||||
sanitize_update_history_entry(entry) || changed
|
sanitize_update_history_entry(entry) || changed
|
||||||
});
|
});
|
||||||
@@ -455,7 +458,7 @@ fn read_update_metadata_file(path: &Path, max_bytes: usize) -> Result<Option<Vec
|
|||||||
if bytes.len() > max_bytes {
|
if bytes.len() > max_bytes {
|
||||||
return Err("更新元数据超过大小限制".to_string());
|
return Err("更新元数据超过大小限制".to_string());
|
||||||
}
|
}
|
||||||
return Ok(Some(bytes));
|
Ok(Some(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
@@ -536,7 +539,7 @@ fn write_update_metadata_atomic(path: &Path, bytes: &[u8]) -> Result<(), String>
|
|||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
let _ = unix_update_unlink_at(&parent, &temp_name);
|
let _ = unix_update_unlink_at(&parent, &temp_name);
|
||||||
}
|
}
|
||||||
return result;
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
@@ -711,7 +714,7 @@ fn remove_update_metadata_file(path: &Path) -> Result<(), String> {
|
|||||||
parent
|
parent
|
||||||
.sync_all()
|
.sync_all()
|
||||||
.map_err(|err| format!("同步更新元数据目录失败: {err}"))?;
|
.map_err(|err| format!("同步更新元数据目录失败: {err}"))?;
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
@@ -1867,7 +1870,7 @@ fn switch_current_symlink_at(base_dir: &Path, version: &str) -> Result<(), Strin
|
|||||||
parent
|
parent
|
||||||
.sync_all()
|
.sync_all()
|
||||||
.map_err(|err| format!("同步版本入口目录失败: {err}"))?;
|
.map_err(|err| format!("同步版本入口目录失败: {err}"))?;
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
|
|||||||
@@ -607,17 +607,15 @@ fn payment_order_payload(
|
|||||||
// A gateway response is a live checkout capability, not durable order
|
// A gateway response is a live checkout capability, not durable order
|
||||||
// history. Once the order is paid, terminal, or expired, suppress URLs,
|
// history. Once the order is paid, terminal, or expired, suppress URLs,
|
||||||
// form parameters, and provider metadata from the public payload.
|
// form parameters, and provider metadata from the public payload.
|
||||||
let gateway_response = record
|
let gateway_response = if record.status.eq_ignore_ascii_case("pending")
|
||||||
.status
|
&& record
|
||||||
.eq_ignore_ascii_case("pending")
|
.expires_at_unix_secs
|
||||||
.then(|| {
|
.is_some_and(|expires_at| expires_at > Utc::now().timestamp().max(0) as u64)
|
||||||
record
|
{
|
||||||
.expires_at_unix_secs
|
record.gateway_response.clone()
|
||||||
.is_some_and(|expires_at| expires_at > Utc::now().timestamp().max(0) as u64)
|
} else {
|
||||||
})
|
None
|
||||||
.unwrap_or(false)
|
};
|
||||||
.then(|| record.gateway_response.clone())
|
|
||||||
.flatten();
|
|
||||||
json!({
|
json!({
|
||||||
"id": record.id,
|
"id": record.id,
|
||||||
"order_no": record.order_no,
|
"order_no": record.order_no,
|
||||||
|
|||||||
@@ -383,8 +383,7 @@ fn forwarded_header_last(headers: &http::HeaderMap, name: &str) -> Option<String
|
|||||||
.filter_map(|value| value.to_str().ok())
|
.filter_map(|value| value.to_str().ok())
|
||||||
.flat_map(|value| value.split(','))
|
.flat_map(|value| value.split(','))
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|value| !value.is_empty())
|
.rfind(|value| !value.is_empty())
|
||||||
.last()
|
|
||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1054,11 +1054,14 @@ fn redirect_to(target: &str, params: Option<RedirectParams>) -> Response<Body> {
|
|||||||
fn build_redirect_location(target: &str, params: Option<RedirectParams>) -> String {
|
fn build_redirect_location(target: &str, params: Option<RedirectParams>) -> String {
|
||||||
let relative_target =
|
let relative_target =
|
||||||
url::Url::parse(target).is_err() && target.starts_with('/') && !target.starts_with("//");
|
url::Url::parse(target).is_err() && target.starts_with('/') && !target.starts_with("//");
|
||||||
let Ok(mut url) = url::Url::parse(target).or_else(|_| {
|
let parsed_target = url::Url::parse(target).or_else(|_| {
|
||||||
relative_target
|
if relative_target {
|
||||||
.then(|| url::Url::parse("http://aether.invalid").and_then(|base| base.join(target)))
|
url::Url::parse("http://aether.invalid").and_then(|base| base.join(target))
|
||||||
.unwrap_or_else(|| Err(url::ParseError::RelativeUrlWithoutBase))
|
} else {
|
||||||
}) else {
|
Err(url::ParseError::RelativeUrlWithoutBase)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let Ok(mut url) = parsed_target else {
|
||||||
return target.to_string();
|
return target.to_string();
|
||||||
};
|
};
|
||||||
match params {
|
match params {
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ fn validate_gateway_data_encryption_key(value: Option<&str>) -> Result<(), &'sta
|
|||||||
let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else {
|
let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
if value.as_bytes().len() < MIN_GATEWAY_DATA_ENCRYPTION_KEY_BYTES {
|
if value.len() < MIN_GATEWAY_DATA_ENCRYPTION_KEY_BYTES {
|
||||||
return Err("gateway data encryption key must contain at least 32 bytes");
|
return Err("gateway data encryption key must contain at least 32 bytes");
|
||||||
}
|
}
|
||||||
if INSECURE_GATEWAY_DATA_ENCRYPTION_KEYS.contains(&value) {
|
if INSECURE_GATEWAY_DATA_ENCRYPTION_KEYS.contains(&value) {
|
||||||
@@ -2984,7 +2984,7 @@ fn read_data_import_input_with_limit(path: &Path, limit: usize) -> io::Result<St
|
|||||||
// A file can grow after metadata() returns. Reading one extra byte catches
|
// A file can grow after metadata() returns. Reading one extra byte catches
|
||||||
// that race without allowing the input buffer to exceed the configured
|
// that race without allowing the input buffer to exceed the configured
|
||||||
// parser budget.
|
// parser budget.
|
||||||
let read_limit = limit.checked_add(1).unwrap_or(usize::MAX);
|
let read_limit = limit.saturating_add(1);
|
||||||
// Do not reserve the whole metadata length: sparse or concurrently grown
|
// Do not reserve the whole metadata length: sparse or concurrently grown
|
||||||
// files can advertise a huge size while containing little data, and a
|
// files can advertise a huge size while containing little data, and a
|
||||||
// single capacity reservation would otherwise become a local DoS vector.
|
// single capacity reservation would otherwise become a local DoS vector.
|
||||||
|
|||||||
@@ -258,7 +258,7 @@ pub(crate) async fn resolve_identity_oauth_login_user(
|
|||||||
.initialize_auth_user_wallet_with_outcome(&user.id, initial_gift, false)
|
.initialize_auth_user_wallet_with_outcome(&user.id, initial_gift, false)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Some(outcome)) => outcome.created.then(|| outcome.wallet.id),
|
Ok(Some(outcome)) => outcome.created.then_some(outcome.wallet.id),
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
let _ = state
|
let _ = state
|
||||||
.rollback_provisional_auth_user_with_wallet(&user.id, None)
|
.rollback_provisional_auth_user_with_wallet(&user.id, None)
|
||||||
|
|||||||
@@ -1538,7 +1538,7 @@ impl AppState {
|
|||||||
body_excerpt,
|
body_excerpt,
|
||||||
..
|
..
|
||||||
}) if matches!(status_code, 400 | 401 | 403) => {
|
}) if matches!(status_code, 400 | 401 | 403) => {
|
||||||
if let Err(_) = self
|
if self
|
||||||
.persist_local_oauth_refresh_failure_state(
|
.persist_local_oauth_refresh_failure_state(
|
||||||
¤t_transport,
|
¤t_transport,
|
||||||
status_code,
|
status_code,
|
||||||
@@ -1546,6 +1546,7 @@ impl AppState {
|
|||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.is_err()
|
||||||
{
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
key_id = %current_transport.key.id,
|
key_id = %current_transport.key.id,
|
||||||
@@ -1596,13 +1597,14 @@ impl AppState {
|
|||||||
.await;
|
.await;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
if let Err(_) = self
|
if self
|
||||||
.persist_local_oauth_refresh_entry(
|
.persist_local_oauth_refresh_entry(
|
||||||
¤t_transport,
|
¤t_transport,
|
||||||
&refreshed_entry,
|
&refreshed_entry,
|
||||||
expected_credential_fence.as_ref(),
|
expected_credential_fence.as_ref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.is_err()
|
||||||
{
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
key_id = %current_transport.key.id,
|
key_id = %current_transport.key.id,
|
||||||
@@ -1792,13 +1794,14 @@ impl AppState {
|
|||||||
.await;
|
.await;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
if let Err(_) = self
|
if self
|
||||||
.persist_local_oauth_refresh_entry(
|
.persist_local_oauth_refresh_entry(
|
||||||
¤t_transport,
|
¤t_transport,
|
||||||
&refreshed_entry,
|
&refreshed_entry,
|
||||||
expected_credential_fence.as_ref(),
|
expected_credential_fence.as_ref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.is_err()
|
||||||
{
|
{
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
key_id = %current_transport.key.id,
|
key_id = %current_transport.key.id,
|
||||||
@@ -1854,7 +1857,7 @@ impl AppState {
|
|||||||
let Some(lease) = lease else {
|
let Some(lease) = lease else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Err(_) = self.runtime_state.lock_release(&lease).await {
|
if self.runtime_state.lock_release(&lease).await.is_err() {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
key_id = %lease.key,
|
key_id = %lease.key,
|
||||||
"gateway local oauth refresh distributed lease release failed"
|
"gateway local oauth refresh distributed lease release failed"
|
||||||
|
|||||||
@@ -79,9 +79,11 @@ fn config_f64(value: Option<&serde_json::Value>, default: f64) -> f64 {
|
|||||||
|
|
||||||
fn config_percent(value: Option<&serde_json::Value>) -> f64 {
|
fn config_percent(value: Option<&serde_json::Value>) -> f64 {
|
||||||
let value = config_f64(value, 0.0);
|
let value = config_f64(value, 0.0);
|
||||||
(value.is_finite() && value > 0.0 && value <= 100.0)
|
if value.is_finite() && value > 0.0 && value <= 100.0 {
|
||||||
.then_some(value)
|
value
|
||||||
.unwrap_or(0.0)
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ fn canonicalize_internal_report_json(value: &Value) -> Value {
|
|||||||
),
|
),
|
||||||
Value::Object(object) => {
|
Value::Object(object) => {
|
||||||
let mut entries = object.iter().collect::<Vec<_>>();
|
let mut entries = object.iter().collect::<Vec<_>>();
|
||||||
entries.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
|
entries.sort_unstable_by_key(|(left, _)| *left);
|
||||||
Value::Object(Map::from_iter(entries.into_iter().map(|(key, value)| {
|
Value::Object(Map::from_iter(entries.into_iter().map(|(key, value)| {
|
||||||
(key.clone(), canonicalize_internal_report_json(value))
|
(key.clone(), canonicalize_internal_report_json(value))
|
||||||
})))
|
})))
|
||||||
|
|||||||
@@ -781,7 +781,7 @@ fn write_service_definition(path: &str, content: &str, mode: u32) -> anyhow::Res
|
|||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
let _ = std::fs::remove_file(&temporary);
|
let _ = std::fs::remove_file(&temporary);
|
||||||
}
|
}
|
||||||
return result;
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
@@ -831,7 +831,7 @@ fn validate_private_service_directory(path: &Path) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
ancestor = directory.parent();
|
ancestor = directory.parent();
|
||||||
}
|
}
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
@@ -864,7 +864,7 @@ fn validate_replaceable_service_file(path: &Path) -> anyhow::Result<()> {
|
|||||||
Err(error) if error.kind() == ErrorKind::NotFound => {}
|
Err(error) if error.kind() == ErrorKind::NotFound => {}
|
||||||
Err(error) => return Err(error.into()),
|
Err(error) => return Err(error.into()),
|
||||||
}
|
}
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
@@ -910,7 +910,7 @@ fn ensure_private_service_directory(path: &Path, mode: u32) -> anyhow::Result<()
|
|||||||
}
|
}
|
||||||
directory.set_permissions(std::fs::Permissions::from_mode(mode))?;
|
directory.set_permissions(std::fs::Permissions::from_mode(mode))?;
|
||||||
directory.sync_all()?;
|
directory.sync_all()?;
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
@@ -949,7 +949,7 @@ fn open_private_service_log(path: &Path, mode: u32) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
file.set_permissions(std::fs::Permissions::from_mode(mode))?;
|
file.set_permissions(std::fs::Permissions::from_mode(mode))?;
|
||||||
file.sync_all()?;
|
file.sync_all()?;
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
|
|||||||
@@ -739,7 +739,7 @@ fn atomic_replace_paths(current_exe: &Path, new_binary: &Path) -> anyhow::Result
|
|||||||
}
|
}
|
||||||
|
|
||||||
eprintln!(" Binary replaced: {}", current_exe.display());
|
eprintln!(" Binary replaced: {}", current_exe.display());
|
||||||
return Ok(backup_path);
|
Ok(backup_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
@@ -792,7 +792,7 @@ fn restore_tunnel_backup_paths(current_exe: &Path, backup_path: &Path) -> anyhow
|
|||||||
error
|
error
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return Ok(());
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
|
|||||||
@@ -563,6 +563,10 @@ fn insert_ascii_header(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The handshake transcript has a fixed set of wire fields. Keep the explicit
|
||||||
|
// arguments and ordering so the client remains interoperable with existing
|
||||||
|
// tunnel servers.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn insert_tunnel_security_handshake_headers(
|
fn insert_tunnel_security_handshake_headers(
|
||||||
headers: &mut http::HeaderMap,
|
headers: &mut http::HeaderMap,
|
||||||
key: &str,
|
key: &str,
|
||||||
|
|||||||
@@ -1617,7 +1617,7 @@ where
|
|||||||
redirect_count,
|
redirect_count,
|
||||||
request_body_size.load(Ordering::Relaxed),
|
request_body_size.load(Ordering::Relaxed),
|
||||||
),
|
),
|
||||||
&error_message,
|
error_message,
|
||||||
total_elapsed,
|
total_elapsed,
|
||||||
);
|
);
|
||||||
send_error(frame_tx, stream_id, error_message).await;
|
send_error(frame_tx, stream_id, error_message).await;
|
||||||
@@ -2091,10 +2091,10 @@ async fn handle_stream_inner(
|
|||||||
redirects_followed,
|
redirects_followed,
|
||||||
request_body_size.load(Ordering::Relaxed),
|
request_body_size.load(Ordering::Relaxed),
|
||||||
),
|
),
|
||||||
&error_message,
|
error_message,
|
||||||
overall_start.elapsed(),
|
overall_start.elapsed(),
|
||||||
);
|
);
|
||||||
send_error(frame_tx, stream_id, &error_message).await;
|
send_error(frame_tx, stream_id, error_message).await;
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3913,9 +3913,11 @@ fn chatgpt_web_blocked_features(value: &serde_json::Value) -> Vec<String> {
|
|||||||
.flatten()
|
.flatten()
|
||||||
.filter_map(serde_json::Value::as_str)
|
.filter_map(serde_json::Value::as_str)
|
||||||
.any(chatgpt_web_is_image_quota_feature);
|
.any(chatgpt_web_is_image_quota_feature);
|
||||||
image_blocked
|
if image_blocked {
|
||||||
.then(|| vec!["image_generation".to_string()])
|
vec!["image_generation".to_string()]
|
||||||
.unwrap_or_default()
|
} else {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_chatgpt_web_conversation_init_response(
|
pub fn parse_chatgpt_web_conversation_init_response(
|
||||||
|
|||||||
@@ -468,7 +468,7 @@ fn normalized_proxy_url_identity(value: &str) -> Option<(String, String, u16)> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let host = parsed.host_str()?.to_ascii_lowercase();
|
let host = parsed.host_str()?.to_ascii_lowercase();
|
||||||
let port = parsed.port().or_else(|| match scheme.as_str() {
|
let port = parsed.port().or(match scheme.as_str() {
|
||||||
"http" => Some(80),
|
"http" => Some(80),
|
||||||
"https" => Some(443),
|
"https" => Some(443),
|
||||||
"socks5" | "socks5h" => Some(1080),
|
"socks5" | "socks5h" => Some(1080),
|
||||||
@@ -629,9 +629,7 @@ fn project_chatgpt_web_metadata(value: &Value) -> Value {
|
|||||||
// This is an opaque idempotency key, not a diagnostic or credential. Keep it
|
// This is an opaque idempotency key, not a diagnostic or credential. Keep it
|
||||||
// bounded and token-safe so quota request de-duplication survives persistence.
|
// bounded and token-safe so quota request de-duplication survives persistence.
|
||||||
copy_safe_token_string_or_null(source, &mut projected, "image_quota_last_local_request_key");
|
copy_safe_token_string_or_null(source, &mut projected, "image_quota_last_local_request_key");
|
||||||
for field in ["image_quota_blocked"] {
|
copy_json_bool_or_null(source, &mut projected, "image_quota_blocked");
|
||||||
copy_json_bool_or_null(source, &mut projected, field);
|
|
||||||
}
|
|
||||||
for field in [
|
for field in [
|
||||||
"default_model_slug",
|
"default_model_slug",
|
||||||
"plan_type",
|
"plan_type",
|
||||||
@@ -1757,9 +1755,7 @@ fn trimmed_string_field(object: &Map<String, Value>, key: &str) -> Option<String
|
|||||||
fn sanitize_network_url(value: &str) -> Option<String> {
|
fn sanitize_network_url(value: &str) -> Option<String> {
|
||||||
let value = value.trim();
|
let value = value.trim();
|
||||||
let mut parsed = url::Url::parse(value).ok()?;
|
let mut parsed = url::Url::parse(value).ok()?;
|
||||||
if parsed.host_str().is_none() {
|
parsed.host_str()?;
|
||||||
return None;
|
|
||||||
}
|
|
||||||
parsed.set_username("").ok()?;
|
parsed.set_username("").ok()?;
|
||||||
parsed.set_password(None).ok()?;
|
parsed.set_password(None).ok()?;
|
||||||
parsed.set_query(None);
|
parsed.set_query(None);
|
||||||
@@ -1916,14 +1912,13 @@ fn body_path_key_segments(path: &str) -> Vec<String> {
|
|||||||
.trim()
|
.trim()
|
||||||
.trim_matches(['\'', '"'])
|
.trim_matches(['\'', '"'])
|
||||||
.to_string();
|
.to_string();
|
||||||
if !inner.is_empty()
|
if (!inner.is_empty()
|
||||||
&& inner != "*"
|
&& inner != "*"
|
||||||
&& inner.parse::<isize>().is_err()
|
&& inner.parse::<isize>().is_err()
|
||||||
&& !inner.contains('-')
|
&& !inner.contains('-'))
|
||||||
|
|| inner.contains(|character: char| character.is_ascii_alphabetic())
|
||||||
{
|
{
|
||||||
segments.push(inner);
|
segments.push(inner);
|
||||||
} else if inner.contains(|character: char| character.is_ascii_alphabetic()) {
|
|
||||||
segments.push(inner);
|
|
||||||
}
|
}
|
||||||
index = close + 1;
|
index = close + 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3358,9 +3358,7 @@ pub fn build_admin_proxy_node_payload(node: &StoredProxyNode) -> serde_json::Val
|
|||||||
fn redact_admin_proxy_node_metadata(
|
fn redact_admin_proxy_node_metadata(
|
||||||
metadata: Option<&serde_json::Value>,
|
metadata: Option<&serde_json::Value>,
|
||||||
) -> Option<serde_json::Value> {
|
) -> Option<serde_json::Value> {
|
||||||
let Some(metadata) = metadata else {
|
let metadata = metadata?;
|
||||||
return None;
|
|
||||||
};
|
|
||||||
let mut metadata = metadata.clone();
|
let mut metadata = metadata.clone();
|
||||||
if let Some(tunnel_security) = metadata
|
if let Some(tunnel_security) = metadata
|
||||||
.as_object_mut()
|
.as_object_mut()
|
||||||
|
|||||||
@@ -1421,9 +1421,11 @@ WHERE id = ?
|
|||||||
&& current
|
&& current
|
||||||
.tunnel_connected_at_unix_secs
|
.tunnel_connected_at_unix_secs
|
||||||
.is_some_and(|last_transition| event_time < last_transition);
|
.is_some_and(|last_transition| event_time < last_transition);
|
||||||
let persisted_detail = stale
|
let persisted_detail = if stale {
|
||||||
.then(|| format!("[stale_ignored] {event_detail}"))
|
format!("[stale_ignored] {event_detail}")
|
||||||
.unwrap_or(event_detail);
|
} else {
|
||||||
|
event_detail
|
||||||
|
};
|
||||||
self.insert_event(
|
self.insert_event(
|
||||||
&mutation.node_id,
|
&mutation.node_id,
|
||||||
Some(node.tunnel_generation.as_str()),
|
Some(node.tunnel_generation.as_str()),
|
||||||
@@ -1744,23 +1746,23 @@ fn proxy_node_registration_matches(
|
|||||||
&& current.heartbeat_interval == mutation.heartbeat_interval
|
&& current.heartbeat_interval == mutation.heartbeat_interval
|
||||||
&& mutation
|
&& mutation
|
||||||
.active_connections
|
.active_connections
|
||||||
.map_or(true, |value| current.active_connections == value)
|
.is_none_or(|value| current.active_connections == value)
|
||||||
&& mutation
|
&& mutation
|
||||||
.total_requests
|
.total_requests
|
||||||
.map_or(true, |value| current.total_requests == value)
|
.is_none_or(|value| current.total_requests == value)
|
||||||
&& mutation
|
&& mutation
|
||||||
.avg_latency_ms
|
.avg_latency_ms
|
||||||
.map_or(true, |value| current.avg_latency_ms == Some(value))
|
.is_none_or(|value| current.avg_latency_ms == Some(value))
|
||||||
&& mutation
|
&& mutation
|
||||||
.hardware_info
|
.hardware_info
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(true, |value| current.hardware_info.as_ref() == Some(value))
|
.is_none_or(|value| current.hardware_info.as_ref() == Some(value))
|
||||||
&& mutation.estimated_max_concurrency.map_or(true, |value| {
|
&& mutation
|
||||||
current.estimated_max_concurrency == Some(value)
|
.estimated_max_concurrency
|
||||||
})
|
.is_none_or(|value| current.estimated_max_concurrency == Some(value))
|
||||||
&& current.tunnel_mode == mutation.tunnel_mode
|
&& current.tunnel_mode == mutation.tunnel_mode
|
||||||
&& replacement_proxy_metadata
|
&& replacement_proxy_metadata
|
||||||
.map_or(true, |value| current.proxy_metadata.as_ref() == Some(value))
|
.is_none_or(|value| current.proxy_metadata.as_ref() == Some(value))
|
||||||
&& current.updated_at_unix_secs == Some(now)
|
&& current.updated_at_unix_secs == Some(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1887,9 +1887,9 @@ ON DUPLICATE KEY UPDATE id = id
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
let now = current_unix_secs_i64();
|
let now = current_unix_secs_i64();
|
||||||
if !current
|
if current
|
||||||
.expires_at_unix_secs
|
.expires_at_unix_secs
|
||||||
.is_some_and(|expires_at| expires_at > now.max(0) as u64)
|
.is_none_or(|expires_at| expires_at <= now.max(0) as u64)
|
||||||
{
|
{
|
||||||
tx.commit().await.map_sql_err()?;
|
tx.commit().await.map_sql_err()?;
|
||||||
return Ok(WalletMutationOutcome::Invalid(
|
return Ok(WalletMutationOutcome::Invalid(
|
||||||
|
|||||||
@@ -1638,9 +1638,11 @@ WHERE id = $1 AND proxy_metadata::jsonb = $3::jsonb
|
|||||||
.await
|
.await
|
||||||
.map_postgres_err()?;
|
.map_postgres_err()?;
|
||||||
let stale = result.rows_affected() == 0;
|
let stale = result.rows_affected() == 0;
|
||||||
let persisted_detail = stale
|
let persisted_detail = if stale {
|
||||||
.then(|| format!("[stale_ignored] {event_detail}"))
|
format!("[stale_ignored] {event_detail}")
|
||||||
.unwrap_or(event_detail);
|
} else {
|
||||||
|
event_detail
|
||||||
|
};
|
||||||
|
|
||||||
sqlx::query(INSERT_PROXY_NODE_EVENT_SQL)
|
sqlx::query(INSERT_PROXY_NODE_EVENT_SQL)
|
||||||
.bind(&mutation.node_id)
|
.bind(&mutation.node_id)
|
||||||
|
|||||||
@@ -2049,6 +2049,10 @@ WHERE id = $1
|
|||||||
self.find_user_auth_by_id(user_id).await
|
self.find_user_auth_by_id(user_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This operation compares and restores four correlated snapshots in one
|
||||||
|
// transaction. Keep the explicit arguments visible at the call site so a
|
||||||
|
// future restore cannot accidentally omit one consistency boundary.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn restore_local_auth_user_state_if_matches(
|
pub async fn restore_local_auth_user_state_if_matches(
|
||||||
&self,
|
&self,
|
||||||
expected_auth: &StoredUserAuthRecord,
|
expected_auth: &StoredUserAuthRecord,
|
||||||
|
|||||||
@@ -1401,9 +1401,11 @@ WHERE id = ?
|
|||||||
&& current
|
&& current
|
||||||
.tunnel_connected_at_unix_secs
|
.tunnel_connected_at_unix_secs
|
||||||
.is_some_and(|last_transition| event_time < last_transition);
|
.is_some_and(|last_transition| event_time < last_transition);
|
||||||
let persisted_detail = stale
|
let persisted_detail = if stale {
|
||||||
.then(|| format!("[stale_ignored] {event_detail}"))
|
format!("[stale_ignored] {event_detail}")
|
||||||
.unwrap_or(event_detail);
|
} else {
|
||||||
|
event_detail
|
||||||
|
};
|
||||||
self.insert_event(
|
self.insert_event(
|
||||||
&mutation.node_id,
|
&mutation.node_id,
|
||||||
Some(node.tunnel_generation.as_str()),
|
Some(node.tunnel_generation.as_str()),
|
||||||
|
|||||||
@@ -1753,9 +1753,9 @@ ON CONFLICT DO NOTHING
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
let now = current_unix_secs_i64();
|
let now = current_unix_secs_i64();
|
||||||
if !current
|
if current
|
||||||
.expires_at_unix_secs
|
.expires_at_unix_secs
|
||||||
.is_some_and(|expires_at| expires_at > now.max(0) as u64)
|
.is_none_or(|expires_at| expires_at <= now.max(0) as u64)
|
||||||
{
|
{
|
||||||
tx.commit().await.map_sql_err()?;
|
tx.commit().await.map_sql_err()?;
|
||||||
return Ok(WalletMutationOutcome::Invalid(
|
return Ok(WalletMutationOutcome::Invalid(
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ impl std::fmt::Debug for LdapBindPasswordUpdate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The successful branch intentionally returns the complete persisted
|
||||||
|
// configuration so callers can continue with the exact CAS snapshot. Boxing
|
||||||
|
// it would change this public repository contract and add needless allocation
|
||||||
|
// on the normal (successful) path.
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum CompareAndSwapLdapConfigResult {
|
pub enum CompareAndSwapLdapConfigResult {
|
||||||
Applied(StoredLdapModuleConfig),
|
Applied(StoredLdapModuleConfig),
|
||||||
|
|||||||
@@ -455,6 +455,10 @@ impl std::fmt::Debug for UpsertOAuthProviderConfigRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep the returned provider value inline: this outcome is part of the
|
||||||
|
// repository API and the successful value is consumed immediately by callers.
|
||||||
|
// Boxing would be an API/ownership change for no security benefit.
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum UpsertOAuthProviderConfigOutcome {
|
pub enum UpsertOAuthProviderConfigOutcome {
|
||||||
Upserted(StoredOAuthProviderConfig),
|
Upserted(StoredOAuthProviderConfig),
|
||||||
@@ -513,6 +517,10 @@ impl UpsertOAuthProviderConfigRecord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validation tests are kept beside the validation implementation so changes
|
||||||
|
// to endpoint policy are reviewed together. The repository traits below are
|
||||||
|
// intentionally declared after this focused test module for API readability.
|
||||||
|
#[allow(clippy::items_after_test_module)]
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
|
|||||||
@@ -311,6 +311,10 @@ impl BindUserOAuthLinkSessionExpectation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returning the full linked-user record avoids a second repository lookup and
|
||||||
|
// is the established public contract. Keep the success value inline rather
|
||||||
|
// than changing every adapter/caller to an allocated box.
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum ResolveOAuthLinkedUserOutcome {
|
pub enum ResolveOAuthLinkedUserOutcome {
|
||||||
Linked(StoredUserAuthRecord),
|
Linked(StoredUserAuthRecord),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const REFERRAL_RECONCILIATION_LIMIT: usize = 200;
|
|||||||
// The list tests intentionally build one page larger than the historical
|
// The list tests intentionally build one page larger than the historical
|
||||||
// in-memory fetch cap. Keep the fixture cap test-only now that production
|
// in-memory fetch cap. Keep the fixture cap test-only now that production
|
||||||
// queries paginate directly in SQL.
|
// queries paginate directly in SQL.
|
||||||
#[cfg(test)]
|
#[cfg(all(test, feature = "sqlite"))]
|
||||||
const REFERRAL_FETCH_LIMIT: usize = 5_000;
|
const REFERRAL_FETCH_LIMIT: usize = 5_000;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
@@ -367,6 +367,10 @@ fn referral_amounts_match(left: f64, right: f64) -> bool {
|
|||||||
/// `applying` reward into `applied` without ever increasing the inviter's gift
|
/// `applying` reward into `applied` without ever increasing the inviter's gift
|
||||||
/// balance. The normal credit path writes a complete before/after snapshot,
|
/// balance. The normal credit path writes a complete before/after snapshot,
|
||||||
/// so recovery can require those same invariants before trusting the fact.
|
/// so recovery can require those same invariants before trusting the fact.
|
||||||
|
// The fact validator compares the complete before/after ledger snapshot. Keep
|
||||||
|
// each value explicit so a caller cannot accidentally substitute a bucket or
|
||||||
|
// omit one of the persisted invariants.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn referral_credit_transaction_fact_valid(
|
fn referral_credit_transaction_fact_valid(
|
||||||
reward_amount_usd: f64,
|
reward_amount_usd: f64,
|
||||||
amount: f64,
|
amount: f64,
|
||||||
|
|||||||
@@ -643,22 +643,22 @@ fn deactivate_imported_credentials(
|
|||||||
.trim_matches(|ch| matches!(ch, '"' | '`'));
|
.trim_matches(|ch| matches!(ch, '"' | '`'));
|
||||||
|
|
||||||
match table_name {
|
match table_name {
|
||||||
"users" => {
|
"users"
|
||||||
if object
|
if object
|
||||||
.get("password_hash")
|
.get("password_hash")
|
||||||
.is_some_and(|value| !value.is_null())
|
.is_some_and(|value| !value.is_null()) =>
|
||||||
{
|
{
|
||||||
set_supported_import_value(
|
set_supported_import_value(
|
||||||
object,
|
object,
|
||||||
&target_has_column,
|
&target_has_column,
|
||||||
"password_hash",
|
"password_hash",
|
||||||
Value::String(format!(
|
Value::String(format!(
|
||||||
"$aether-import-revoked${}",
|
"$aether-import-revoked${}",
|
||||||
imported_credential_tombstone()
|
imported_credential_tombstone()
|
||||||
)),
|
)),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
"users" => {}
|
||||||
"api_keys" => {
|
"api_keys" => {
|
||||||
if object.contains_key("key_hash") {
|
if object.contains_key("key_hash") {
|
||||||
set_supported_import_value(
|
set_supported_import_value(
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ async fn enforce_mysql_identity_import_invariants(
|
|||||||
for user_id in &scope.user_ids {
|
for user_id in &scope.user_ids {
|
||||||
let auth_source =
|
let auth_source =
|
||||||
sqlx::query_scalar::<_, String>("SELECT auth_source FROM users WHERE id = ? LIMIT 1")
|
sqlx::query_scalar::<_, String>("SELECT auth_source FROM users WHERE id = ? LIMIT 1")
|
||||||
.bind(&user_id)
|
.bind(user_id)
|
||||||
.fetch_optional(&mut **tx)
|
.fetch_optional(&mut **tx)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?
|
.map_sql_err()?
|
||||||
@@ -169,7 +169,7 @@ async fn enforce_mysql_identity_import_invariants(
|
|||||||
}
|
}
|
||||||
if auth_source == "oauth" {
|
if auth_source == "oauth" {
|
||||||
sqlx::query("UPDATE users SET email_verified = 0 WHERE id = ?")
|
sqlx::query("UPDATE users SET email_verified = 0 WHERE id = ?")
|
||||||
.bind(&user_id)
|
.bind(user_id)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ async fn enforce_postgres_identity_import_invariants(
|
|||||||
let auth_source = sqlx::query_scalar::<_, String>(
|
let auth_source = sqlx::query_scalar::<_, String>(
|
||||||
"SELECT auth_source::text FROM public.users WHERE id = $1 LIMIT 1",
|
"SELECT auth_source::text FROM public.users WHERE id = $1 LIMIT 1",
|
||||||
)
|
)
|
||||||
.bind(&user_id)
|
.bind(user_id)
|
||||||
.fetch_optional(&mut **tx)
|
.fetch_optional(&mut **tx)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?
|
.map_sql_err()?
|
||||||
@@ -182,7 +182,7 @@ async fn enforce_postgres_identity_import_invariants(
|
|||||||
}
|
}
|
||||||
if auth_source == "oauth" {
|
if auth_source == "oauth" {
|
||||||
sqlx::query("UPDATE public.users SET email_verified = FALSE WHERE id = $1")
|
sqlx::query("UPDATE public.users SET email_verified = FALSE WHERE id = $1")
|
||||||
.bind(&user_id)
|
.bind(user_id)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ async fn enforce_sqlite_identity_import_invariants(
|
|||||||
for user_id in &scope.user_ids {
|
for user_id in &scope.user_ids {
|
||||||
let auth_source =
|
let auth_source =
|
||||||
sqlx::query_scalar::<_, String>("SELECT auth_source FROM users WHERE id = ? LIMIT 1")
|
sqlx::query_scalar::<_, String>("SELECT auth_source FROM users WHERE id = ? LIMIT 1")
|
||||||
.bind(&user_id)
|
.bind(user_id)
|
||||||
.fetch_optional(&mut **tx)
|
.fetch_optional(&mut **tx)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?
|
.map_sql_err()?
|
||||||
@@ -163,7 +163,7 @@ async fn enforce_sqlite_identity_import_invariants(
|
|||||||
}
|
}
|
||||||
if auth_source == "oauth" {
|
if auth_source == "oauth" {
|
||||||
sqlx::query("UPDATE users SET email_verified = 0 WHERE id = ?")
|
sqlx::query("UPDATE users SET email_verified = 0 WHERE id = ?")
|
||||||
.bind(&user_id)
|
.bind(user_id)
|
||||||
.execute(&mut **tx)
|
.execute(&mut **tx)
|
||||||
.await
|
.await
|
||||||
.map_sql_err()?;
|
.map_sql_err()?;
|
||||||
|
|||||||
@@ -83,6 +83,10 @@ impl From<&StoredUserAuthRecord> for MemoryAuthApiKeyOwnerSnapshot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The trusted snapshot is returned on the common path so callers can use the
|
||||||
|
// complete immutable view without another lookup. Preserve this established
|
||||||
|
// in-memory registry representation rather than introducing heap allocation.
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum MemoryAuthApiKeyOwnerRegistryEntry {
|
enum MemoryAuthApiKeyOwnerRegistryEntry {
|
||||||
Trusted(MemoryAuthApiKeyOwnerSnapshot),
|
Trusted(MemoryAuthApiKeyOwnerSnapshot),
|
||||||
|
|||||||
@@ -1825,9 +1825,9 @@ impl WalletWriteRepository for InMemoryWalletRepository {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
let now_secs = current_unix_secs();
|
let now_secs = current_unix_secs();
|
||||||
if !current_order
|
if current_order
|
||||||
.expires_at_unix_secs
|
.expires_at_unix_secs
|
||||||
.is_some_and(|expires_at| expires_at > now_secs)
|
.is_none_or(|expires_at| expires_at <= now_secs)
|
||||||
{
|
{
|
||||||
return Ok(WalletMutationOutcome::Invalid(
|
return Ok(WalletMutationOutcome::Invalid(
|
||||||
"wallet recharge order is expired".to_string(),
|
"wallet recharge order is expired".to_string(),
|
||||||
|
|||||||
@@ -2420,6 +2420,7 @@ mod tests {
|
|||||||
assert!(same_subject.try_acquire().await.is_ok());
|
assert!(same_subject.try_acquire().await.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
async fn memory_keyed_semaphores_isolate_resource_capacity() {
|
async fn memory_keyed_semaphores_isolate_resource_capacity() {
|
||||||
let runtime = RuntimeState::memory(MemoryRuntimeStateConfig::default());
|
let runtime = RuntimeState::memory(MemoryRuntimeStateConfig::default());
|
||||||
let first = runtime
|
let first = runtime
|
||||||
|
|||||||
@@ -790,6 +790,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn assert_sensitive_bodies_disabled(
|
fn assert_sensitive_bodies_disabled(
|
||||||
request_body: &Option<Value>,
|
request_body: &Option<Value>,
|
||||||
request_body_ref: &Option<String>,
|
request_body_ref: &Option<String>,
|
||||||
|
|||||||
@@ -1412,8 +1412,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn bounded_report_body_decode_rejects_oversized_encoded_values_before_allocation() {
|
fn bounded_report_body_decode_rejects_oversized_encoded_values_before_allocation() {
|
||||||
let encoded =
|
let encoded = "A".repeat(
|
||||||
"A".repeat(((super::MAX_INTERNAL_REPORT_BODY_BYTES + 2) / 3 * 4).saturating_add(4));
|
super::MAX_INTERNAL_REPORT_BODY_BYTES
|
||||||
|
.div_ceil(3)
|
||||||
|
.saturating_mul(4)
|
||||||
|
.saturating_add(4),
|
||||||
|
);
|
||||||
let error = super::decode_internal_report_body_base64(&encoded)
|
let error = super::decode_internal_report_body_base64(&encoded)
|
||||||
.expect_err("oversized report body must be rejected before decoding");
|
.expect_err("oversized report body must be rejected before decoding");
|
||||||
assert!(error.contains("internal report body exceeds"));
|
assert!(error.contains("internal report body exceeds"));
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ impl FileVideoTaskStore {
|
|||||||
.strip_prefix(VIDEO_TASK_STORE_PURPOSE)
|
.strip_prefix(VIDEO_TASK_STORE_PURPOSE)
|
||||||
.and_then(|value| value.strip_prefix('\0'))
|
.and_then(|value| value.strip_prefix('\0'))
|
||||||
.ok_or_else(|| invalid_store_data("video task store purpose mismatch"))?;
|
.ok_or_else(|| invalid_store_data("video task store purpose mismatch"))?;
|
||||||
let mut registry: VideoTaskRegistry = serde_json::from_str(&plaintext)
|
let mut registry: VideoTaskRegistry = serde_json::from_str(plaintext)
|
||||||
.map_err(|_| invalid_store_data("decrypted video task store is invalid"))?;
|
.map_err(|_| invalid_store_data("decrypted video task store is invalid"))?;
|
||||||
let needs_rewrite = registry.sanitize_persisted_diagnostics();
|
let needs_rewrite = registry.sanitize_persisted_diagnostics();
|
||||||
return Ok(LoadedVideoTaskRegistry {
|
return Ok(LoadedVideoTaskRegistry {
|
||||||
|
|||||||
Reference in New Issue
Block a user