feat: separate enterprise work and personal routes
This commit is contained in:
parent
e6d45eaea8
commit
7414810862
9 changed files with 546 additions and 41 deletions
|
|
@ -92,7 +92,7 @@ fn keychain_has(host: &str, username: &str) -> bool {
|
|||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn keychain_read(host: &str, username: &str) -> Result<String, String> {
|
||||
pub(crate) 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()
|
||||
|
|
@ -124,7 +124,7 @@ fn keychain_has(_host: &str, _username: &str) -> bool {
|
|||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn keychain_read(_host: &str, _username: &str) -> Result<String, String> {
|
||||
pub(crate) fn keychain_read(_host: &str, _username: &str) -> Result<String, String> {
|
||||
Err("HOLOLAKE_PERSISTENT_CREDENTIAL_UNAVAILABLE".into())
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ fn expected_account_for_human_number(number: &str) -> Option<&'static str> {
|
|||
}
|
||||
}
|
||||
|
||||
fn validate_number_account_binding(number: &str, username: &str) -> Result<(), String> {
|
||||
pub(crate) fn validate_number_account_binding(number: &str, username: &str) -> Result<(), String> {
|
||||
match expected_account_for_human_number(number) {
|
||||
Some(expected) if username.eq_ignore_ascii_case(expected) => Ok(()),
|
||||
Some(_) => Err("HOLOLAKE_LOGIN_ACCOUNT_NUMBER_MISMATCH".into()),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,251 @@
|
|||
//! 企业四域私有责任仓库的本机只读工作投影。
|
||||
//!
|
||||
//! 企业账号先进入零感域的光湖频道,再展开本人责任工作域。仓库凭证只从
|
||||
//! 系统钥匙串进入一次 Git 子进程环境,不写入 remote URL、配置或文件。
|
||||
|
||||
use serde::Serialize;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
use crate::code_channel;
|
||||
use crate::code_repo_login;
|
||||
use crate::zero_point::{self, ZeroPointState};
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EnterpriseWorkChannelSnapshot {
|
||||
pub state: &'static str,
|
||||
pub work_entry_domain: &'static str,
|
||||
pub work_entry_channel: &'static str,
|
||||
pub responsibility_domain: String,
|
||||
pub repository: String,
|
||||
pub remote_repository_url: String,
|
||||
pub local_path: String,
|
||||
pub channel_id: String,
|
||||
pub authority: &'static str,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ensure_enterprise_work_channel(
|
||||
app: AppHandle,
|
||||
state: State<'_, ZeroPointState>,
|
||||
) -> Result<EnterpriseWorkChannelSnapshot, 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_WORK_ROUTE_REQUIRED".into());
|
||||
}
|
||||
let session = code_repo_login::current_login_session_for_domain(&app, &domain)?
|
||||
.ok_or_else(|| "HOLOLAKE_LOGIN_SESSION_REQUIRED".to_string())?;
|
||||
code_repo_login::validate_number_account_binding(&human_number, &session.username)?;
|
||||
let (repository, registered_domain) = enterprise_repository(&human_number)?;
|
||||
if registered_domain != domain {
|
||||
return Err("HOLOLAKE_ENTERPRISE_WORK_DOMAIN_MISMATCH".into());
|
||||
}
|
||||
let password = code_repo_login::keychain_read(&session.host, &session.username)?;
|
||||
let root =
|
||||
crate::authenticated_storage::account_storage_root(&app, "enterprise-work-channel-v1")?;
|
||||
let remote = format!("https://guanghu.chat/code/{repository}.git");
|
||||
let local = root.join("repository");
|
||||
let username = session.username.clone();
|
||||
let repository_owned = repository.to_string();
|
||||
let remote_owned = remote.clone();
|
||||
let local_for_sync = local.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
sync_repository_at(&root, &local_for_sync, &remote_owned, &username, &password)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_JOIN_FAILED: {error}"))??;
|
||||
let entry = code_channel::register_managed_repository(
|
||||
&app,
|
||||
&local,
|
||||
format!("{} · 企业责任工作仓库", display_domain(&domain)),
|
||||
)?;
|
||||
Ok(EnterpriseWorkChannelSnapshot {
|
||||
state: "READY_READ_ONLY_WORK_PROJECTION",
|
||||
work_entry_domain: "ZERO_SENSE_DOMAIN",
|
||||
work_entry_channel: "GUANGHU_CHANNEL",
|
||||
responsibility_domain: domain,
|
||||
repository: repository_owned,
|
||||
remote_repository_url: remote,
|
||||
local_path: local.to_string_lossy().into_owned(),
|
||||
channel_id: entry.channel_id,
|
||||
authority: "AUTHENTICATED_USER_READ_ONLY_NO_PUSH_NO_PERSONAL_NODE_AUTHORITY",
|
||||
})
|
||||
}
|
||||
|
||||
fn enterprise_repository(number: &str) -> Result<(&'static str, &'static str), String> {
|
||||
match number {
|
||||
"TCS-GL-0005∞" => Ok(("huaer/guanghu-branch-work", "BRANCH_DOMAIN")),
|
||||
"TCS-GL-0006∞" => Ok(("yeye/guanghu-zero-work", "ZERO_DOMAIN")),
|
||||
"TCS-GL-0007∞" => Ok(("feimao/guanghu-zero-sense-work", "ZERO_SENSE_DOMAIN")),
|
||||
"TCS-GL-0008∞" => Ok(("juzi/guanghu-zero-sense-work", "ZERO_SENSE_DOMAIN")),
|
||||
"TCS-GL-0016∞" => Ok(("awen/guanghu-main-work", "MAIN_DOMAIN")),
|
||||
_ => Err("HOLOLAKE_ENTERPRISE_WORK_BINDING_UNREGISTERED".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn display_domain(domain: &str) -> &'static str {
|
||||
match domain {
|
||||
"MAIN_DOMAIN" => "光湖主域",
|
||||
"BRANCH_DOMAIN" => "光湖分域",
|
||||
"ZERO_DOMAIN" => "光湖零域",
|
||||
"ZERO_SENSE_DOMAIN" => "光湖零感域",
|
||||
_ => "企业责任域",
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_repository_at(
|
||||
root: &Path,
|
||||
repository: &Path,
|
||||
remote: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), String> {
|
||||
fs::create_dir_all(root)
|
||||
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_STORAGE_FAILED: {error}"))?;
|
||||
let askpass = write_askpass(root)?;
|
||||
if repository.exists() {
|
||||
let actual = git_output(
|
||||
repository,
|
||||
&askpass,
|
||||
username,
|
||||
password,
|
||||
&["config", "--get", "remote.origin.url"],
|
||||
)?;
|
||||
if actual.trim() != remote {
|
||||
return Err("HOLOLAKE_ENTERPRISE_WORK_REMOTE_MISMATCH".into());
|
||||
}
|
||||
git(
|
||||
repository,
|
||||
&askpass,
|
||||
username,
|
||||
password,
|
||||
&["fetch", "--depth", "1", "origin", "main"],
|
||||
)?;
|
||||
git(
|
||||
repository,
|
||||
&askpass,
|
||||
username,
|
||||
password,
|
||||
&["reset", "--hard", "FETCH_HEAD"],
|
||||
)?;
|
||||
git(repository, &askpass, username, password, &["clean", "-fd"])?;
|
||||
} else {
|
||||
let destination = repository.to_string_lossy().into_owned();
|
||||
git(
|
||||
root,
|
||||
&askpass,
|
||||
username,
|
||||
password,
|
||||
&[
|
||||
"clone",
|
||||
"--depth",
|
||||
"1",
|
||||
"--branch",
|
||||
"main",
|
||||
remote,
|
||||
&destination,
|
||||
],
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_askpass(root: &Path) -> Result<PathBuf, String> {
|
||||
let path = root.join("git-askpass.sh");
|
||||
let body = "#!/bin/sh\ncase \"$1\" in\n *Username*) printf '%s' \"$HOLOLAKE_GIT_USERNAME\" ;;\n *Password*) printf '%s' \"$HOLOLAKE_GIT_PASSWORD\" ;;\n *) exit 1 ;;\nesac\n";
|
||||
fs::write(&path, body)
|
||||
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_ASKPASS_FAILED: {error}"))?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o700))
|
||||
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_ASKPASS_FAILED: {error}"))?;
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn git(
|
||||
cwd: &Path,
|
||||
askpass: &Path,
|
||||
username: &str,
|
||||
password: &str,
|
||||
args: &[&str],
|
||||
) -> Result<(), String> {
|
||||
let output = git_command(cwd, askpass, username, password, args)
|
||||
.output()
|
||||
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_GIT_FAILED: {error}"))?;
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("HOLOLAKE_ENTERPRISE_WORK_GIT_FAILED".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn git_output(
|
||||
cwd: &Path,
|
||||
askpass: &Path,
|
||||
username: &str,
|
||||
password: &str,
|
||||
args: &[&str],
|
||||
) -> Result<String, String> {
|
||||
let output = git_command(cwd, askpass, username, password, args)
|
||||
.output()
|
||||
.map_err(|error| format!("HOLOLAKE_ENTERPRISE_WORK_GIT_FAILED: {error}"))?;
|
||||
if !output.status.success() {
|
||||
return Err("HOLOLAKE_ENTERPRISE_WORK_GIT_FAILED".into());
|
||||
}
|
||||
String::from_utf8(output.stdout)
|
||||
.map_err(|_| "HOLOLAKE_ENTERPRISE_WORK_GIT_OUTPUT_INVALID".into())
|
||||
}
|
||||
|
||||
fn git_command(
|
||||
cwd: &Path,
|
||||
askpass: &Path,
|
||||
username: &str,
|
||||
password: &str,
|
||||
args: &[&str],
|
||||
) -> Command {
|
||||
let mut command = Command::new("git");
|
||||
command
|
||||
.current_dir(cwd)
|
||||
.args(["-c", "credential.helper="])
|
||||
.args(args)
|
||||
.env("GIT_TERMINAL_PROMPT", "0")
|
||||
.env("GIT_ASKPASS", askpass)
|
||||
.env("HOLOLAKE_GIT_USERNAME", username)
|
||||
.env("HOLOLAKE_GIT_PASSWORD", password);
|
||||
command
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn every_enterprise_number_has_one_exact_private_repository() {
|
||||
assert_eq!(
|
||||
enterprise_repository("TCS-GL-0007∞").unwrap().0,
|
||||
"feimao/guanghu-zero-sense-work"
|
||||
);
|
||||
assert_eq!(
|
||||
enterprise_repository("TCS-GL-0008∞").unwrap().0,
|
||||
"juzi/guanghu-zero-sense-work"
|
||||
);
|
||||
assert!(enterprise_repository("ICE-GL∞").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn askpass_script_contains_no_secret() {
|
||||
let root = tempdir().unwrap();
|
||||
let path = write_askpass(root.path()).unwrap();
|
||||
let body = fs::read_to_string(path).unwrap();
|
||||
assert!(body.contains("HOLOLAKE_GIT_PASSWORD"));
|
||||
assert!(!body.contains("temporary-secret"));
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ mod code_repo_login;
|
|||
mod direct_local_broker;
|
||||
mod direct_local_session;
|
||||
mod dynamic_capability_routing;
|
||||
mod enterprise_work_channel;
|
||||
mod glp_envelope;
|
||||
mod home_status;
|
||||
mod knowledge_base;
|
||||
|
|
@ -74,6 +75,7 @@ pub fn run() {
|
|||
code_repo_login::get_enterprise_entry,
|
||||
code_repo_login::confirm_enterprise_persona_relationship,
|
||||
code_repo_login::submit_enterprise_responsibility_receipt,
|
||||
enterprise_work_channel::ensure_enterprise_work_channel,
|
||||
code_repo_login::sign_out_code_repo_login,
|
||||
user_pncc_channel::get_user_pncc_channel,
|
||||
user_pncc_channel::ensure_user_pncc_channel,
|
||||
|
|
|
|||
|
|
@ -104,6 +104,11 @@ fn trusted_subject(
|
|||
let Some((number, domain)) = zero_point::verified_user_route(state)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
// 企业账号的代码入口是企业责任仓库;不能自动创建一个本机个人仓库
|
||||
// 来冒充工作域或绕过之后的个人节点拥有权验证。
|
||||
if domain != "FIFTH_DOMAIN" {
|
||||
return Ok(None);
|
||||
}
|
||||
let Some(session) = code_repo_login::current_login_session_for_domain(app, &domain)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue