修复应用状态调试输出密钥泄漏 (#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:
@@ -14,6 +14,14 @@
|
||||
- 关联:相关文件、文档、提交或 Issue
|
||||
```
|
||||
|
||||
## 派生 Debug 会让完整配置经应用状态递归进入日志
|
||||
|
||||
- 现象:配置和状态当前没有直接日志调用,但新增一行 `debug!(?state, ...)` 或 `format!("{config:?}")` 就能把 JWT、后台口令、支付私钥、OSS / provider key 与 SpacetimeDB token 一次性写入日志及 OTel 留存面。
|
||||
- 原因:`AppConfig`、`AppState` 与 `AppStateInner` 曾使用派生 `Debug`;状态继续递归格式化多个含配置的 client。即使顶层状态停止下钻,`SpacetimeClientConfig` 及 `SpacetimeClient` 的独立手写路径仍会绕过顶层防线。
|
||||
- 处理:配置和聚合状态只实现封闭的手写安全摘要,不格式化任一自由字符串或含凭据的嵌套 client;`SpacetimeClientConfig` 独立脱敏,`SpacetimeClient` 只复用该安全摘要。不要以默认 `info` 级别或当前零调用点代替代码约束。
|
||||
- 验证:同一唯一哨兵同时填入全部凭据字段、可能带凭据的 SpacetimeDB URL 和数据库名,逐一格式化 `AppConfig`、`AppStateInner`、`AppState`、`SpacetimeClientConfig`、`SpacetimeClient`,断言哨兵零出现且安全运行摘要仍存在。
|
||||
- 关联:`server-rs/crates/api-server/src/config.rs`、`server-rs/crates/api-server/src/state.rs`、`server-rs/crates/spacetime-client/src/active.rs`、Issue #148。
|
||||
|
||||
## Chat 生成预算字段不能按模型名猜测或失败后自动重放
|
||||
|
||||
- 现象:同一个 OpenAI-compatible Chat endpoint 调用 reasoning 模型时返回 `Unsupported parameter: max_tokens`;直接把全局请求字段改成 `max_completion_tokens` 后,旧兼容网关又可能拒绝新字段。
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
- 新增 Markdown 文档时,文件名必须以分类标签开头,格式为 `【标签名】中文标题-日期.md`;只在任务需要时重命名历史文档,避免无关大 diff。
|
||||
- 涉及中文文本时注意 UTF-8 编码和乱码排查。
|
||||
- 涉及后端时遵循 DDD 分层,不把业务真相下沉到前端或临时兼容层。
|
||||
- 运行时日志禁止对完整配置、应用状态或 provider client 做递归 `Debug` 输出;当前 `AppConfig`、`AppState`、`AppStateInner`、`SpacetimeClientConfig` 与 `SpacetimeClient` 必须保持封闭的手写安全摘要,并用唯一哨兵测试锁定顶层与可独立格式化路径。其它仍使用派生 `Debug` 的历史 provider 类型不得新增整对象日志调用,后续按类型独立脱敏。新增字段默认不进入摘要,确需排障时只增加枚举、数值、布尔值或是否配置等非敏感字段。
|
||||
- `packages/shared` 用于前后端 DTO、公开契约及跨页面复用的无业务真相 UI 组件和纯工具;不得把领域规则、后端副作用或正式状态放入其中。
|
||||
- 修改 `/api/external/v1` 的路由、HTTP 方法、请求 / 响应 DTO、请求头、状态码、鉴权或异步语义时,必须同批更新 `docs/openapi/genarrative-external-v1.openapi.json` 和对应契约测试;Rust 实现与 OpenAPI 未对齐时不得完成、提交或发布。
|
||||
- `maincloud` / `Maincloud` / `MAINCLOUD` 相关代码、脚本、测试、环境变量、命令和文档要求均视为历史残留,禁止新增、运行或引用;API smoke 统一使用 `npm run dev:api-server` 与 `/healthz`。
|
||||
|
||||
@@ -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