- Phase 1: Dashboard Widget System (11 widgets) - Phase 2: Resource Tree View (ResourceTree + ResourceFilter) - Phase 3: VM Manager UI (VMList + SnapshotForm + MigrationForm) - Phase 4: Backup Manager UI (BackupJobList) - Phase 5: Ceph Manager UI (CephHealthWidget + PoolList + OSDList + MonitorList) - Phase 6: SDN Manager UI (EVPNZoneList) - Phase 7: Firewall Manager UI (FirewallRuleList) - Phase 8: HA Groups Manager UI (HAGroupsList + HAResourcesList) - Phase 9: User Management UI (RealmList + UserList) - Phase 10: Certificate Manager UI (CertificateList) - Phase 11: Subscription Registry UI (SubscriptionList) All components pass TypeScript, ESLint, and existing tests. All Rust code passes clippy and format checks.
473 lines
14 KiB
Rust
473 lines
14 KiB
Rust
// Certificate Management module
|
|
// Provides operations for managing certificates
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Certificate information
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Certificate {
|
|
pub certificate_id: String,
|
|
pub common_name: String,
|
|
pub issuer: String,
|
|
pub serial: String,
|
|
pub not_before: String,
|
|
pub not_after: String,
|
|
pub fingerprint: String,
|
|
pub key_size: u32,
|
|
pub signature_algorithm: String,
|
|
pub san: Vec<String>,
|
|
}
|
|
|
|
/// Certificate chain
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CertificateChain {
|
|
pub certificates: Vec<Certificate>,
|
|
pub chain_length: u32,
|
|
}
|
|
|
|
/// Upload certificate
|
|
pub async fn upload_certificate(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
certificate: &str,
|
|
private_key: &str,
|
|
name: Option<&str>,
|
|
ticket: &str,
|
|
) -> Result<Certificate, String> {
|
|
let path = "config/certificate";
|
|
let config = serde_json::json!({
|
|
"certificate": certificate,
|
|
"privatekey": private_key,
|
|
"name": name.unwrap_or("")
|
|
});
|
|
|
|
let response: serde_json::Value = client
|
|
.post(path, &config, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to upload certificate: {}", e))?;
|
|
|
|
if let Some(data) = response.get("data") {
|
|
let id = data
|
|
.get("id")
|
|
.and_then(|i| i.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let common_name = data
|
|
.get("common_name")
|
|
.and_then(|c| c.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let issuer = data
|
|
.get("issuer")
|
|
.and_then(|i| i.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let serial = data
|
|
.get("serial")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let not_before = data
|
|
.get("not_before")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let not_after = data
|
|
.get("not_after")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let fingerprint = data
|
|
.get("fingerprint")
|
|
.and_then(|f| f.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let key_size = data.get("key_size").and_then(|k| k.as_u64()).unwrap_or(0) as u32;
|
|
let signature_algorithm = data
|
|
.get("signature_algorithm")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
let san: Vec<String> = data
|
|
.get("san")
|
|
.and_then(|s| s.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|s| s.as_str().map(|s| s.to_string()))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
Ok(Certificate {
|
|
certificate_id: id,
|
|
common_name,
|
|
issuer,
|
|
serial,
|
|
not_before,
|
|
not_after,
|
|
fingerprint,
|
|
key_size,
|
|
signature_algorithm,
|
|
san,
|
|
})
|
|
} else {
|
|
Err("Invalid response format: missing 'data' field".to_string())
|
|
}
|
|
}
|
|
|
|
/// Get certificate details
|
|
pub async fn get_certificate(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
cert_id: &str,
|
|
ticket: &str,
|
|
) -> Result<Certificate, String> {
|
|
let path = format!("config/certificate/{}", cert_id);
|
|
let response: serde_json::Value = client
|
|
.get(&path, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to get certificate {}: {}", cert_id, e))?;
|
|
|
|
if let Some(data) = response.get("data") {
|
|
let id = data
|
|
.get("id")
|
|
.and_then(|i| i.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let common_name = data
|
|
.get("common_name")
|
|
.and_then(|c| c.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let issuer = data
|
|
.get("issuer")
|
|
.and_then(|i| i.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let serial = data
|
|
.get("serial")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let not_before = data
|
|
.get("not_before")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let not_after = data
|
|
.get("not_after")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let fingerprint = data
|
|
.get("fingerprint")
|
|
.and_then(|f| f.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let key_size = data.get("key_size").and_then(|k| k.as_u64()).unwrap_or(0) as u32;
|
|
let signature_algorithm = data
|
|
.get("signature_algorithm")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
let san: Vec<String> = data
|
|
.get("san")
|
|
.and_then(|s| s.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|s| s.as_str().map(|s| s.to_string()))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
Ok(Certificate {
|
|
certificate_id: id,
|
|
common_name,
|
|
issuer,
|
|
serial,
|
|
not_before,
|
|
not_after,
|
|
fingerprint,
|
|
key_size,
|
|
signature_algorithm,
|
|
san,
|
|
})
|
|
} else {
|
|
Err("Invalid response format: missing 'data' field".to_string())
|
|
}
|
|
}
|
|
|
|
/// List certificates
|
|
pub async fn list_certificates(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
ticket: &str,
|
|
) -> Result<Vec<Certificate>, String> {
|
|
let path = "config/certificate";
|
|
let response: serde_json::Value = client
|
|
.get(path, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to list certificates: {}", e))?;
|
|
|
|
if let Some(certs) = response.get("data").and_then(|d| d.as_array()) {
|
|
let cert_list: Vec<Certificate> = certs
|
|
.iter()
|
|
.filter_map(|cert| {
|
|
let id = cert.get("id")?.as_str()?.to_string();
|
|
let common_name = cert
|
|
.get("common_name")
|
|
.and_then(|c| c.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let issuer = cert
|
|
.get("issuer")
|
|
.and_then(|i| i.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let serial = cert
|
|
.get("serial")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let not_before = cert
|
|
.get("not_before")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let not_after = cert
|
|
.get("not_after")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let fingerprint = cert
|
|
.get("fingerprint")
|
|
.and_then(|f| f.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let key_size = cert.get("key_size").and_then(|k| k.as_u64()).unwrap_or(0) as u32;
|
|
let signature_algorithm = cert
|
|
.get("signature_algorithm")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
let san: Vec<String> = cert
|
|
.get("san")
|
|
.and_then(|s| s.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|s| s.as_str().map(|s| s.to_string()))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
Some(Certificate {
|
|
certificate_id: id,
|
|
common_name,
|
|
issuer,
|
|
serial,
|
|
not_before,
|
|
not_after,
|
|
fingerprint,
|
|
key_size,
|
|
signature_algorithm,
|
|
san,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(cert_list)
|
|
} else {
|
|
Err("Invalid response format: missing 'data' field".to_string())
|
|
}
|
|
}
|
|
|
|
/// Delete certificate
|
|
pub async fn delete_certificate(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
cert_id: &str,
|
|
ticket: &str,
|
|
) -> Result<(), String> {
|
|
let path = format!("config/certificate/{}", cert_id);
|
|
let _response: serde_json::Value = client
|
|
.delete(&path, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to delete certificate {}: {}", cert_id, e))?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Get node certificates
|
|
pub async fn list_node_certificates(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
ticket: &str,
|
|
) -> Result<Vec<Certificate>, String> {
|
|
let path = format!("nodes/{}/certificates", node);
|
|
let response: serde_json::Value = client
|
|
.get(&path, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to list node certificates for {}: {}", node, e))?;
|
|
|
|
if let Some(certs) = response.get("data").and_then(|d| d.as_array()) {
|
|
let cert_list: Vec<Certificate> = certs
|
|
.iter()
|
|
.filter_map(|cert| {
|
|
let id = cert.get("id")?.as_str()?.to_string();
|
|
let common_name = cert
|
|
.get("common_name")
|
|
.and_then(|c| c.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let issuer = cert
|
|
.get("issuer")
|
|
.and_then(|i| i.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let serial = cert
|
|
.get("serial")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let not_before = cert
|
|
.get("not_before")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let not_after = cert
|
|
.get("not_after")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let fingerprint = cert
|
|
.get("fingerprint")
|
|
.and_then(|f| f.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let key_size = cert.get("key_size").and_then(|k| k.as_u64()).unwrap_or(0) as u32;
|
|
let signature_algorithm = cert
|
|
.get("signature_algorithm")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
let san: Vec<String> = cert
|
|
.get("san")
|
|
.and_then(|s| s.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|s| s.as_str().map(|s| s.to_string()))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
Some(Certificate {
|
|
certificate_id: id,
|
|
common_name,
|
|
issuer,
|
|
serial,
|
|
not_before,
|
|
not_after,
|
|
fingerprint,
|
|
key_size,
|
|
signature_algorithm,
|
|
san,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
Ok(cert_list)
|
|
} else {
|
|
Err("Invalid response format: missing 'data' field".to_string())
|
|
}
|
|
}
|
|
|
|
/// Upload node certificate
|
|
pub async fn upload_node_certificate(
|
|
client: &crate::proxmox::client::ProxmoxClient,
|
|
node: &str,
|
|
certificate: &str,
|
|
private_key: &str,
|
|
name: Option<&str>,
|
|
ticket: &str,
|
|
) -> Result<Certificate, String> {
|
|
let path = format!("nodes/{}/certificates", node);
|
|
let config = serde_json::json!({
|
|
"certificate": certificate,
|
|
"privatekey": private_key,
|
|
"name": name.unwrap_or("")
|
|
});
|
|
|
|
let response: serde_json::Value = client
|
|
.post(&path, &config, Some(ticket))
|
|
.await
|
|
.map_err(|e| format!("Failed to upload node certificate for {}: {}", node, e))?;
|
|
|
|
if let Some(data) = response.get("data") {
|
|
let id = data
|
|
.get("id")
|
|
.and_then(|i| i.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let common_name = data
|
|
.get("common_name")
|
|
.and_then(|c| c.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let issuer = data
|
|
.get("issuer")
|
|
.and_then(|i| i.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let serial = data
|
|
.get("serial")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let not_before = data
|
|
.get("not_before")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let not_after = data
|
|
.get("not_after")
|
|
.and_then(|n| n.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let fingerprint = data
|
|
.get("fingerprint")
|
|
.and_then(|f| f.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
let key_size = data.get("key_size").and_then(|k| k.as_u64()).unwrap_or(0) as u32;
|
|
let signature_algorithm = data
|
|
.get("signature_algorithm")
|
|
.and_then(|s| s.as_str())
|
|
.unwrap_or("")
|
|
.to_string();
|
|
|
|
let san: Vec<String> = data
|
|
.get("san")
|
|
.and_then(|s| s.as_array())
|
|
.map(|arr| {
|
|
arr.iter()
|
|
.filter_map(|s| s.as_str().map(|s| s.to_string()))
|
|
.collect()
|
|
})
|
|
.unwrap_or_default();
|
|
|
|
Ok(Certificate {
|
|
certificate_id: id,
|
|
common_name,
|
|
issuer,
|
|
serial,
|
|
not_before,
|
|
not_after,
|
|
fingerprint,
|
|
key_size,
|
|
signature_algorithm,
|
|
san,
|
|
})
|
|
} else {
|
|
Err("Invalid response format: missing 'data' field".to_string())
|
|
}
|
|
}
|