tftsr-devops_investigation/src-tauri/src/proxmox/certificates.rs
Shaun Arman a438e313a6 feat: Implement Proxmox Datacenter Manager feature parity - Phases 1-11
- 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.
2026-06-11 09:38:36 -05:00

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())
}
}