feat(enterprise): add first-login rotation and receipt bridge
This commit is contained in:
parent
a6cab287fb
commit
08f514b9d1
9 changed files with 322 additions and 4 deletions
|
|
@ -45,6 +45,13 @@ pub struct LoginReceipt {
|
|||
pub domain: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasswordRotationReceipt {
|
||||
pub username: String,
|
||||
pub password_changed: bool,
|
||||
}
|
||||
|
||||
fn session_path(app: &AppHandle) -> Result<std::path::PathBuf, String> {
|
||||
Ok(app
|
||||
.path()
|
||||
|
|
@ -84,6 +91,20 @@ fn keychain_has(host: &str, username: &str) -> bool {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn keychain_read(host: &str, username: &str) -> Result<String, String> {
|
||||
let output = Command::new("/usr/bin/security")
|
||||
.args(["find-internet-password", "-w", "-s", host, "-a", username])
|
||||
.output()
|
||||
.map_err(|_| "HOLOLAKE_LOGIN_KEYCHAIN_READ_FAILED".to_string())?;
|
||||
if !output.status.success() {
|
||||
return Err("HOLOLAKE_LOGIN_KEYCHAIN_READ_FAILED".into());
|
||||
}
|
||||
String::from_utf8(output.stdout)
|
||||
.map(|value| value.trim_end().to_string())
|
||||
.map_err(|_| "HOLOLAKE_LOGIN_KEYCHAIN_READ_FAILED".into())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn keychain_remove(host: &str, username: &str) {
|
||||
let _ = Command::new("/usr/bin/security")
|
||||
|
|
@ -102,6 +123,11 @@ fn keychain_has(_host: &str, _username: &str) -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn keychain_read(_host: &str, _username: &str) -> Result<String, String> {
|
||||
Err("HOLOLAKE_PERSISTENT_CREDENTIAL_UNAVAILABLE".into())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn keychain_remove(_host: &str, _username: &str) {}
|
||||
|
||||
|
|
@ -175,6 +201,143 @@ fn login_host_for_domain(domain: &str) -> Result<&'static str, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// 企业新账号第一次登录时在 HoloLake 内完成强制换密。
|
||||
/// 旧、新密码只存在于本次 HTTPS 请求内;服务端回执不含密码。
|
||||
#[tauri::command]
|
||||
pub async fn change_enterprise_first_login_password(
|
||||
state: State<'_, ZeroPointState>,
|
||||
username: String,
|
||||
current_password: String,
|
||||
new_password: String,
|
||||
) -> Result<PasswordRotationReceipt, String> {
|
||||
let Some((human_number, domain)) = zero_point::verified_user_route(&state)? else {
|
||||
return Err("HOLOLAKE_DOMAIN_ROUTE_REQUIRED".into());
|
||||
};
|
||||
if domain == "FIFTH_DOMAIN" {
|
||||
return Err("HOLOLAKE_FIRST_LOGIN_PASSWORD_CHANGE_ENTERPRISE_ONLY".into());
|
||||
}
|
||||
let username = validate_username(&username)?;
|
||||
if current_password.is_empty() || current_password.len() > 512 {
|
||||
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
||||
}
|
||||
if new_password.len() < 14 || new_password.len() > 128 || new_password == current_password {
|
||||
return Err("HOLOLAKE_NEW_PASSWORD_POLICY_INVALID".into());
|
||||
}
|
||||
let response = reqwest::Client::builder()
|
||||
.read_timeout(Duration::from_secs(30))
|
||||
.use_rustls_tls()
|
||||
.build()
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?
|
||||
.post("https://guanghu.chat/api/hololake/enterprise/change-password")
|
||||
.basic_auth(&username, Some(¤t_password))
|
||||
.json(&serde_json::json!({
|
||||
"human_number": human_number,
|
||||
"new_password": new_password,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
|
||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
||||
}
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"HOLOLAKE_FIRST_LOGIN_PASSWORD_CHANGE_FAILED: {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
Ok(PasswordRotationReceipt {
|
||||
username,
|
||||
password_changed: true,
|
||||
})
|
||||
}
|
||||
|
||||
async fn enterprise_authenticated_post(
|
||||
app: &AppHandle,
|
||||
state: &ZeroPointState,
|
||||
path: &str,
|
||||
mut payload: serde_json::Value,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
let Some((human_number, domain)) = zero_point::verified_user_route(state)? else {
|
||||
return Err("HOLOLAKE_DOMAIN_ROUTE_REQUIRED".into());
|
||||
};
|
||||
if domain == "FIFTH_DOMAIN" {
|
||||
return Err("HOLOLAKE_ENTERPRISE_RECEIPT_ROUTE_REQUIRED".into());
|
||||
}
|
||||
let session = current_login_session_for_domain(app, &domain)?
|
||||
.ok_or_else(|| "HOLOLAKE_LOGIN_SESSION_REQUIRED".to_string())?;
|
||||
let password = keychain_read(&session.host, &session.username)?;
|
||||
payload["human_number"] = serde_json::Value::String(human_number);
|
||||
let response = reqwest::Client::builder()
|
||||
.read_timeout(Duration::from_secs(20))
|
||||
.use_rustls_tls()
|
||||
.build()
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?
|
||||
.post(format!("https://guanghu.chat/api/hololake/enterprise/{path}"))
|
||||
.basic_auth(&session.username, Some(password))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_LOGIN_NETWORK_FAILED: {error}"))?;
|
||||
if response.status() == reqwest::StatusCode::UNAUTHORIZED {
|
||||
return Err("HOLOLAKE_LOGIN_CREDENTIALS_INVALID".into());
|
||||
}
|
||||
if !response.status().is_success() {
|
||||
return Err(format!("HOLOLAKE_ENTERPRISE_RECEIPT_FAILED: {}", response.status()));
|
||||
}
|
||||
response
|
||||
.json()
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_RECEIPT_FAILED: {error}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_enterprise_entry(
|
||||
app: AppHandle,
|
||||
state: State<'_, ZeroPointState>,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
enterprise_authenticated_post(&app, &state, "me/entry", serde_json::json!({})).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn confirm_enterprise_persona_relationship(
|
||||
app: AppHandle,
|
||||
state: State<'_, ZeroPointState>,
|
||||
decision: String,
|
||||
idempotency_key: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
enterprise_authenticated_post(
|
||||
&app,
|
||||
&state,
|
||||
"relationship-confirmations",
|
||||
serde_json::json!({ "decision": decision, "idempotency_key": idempotency_key }),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn submit_enterprise_responsibility_receipt(
|
||||
app: AppHandle,
|
||||
state: State<'_, ZeroPointState>,
|
||||
decision: String,
|
||||
note: String,
|
||||
responsibility_version: String,
|
||||
idempotency_key: String,
|
||||
) -> Result<serde_json::Value, String> {
|
||||
enterprise_authenticated_post(
|
||||
&app,
|
||||
&state,
|
||||
"responsibility-receipts",
|
||||
serde_json::json!({
|
||||
"decision": decision,
|
||||
"note": note,
|
||||
"responsibility_version": responsibility_version,
|
||||
"idempotency_key": idempotency_key,
|
||||
}),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// 登录验证:基本认证打 Forgejo 用户接口,密码错=401 即拒;
|
||||
/// 通过后凭证进钥匙串,会话(不含密码)落盘。
|
||||
#[tauri::command]
|
||||
|
|
|
|||
|
|
@ -70,6 +70,10 @@ pub fn run() {
|
|||
pncc_server_projection::query_jd_pncc_server_projection,
|
||||
code_repo_login::check_code_repo_login,
|
||||
code_repo_login::perform_code_repo_login,
|
||||
code_repo_login::change_enterprise_first_login_password,
|
||||
code_repo_login::get_enterprise_entry,
|
||||
code_repo_login::confirm_enterprise_persona_relationship,
|
||||
code_repo_login::submit_enterprise_responsibility_receipt,
|
||||
code_repo_login::sign_out_code_repo_login,
|
||||
user_pncc_channel::get_user_pncc_channel,
|
||||
user_pncc_channel::ensure_user_pncc_channel,
|
||||
|
|
|
|||
Loading…
Reference in a new issue