Harden auth/vault path sanitization and clean WeKnora docs
This commit is contained in:
@@ -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 admin(mnote-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}"));
|
||||
|
||||
// role:mnote-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(())
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user