tftsr-devops_investigation/src-tauri/src/proxmox/certificates.rs
Shaun Arman 9687f97d7c fix(proxmox): remove double-unwrap of Proxmox data envelope across all modules
handle_response() in client.rs already strips the {"data":...} wrapper
before returning to callers. Every proxmox module was calling .get("data")
a second time on the already-unwrapped Value, which always returned None
and caused all API responses to silently yield empty results or errors.

vm.rs had an additional bug: list_vms used POST on cluster/resources (a
GET-only endpoint) and dropped VMs with no cpu field via filter_map ?
instead of unwrap_or(0.0). Both corrected.

Affected modules: vm, ceph, ceph_cluster, certificates, acme, firewall,
sdn, ha, apt, updates, updates_ext, tasks, migration, metrics, shell,
auth_realm, views, backup — 18 files, 19 functions.

426 Rust tests pass. clippy -D warnings clean. tsc --noEmit clean.
2026-06-20 19:38:49 -05:00

470 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))?;
{
let data = &response;
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,
})
}
}
/// 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))?;
{
let data = &response;
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,
})
}
}
/// 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.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.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))?;
{
let data = &response;
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,
})
}
}