修复应用状态调试输出密钥泄漏 (#150)
修复 Issue #148。 - AppConfig、AppState、AppStateInner 改为封闭的手写 Debug 安全摘要 - SpacetimeClientConfig 隐藏 token、URL 与数据库自由字符串,SpacetimeClient 复用安全摘要 - 唯一哨兵覆盖五条独立 Debug 路径,并精确锁定 AppConfig 允许输出字段 - 补充运行时日志规约与项目排障记忆 验证: - cargo test -p api-server debug_summaries_redact_all_runtime_credentials - cargo check -p spacetime-client -p api-server --all-targets - cargo fmt --all --check - npm run check:encoding - git diff --check 剩余边界:报告第二档中的历史 provider 配置类型独立 Debug 脱敏另行处理,本 PR 聚焦第一档主泄漏链。 Closes #148 Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/150
This commit was merged in pull request #150.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use std::{env, fs, net::SocketAddr, path::PathBuf, time::Duration};
|
||||
use std::{env, fmt, fs, net::SocketAddr, path::PathBuf, time::Duration};
|
||||
|
||||
use platform_llm::{
|
||||
DEFAULT_ARK_BASE_URL, DEFAULT_MAX_RETRIES, DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
@@ -27,7 +27,7 @@ const DEFAULT_ALIYUN_MATTING_ENDPOINT: &str = "imageseg.cn-shanghai.aliyuncs.com
|
||||
const DEFAULT_ALIYUN_MATTING_REQUEST_TIMEOUT_MS: u64 = 30_000;
|
||||
|
||||
// 集中管理 api-server 的启动配置,避免入口层直接散落环境变量解析逻辑。
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct AppConfig {
|
||||
pub bind_host: String,
|
||||
pub bind_port: u16,
|
||||
@@ -212,6 +212,41 @@ pub struct AppConfig {
|
||||
pub slow_request_threshold_ms: u64,
|
||||
}
|
||||
|
||||
impl fmt::Debug for AppConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// 配置由环境变量和私密文件聚合而来。这里只输出排障所需的封闭运行摘要,
|
||||
// 不递归格式化任一字符串配置,避免 URL 凭据、Token、私钥或未来新增字段进入日志。
|
||||
f.debug_struct("AppConfig")
|
||||
.field("bind_port", &self.bind_port)
|
||||
.field("listen_backlog", &self.listen_backlog)
|
||||
.field("worker_threads", &self.worker_threads)
|
||||
.field("process_role", &self.process_role)
|
||||
.field("external_generation_mode", &self.external_generation_mode)
|
||||
.field(
|
||||
"external_generation_worker_concurrency",
|
||||
&self.external_generation_worker_concurrency,
|
||||
)
|
||||
.field("max_concurrent_requests", &self.max_concurrent_requests)
|
||||
.field(
|
||||
"admin_max_concurrent_requests",
|
||||
&self.admin_max_concurrent_requests,
|
||||
)
|
||||
.field("spacetime_pool_size", &self.spacetime_pool_size)
|
||||
.field("sms_auth_enabled", &self.sms_auth_enabled)
|
||||
.field("wechat_auth_enabled", &self.wechat_auth_enabled)
|
||||
.field("wechat_pay_enabled", &self.wechat_pay_enabled)
|
||||
.field("aliyun_matting_enabled", &self.aliyun_matting_enabled)
|
||||
.field("tracking_outbox_enabled", &self.tracking_outbox_enabled)
|
||||
.field(
|
||||
"wallet_refund_outbox_enabled",
|
||||
&self.wallet_refund_outbox_enabled,
|
||||
)
|
||||
.field("otel_enabled", &self.otel_enabled)
|
||||
.field("credentials", &"<redacted>")
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ProcessRole {
|
||||
Api,
|
||||
|
||||
@@ -122,9 +122,15 @@ impl BackpressureState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct AppState(Arc<AppStateInner>);
|
||||
|
||||
impl fmt::Debug for AppState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_tuple("AppState").field(self.0.as_ref()).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Deref for AppState {
|
||||
type Target = AppStateInner;
|
||||
|
||||
@@ -225,7 +231,6 @@ impl FromRef<AppState> for PuzzleApiState {
|
||||
}
|
||||
|
||||
// Axum/Hyper 会在路由树和连接 service 上频繁 clone state;AppState 外层必须保持浅拷贝。
|
||||
#[derive(Debug)]
|
||||
pub struct AppStateInner {
|
||||
// 配置会在后续中间件、路由和平台适配接入时逐步消费。
|
||||
#[allow(dead_code)]
|
||||
@@ -285,6 +290,35 @@ pub struct AppStateInner {
|
||||
test_runtime_snapshot_store: Arc<Mutex<HashMap<String, RuntimeSnapshotRecord>>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for AppStateInner {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// AppState 聚合多个仍可独立 Debug 的平台 client。这里使用封闭摘要,禁止 Debug
|
||||
// 递归下钻到 JWT、管理员口令、OSS、微信、LLM 等运行时凭据。
|
||||
f.debug_struct("AppStateInner")
|
||||
.field("config", &self.config)
|
||||
.field("ready", &self.ready.load(Ordering::Relaxed))
|
||||
.field(
|
||||
"bgfilter_worker_reached",
|
||||
&self.bgfilter_worker_reached.load(Ordering::Relaxed),
|
||||
)
|
||||
.field("admin_runtime_enabled", &self.admin_runtime.is_some())
|
||||
.field("oss_client_enabled", &self.oss_client.is_some())
|
||||
.field("spacetime_client", &self.spacetime_client)
|
||||
.field("tracking_outbox_enabled", &self.tracking_outbox.is_some())
|
||||
.field(
|
||||
"wallet_refund_outbox_enabled",
|
||||
&self.wallet_refund_outbox.is_some(),
|
||||
)
|
||||
.field("llm_client_enabled", &self.llm_client.is_some())
|
||||
.field(
|
||||
"editor_agent_llm_client_enabled",
|
||||
&self.editor_agent_llm_client.is_some(),
|
||||
)
|
||||
.field("matting_client_enabled", &self.matting_client.is_some())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[cfg(any())]
|
||||
struct CreativeAgentSessionRuntimeRecord {
|
||||
@@ -2253,6 +2287,105 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn debug_summaries_redact_all_runtime_credentials() {
|
||||
const SENSITIVE_KEY_LURE: &str = "ISSUE_148_DEBUG_SECRET_LURE";
|
||||
|
||||
let secret = || Some(SENSITIVE_KEY_LURE.to_string());
|
||||
let config = AppConfig {
|
||||
bgfilter_internal_token: secret(),
|
||||
editor_bgfilter_token: secret(),
|
||||
aliyun_matting_access_key_id: secret(),
|
||||
aliyun_matting_access_key_secret: secret(),
|
||||
admin_username: Some("debug-admin".to_string()),
|
||||
admin_password: secret(),
|
||||
internal_api_secret: secret(),
|
||||
jwt_secret: SENSITIVE_KEY_LURE.to_string(),
|
||||
sms_access_key_id: secret(),
|
||||
sms_access_key_secret: secret(),
|
||||
sms_mock_verify_code: SENSITIVE_KEY_LURE.to_string(),
|
||||
wechat_app_secret: secret(),
|
||||
wechat_mini_program_app_secret: secret(),
|
||||
wechat_pay_private_key_pem: secret(),
|
||||
wechat_pay_private_key_path: Some(std::path::PathBuf::from(SENSITIVE_KEY_LURE)),
|
||||
wechat_pay_platform_public_key_pem: secret(),
|
||||
wechat_pay_platform_public_key_path: Some(std::path::PathBuf::from(SENSITIVE_KEY_LURE)),
|
||||
wechat_pay_api_v3_key: secret(),
|
||||
wechat_mini_program_virtual_payment_offer_id: secret(),
|
||||
wechat_mini_program_virtual_payment_app_key: secret(),
|
||||
wechat_mini_program_virtual_payment_sandbox_app_key: secret(),
|
||||
wechat_mini_program_message_token: secret(),
|
||||
wechat_mini_program_message_encoding_aes_key: secret(),
|
||||
oss_bucket: Some("debug-bucket".to_string()),
|
||||
oss_endpoint: Some("oss.example.invalid".to_string()),
|
||||
oss_access_key_id: secret(),
|
||||
oss_access_key_secret: secret(),
|
||||
spacetime_server_url: format!("https://spacetime.invalid/?token={SENSITIVE_KEY_LURE}"),
|
||||
spacetime_database: format!("debug-{SENSITIVE_KEY_LURE}"),
|
||||
spacetime_token: secret(),
|
||||
spacetime_runtime_service_bootstrap_secret: secret(),
|
||||
llm_base_url: "https://llm.example.invalid".to_string(),
|
||||
llm_api_key: secret(),
|
||||
llm_model: "debug-model".to_string(),
|
||||
dashscope_api_key: secret(),
|
||||
vector_engine_base_url: "https://vector.example.invalid".to_string(),
|
||||
vector_engine_api_key: secret(),
|
||||
hyper3d_api_key: secret(),
|
||||
volcengine_speech_api_key: secret(),
|
||||
volcengine_speech_app_id: secret(),
|
||||
volcengine_speech_access_key: secret(),
|
||||
ark_character_video_api_key: secret(),
|
||||
..AppConfig::default()
|
||||
};
|
||||
|
||||
let spacetime_config = spacetime_client_config_for_process(&config);
|
||||
let spacetime_client = SpacetimeClient::new(spacetime_config.clone());
|
||||
let state = AppState::new(config.clone()).expect("state should build");
|
||||
let config_debug = format!("{config:?}");
|
||||
let expected_config_debug = format!(
|
||||
"AppConfig {{ bind_port: {:?}, listen_backlog: {:?}, worker_threads: {:?}, process_role: {:?}, external_generation_mode: {:?}, external_generation_worker_concurrency: {:?}, max_concurrent_requests: {:?}, admin_max_concurrent_requests: {:?}, spacetime_pool_size: {:?}, sms_auth_enabled: {:?}, wechat_auth_enabled: {:?}, wechat_pay_enabled: {:?}, aliyun_matting_enabled: {:?}, tracking_outbox_enabled: {:?}, wallet_refund_outbox_enabled: {:?}, otel_enabled: {:?}, credentials: \"<redacted>\", .. }}",
|
||||
config.bind_port,
|
||||
config.listen_backlog,
|
||||
config.worker_threads,
|
||||
config.process_role,
|
||||
config.external_generation_mode,
|
||||
config.external_generation_worker_concurrency,
|
||||
config.max_concurrent_requests,
|
||||
config.admin_max_concurrent_requests,
|
||||
config.spacetime_pool_size,
|
||||
config.sms_auth_enabled,
|
||||
config.wechat_auth_enabled,
|
||||
config.wechat_pay_enabled,
|
||||
config.aliyun_matting_enabled,
|
||||
config.tracking_outbox_enabled,
|
||||
config.wallet_refund_outbox_enabled,
|
||||
config.otel_enabled,
|
||||
);
|
||||
assert_eq!(
|
||||
config_debug, expected_config_debug,
|
||||
"AppConfig Debug 只能输出显式允许的枚举、数值、布尔值和脱敏占位;新增自由字符串必须默认缺席"
|
||||
);
|
||||
|
||||
let outputs = [
|
||||
("AppConfig", config_debug.clone()),
|
||||
("SpacetimeClientConfig", format!("{spacetime_config:?}")),
|
||||
("SpacetimeClient", format!("{spacetime_client:?}")),
|
||||
("AppStateInner", format!("{:?}", state.0.as_ref())),
|
||||
("AppState", format!("{state:?}")),
|
||||
];
|
||||
|
||||
for (type_name, output) in outputs {
|
||||
assert!(
|
||||
!output.contains(SENSITIVE_KEY_LURE),
|
||||
"{type_name} Debug leaked the credential lure: {output}"
|
||||
);
|
||||
}
|
||||
assert!(config_debug.contains("process_role"));
|
||||
assert!(config_debug.contains("<redacted>"));
|
||||
assert!(format!("{spacetime_config:?}").contains("pool_size"));
|
||||
assert!(format!("{state:?}").contains("ready: true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_state_reuses_character_animation_oss_client_and_eight_permits() {
|
||||
let state = AppState::new(AppConfig::default()).expect("state should build");
|
||||
|
||||
@@ -106,7 +106,7 @@ use tracing::warn;
|
||||
|
||||
use crate::module_bindings::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
pub struct SpacetimeClientConfig {
|
||||
pub server_url: String,
|
||||
pub database: String,
|
||||
@@ -116,6 +116,23 @@ pub struct SpacetimeClientConfig {
|
||||
pub subscribe_cached_read_models: bool,
|
||||
}
|
||||
|
||||
impl fmt::Debug for SpacetimeClientConfig {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
// server_url 也可能包含 userinfo 或查询凭据,因此与 token 一并只输出安全摘要。
|
||||
f.debug_struct("SpacetimeClientConfig")
|
||||
.field("server_url_configured", &!self.server_url.trim().is_empty())
|
||||
.field("database_configured", &!self.database.trim().is_empty())
|
||||
.field("token", &"<redacted>")
|
||||
.field("pool_size", &self.pool_size)
|
||||
.field("procedure_timeout", &self.procedure_timeout)
|
||||
.field(
|
||||
"subscribe_cached_read_models",
|
||||
&self.subscribe_cached_read_models,
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SpacetimeClientStage {
|
||||
Ready,
|
||||
|
||||
Reference in New Issue
Block a user