Harden auth/vault path sanitization and clean WeKnora docs

This commit is contained in:
Agent Board
2026-07-28 17:04:27 +08:00
parent 2deaf59f7b
commit 26ff1a9c9a
190 changed files with 13454 additions and 4987 deletions
@@ -46,7 +46,7 @@ enum Backend {
TursoSynced,
}
#[derive(Debug, Clone)]
#[derive(Clone)]
struct BackendConfig {
backend: Backend,
sqlite_path: Option<PathBuf>,
@@ -57,13 +57,60 @@ struct BackendConfig {
turso_token: Option<String>,
}
#[derive(Debug, Clone)]
impl std::fmt::Debug for BackendConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BackendConfig")
.field("backend", &self.backend)
.field("sqlite_path", &self.sqlite_path)
.field("libsql_local_path", &self.libsql_local_path)
.field("replica_path", &self.replica_path)
.field("synced_path", &self.synced_path)
.field("turso_url", &self.turso_url)
.field(
"turso_token",
&self.turso_token.as_ref().map(|_| "<redacted>"),
)
.finish()
}
}
#[derive(Clone)]
struct Args {
command: String,
flags: BTreeMap<String, String>,
switches: BTreeSet<String>,
}
impl std::fmt::Debug for Args {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// flags 可能含 password / turso-token 等敏感值
const SENSITIVE_FLAGS: &[&str] = &[
"password",
"turso-token",
"token",
"auth-token",
"secret",
"api-key",
];
let redacted_flags: BTreeMap<&String, String> = self
.flags
.iter()
.map(|(k, v)| {
let key_l = k.to_ascii_lowercase();
let hide = SENSITIVE_FLAGS
.iter()
.any(|s| key_l == *s || key_l.contains(s));
(k, if hide { "<redacted>".into() } else { v.clone() })
})
.collect();
f.debug_struct("Args")
.field("command", &self.command)
.field("flags", &redacted_flags)
.field("switches", &self.switches)
.finish()
}
}
enum StoreHandle {
Sqlite(SqliteControlPlaneStore),
Turso(TursoControlPlaneStore),
@@ -887,7 +934,7 @@ fn cmd_init(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
"backend": format!("{:?}", config.backend),
"planned": [
"run schema migrations",
"upsert default e2e/admin user",
"upsert default ops admin user (mnote-admin)",
"create password identity",
"upsert default local workspace",
"grant write access",
@@ -900,20 +947,46 @@ fn cmd_init(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
}
let store = open_store(&config)?;
// 7-76 方案 A:默认 init 主体是 ops adminmnote-admin),不是 AI 主体 mnote-e2e。
let user_id = flag_or_env(args, "user-id", "MNOTE_CONTROL_PLANE_INIT_USER_ID")
.unwrap_or_else(|| "mnote-e2e".to_string());
.unwrap_or_else(|| "mnote-admin".to_string());
let email = flag_or_env(args, "email", "MNOTE_CONTROL_PLANE_INIT_EMAIL")
.unwrap_or_else(|| "mnote.e2e@example.com".to_string());
.unwrap_or_else(|| "mnote.admin@example.com".to_string());
let username = flag_or_env(args, "username", "MNOTE_CONTROL_PLANE_INIT_USERNAME")
.unwrap_or_else(|| "mnote-e2e".to_string());
.unwrap_or_else(|| "mnote-admin".to_string());
let display_name = flag_or_env(
args,
"display-name",
"MNOTE_CONTROL_PLANE_INIT_DISPLAY_NAME",
)
.unwrap_or_else(|| username.clone());
let password = flag_or_env(args, "password", "MNOTE_CONTROL_PLANE_INIT_PASSWORD")
.unwrap_or_else(|| "MnoteE2E123!".to_string());
// 禁止硬编码默认管理员密码。生产/本地 init 必须显式传入:
// --password <secret> 或 MNOTE_CONTROL_PLANE_INIT_PASSWORD
// 仅当显式打开不安全开关时,才允许本地已知默认口令(仅 smoke)。
let password = match flag_or_env(args, "password", "MNOTE_CONTROL_PLANE_INIT_PASSWORD") {
Some(value) => value,
None => {
let allow_insecure = std::env::var("MNOTE_CONTROL_PLANE_ALLOW_INSECURE_DEFAULT_PASSWORD")
.ok()
.map(|v| {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes"
)
})
.unwrap_or(false);
if allow_insecure {
// 与 scripts/TESTING_REFERENCE 本地 admin 口令对齐(仅 insecure 开关)
"MnoteAdmin123!".to_string()
} else {
return Err(
"init 需要管理员密码:请传 --password 或设置 MNOTE_CONTROL_PLANE_INIT_PASSWORD\
本地若必须使用默认口令,需额外设置 MNOTE_CONTROL_PLANE_ALLOW_INSECURE_DEFAULT_PASSWORD=1"
.into(),
);
}
}
};
let workspace_id = flag_or_env(
args,
"workspace-id",
@@ -925,26 +998,37 @@ fn cmd_init(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
"workspace-name",
"MNOTE_CONTROL_PLANE_INIT_WORKSPACE_NAME",
)
.unwrap_or_else(|| "MNote E2E Workspace".to_string());
.unwrap_or_else(|| "MNote Admin Workspace".to_string());
let root_path = flag_or_env(args, "root-path", "MNOTE_CONTROL_PLANE_INIT_ROOT_PATH")
.unwrap_or_else(|| "/mnt/Data1T/Mnote_data/workspaces/default".to_string());
.unwrap_or_else(|| {
format!("/mnt/Data1T/Mnote_data/users/{user_id}/workspaces/my-space")
});
let root_uri = flag_or_env(args, "root-uri", "MNOTE_CONTROL_PLANE_INIT_ROOT_URI")
.unwrap_or_else(|| format!("file://{root_path}"));
// rolemnote-e2e → ai_service;默认 admin 账号 → admin;其它可显式 --role
let role = flag_or_env(args, "role", "MNOTE_CONTROL_PLANE_INIT_ROLE").unwrap_or_else(|| {
if user_id == "mnote-e2e" || username == "mnote-e2e" {
"ai_service".to_string()
} else {
"admin".to_string()
}
});
let user = store.upsert_user(UpsertUserInput {
id: Some(user_id.clone()),
email: Some(email.clone()),
username: username.clone(),
display_name,
role: Some("admin".to_string()),
role: Some(role),
password_hash: None,
})?;
let _ = store.create_password_identity(control_plane::CreatePasswordIdentityInput {
store.create_password_identity(control_plane::CreatePasswordIdentityInput {
user_id: user.id.clone(),
email: Some(email),
username,
password,
});
})?;
let workspace = store.upsert_workspace(UpsertWorkspaceInput {
id: Some(workspace_id),
owner_user_id: user.id.clone(),
@@ -997,7 +1081,8 @@ fn cmd_init(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
"ok": true,
"backend": format!("{:?}", config.backend),
"userId": user.id,
"workspaceId": workspace.id
"workspaceId": workspace.id,
"role": user.role,
}))?
);
Ok(())
+90 -4
View File
@@ -19,12 +19,17 @@ pub struct UserRecord {
pub revision: i64,
}
/// 会话令牌哈希(领域前缀,避免与 share/password 等 token 类型混淆)。
/// 注意:改前缀会使既有 session 行失效,需用户重新登录。
pub fn session_token_hash(raw_token: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(b"mnote-session-token-v1:");
hasher.update(raw_token.as_bytes());
hex::encode(hasher.finalize())
format!("sha256-v1:{}", hex::encode(hasher.finalize()))
}
/// 密码哈希 v1:SHA-256 + 固定领域前缀(兼容既有 `sha256-v1:` 存档)。
/// 技术债:生产应迁移 Argon2id + 每用户随机盐;在此保持算法兼容以免批量锁死账号。
pub fn password_hash_v1(password: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(b"mnote-password-v1:");
@@ -39,27 +44,76 @@ pub fn share_token_hash_v1(token: &str) -> String {
format!("sha256-v1:{}", hex::encode(hasher.finalize()))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpsertUserInput {
pub id: Option<EntityId>,
pub email: Option<String>,
pub username: String,
pub display_name: String,
pub role: Option<String>,
/// 密码哈希;Debug 脱敏,避免日志泄露。
pub password_hash: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
impl std::fmt::Debug for UpsertUserInput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UpsertUserInput")
.field("id", &self.id)
.field("email", &self.email)
.field("username", &self.username)
.field("display_name", &self.display_name)
.field("role", &self.role)
.field(
"password_hash",
&self
.password_hash
.as_ref()
.map(|_| "<redacted>")
.unwrap_or("None"),
)
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct CreatePasswordIdentityInput {
pub user_id: EntityId,
pub email: Option<String>,
pub username: String,
/// 明文密码仅用于创建身份;Debug/Serialize 必须脱敏。
pub password: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
impl Serialize for CreatePasswordIdentityInput {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("CreatePasswordIdentityInput", 4)?;
state.serialize_field("user_id", &self.user_id)?;
state.serialize_field("email", &self.email)?;
state.serialize_field("username", &self.username)?;
state.serialize_field("password", "<redacted>")?;
state.end()
}
}
impl std::fmt::Debug for CreatePasswordIdentityInput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CreatePasswordIdentityInput")
.field("user_id", &self.user_id)
.field("email", &self.email)
.field("username", &self.username)
.field("password", &"<redacted>")
.finish()
}
}
#[derive(Clone, PartialEq, Eq, Deserialize)]
pub struct AuthenticatePasswordInput {
pub account: String,
/// 明文密码仅用于鉴权;Debug/Serialize 脱敏。
pub password: String,
pub session_id: Option<EntityId>,
pub token_hash: String,
@@ -68,6 +122,38 @@ pub struct AuthenticatePasswordInput {
pub expires_at: Option<Timestamp>,
}
impl Serialize for AuthenticatePasswordInput {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeStruct;
let mut state = serializer.serialize_struct("AuthenticatePasswordInput", 7)?;
state.serialize_field("account", &self.account)?;
state.serialize_field("password", "<redacted>")?;
state.serialize_field("session_id", &self.session_id)?;
state.serialize_field("token_hash", "<redacted>")?;
state.serialize_field("user_agent", &self.user_agent)?;
state.serialize_field("ip_hash", &self.ip_hash)?;
state.serialize_field("expires_at", &self.expires_at)?;
state.end()
}
}
impl std::fmt::Debug for AuthenticatePasswordInput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AuthenticatePasswordInput")
.field("account", &self.account)
.field("password", &"<redacted>")
.field("session_id", &self.session_id)
.field("token_hash", &"<redacted>")
.field("user_agent", &self.user_agent)
.field("ip_hash", &self.ip_hash)
.field("expires_at", &self.expires_at)
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthSessionRecord {
pub id: EntityId,