//! Certificate service implementation use std::sync::Arc; use base64::Engine as _; use crate::metadata::LbMetadataStore; use fiberlb_api::{ certificate_service_server::CertificateService, CreateCertificateRequest, CreateCertificateResponse, DeleteCertificateRequest, DeleteCertificateResponse, GetCertificateRequest, GetCertificateResponse, ListCertificatesRequest, ListCertificatesResponse, Certificate as ProtoCertificate, CertificateType as ProtoCertificateType, }; use fiberlb_types::{ Certificate, CertificateId, CertificateType, LoadBalancerId, }; use iam_service_auth::{get_tenant_context, resource_for_tenant, AuthService}; use tonic::{Request, Response, Status}; use uuid::Uuid; /// Certificate service implementation pub struct CertificateServiceImpl { metadata: Arc, auth: Arc, } impl CertificateServiceImpl { /// Create a new CertificateServiceImpl pub fn new(metadata: Arc, auth: Arc) -> Self { Self { metadata, auth } } } const ACTION_CERTS_CREATE: &str = "network:certificates:create"; const ACTION_CERTS_READ: &str = "network:certificates:read"; const ACTION_CERTS_LIST: &str = "network:certificates:list"; const ACTION_CERTS_DELETE: &str = "network:certificates:delete"; /// Convert domain Certificate to proto fn certificate_to_proto(cert: &Certificate) -> ProtoCertificate { ProtoCertificate { id: cert.id.to_string(), loadbalancer_id: cert.loadbalancer_id.to_string(), name: cert.name.clone(), certificate: cert.certificate.clone(), private_key: cert.private_key.clone(), cert_type: match cert.cert_type { CertificateType::Server => ProtoCertificateType::Server.into(), CertificateType::ClientCa => ProtoCertificateType::ClientCa.into(), CertificateType::Sni => ProtoCertificateType::Sni.into(), }, expires_at: cert.expires_at, created_at: cert.created_at, updated_at: cert.updated_at, } } /// Parse CertificateId from string fn parse_certificate_id(id: &str) -> Result { let uuid: Uuid = id .parse() .map_err(|_| Status::invalid_argument("invalid certificate ID"))?; Ok(CertificateId::from_uuid(uuid)) } /// Parse LoadBalancerId from string fn parse_lb_id(id: &str) -> Result { let uuid: Uuid = id .parse() .map_err(|_| Status::invalid_argument("invalid load balancer ID"))?; Ok(LoadBalancerId::from_uuid(uuid)) } /// Convert proto certificate type to domain fn proto_to_cert_type(cert_type: i32) -> CertificateType { match ProtoCertificateType::try_from(cert_type) { Ok(ProtoCertificateType::Server) => CertificateType::Server, Ok(ProtoCertificateType::ClientCa) => CertificateType::ClientCa, Ok(ProtoCertificateType::Sni) => CertificateType::Sni, _ => CertificateType::Server, } } #[tonic::async_trait] impl CertificateService for CertificateServiceImpl { async fn create_certificate( &self, request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; let req = request.into_inner(); // Validate required fields if req.name.is_empty() { return Err(Status::invalid_argument("name is required")); } if req.loadbalancer_id.is_empty() { return Err(Status::invalid_argument("loadbalancer_id is required")); } if req.certificate.is_empty() { return Err(Status::invalid_argument("certificate is required")); } if req.private_key.is_empty() { return Err(Status::invalid_argument("private_key is required")); } let lb_id = parse_lb_id(&req.loadbalancer_id)?; // Verify load balancer exists let lb = self.metadata .load_lb_by_id(&lb_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("load balancer not found"))?; if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id { return Err(Status::permission_denied("load balancer not in tenant scope")); } self.auth .authorize( &tenant, ACTION_CERTS_CREATE, &resource_for_tenant("certificate", "*", &lb.org_id, &lb.project_id), ) .await?; // Parse certificate type let cert_type = proto_to_cert_type(req.cert_type); // TODO: Parse certificate to extract expiry date // For now, set expires_at to 1 year from now let expires_at = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs() + (365 * 24 * 60 * 60); // Create new certificate let certificate = Certificate::new( &req.name, lb_id, &req.certificate, &req.private_key, cert_type, expires_at, ); // Save certificate self.metadata .save_certificate(&certificate) .await .map_err(|e| Status::internal(format!("failed to save certificate: {}", e)))?; Ok(Response::new(CreateCertificateResponse { certificate: Some(certificate_to_proto(&certificate)), })) } async fn get_certificate( &self, request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; let req = request.into_inner(); if req.id.is_empty() { return Err(Status::invalid_argument("id is required")); } let cert_id = parse_certificate_id(&req.id)?; let certificate = self .metadata .find_certificate_by_id(&cert_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("certificate not found"))?; let lb = self .metadata .load_lb_by_id(&certificate.loadbalancer_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("load balancer not found"))?; if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id { return Err(Status::permission_denied("load balancer not in tenant scope")); } self.auth .authorize( &tenant, ACTION_CERTS_READ, &resource_for_tenant( "certificate", &certificate.id.to_string(), &lb.org_id, &lb.project_id, ), ) .await?; Ok(Response::new(GetCertificateResponse { certificate: Some(certificate_to_proto(&certificate)), })) } async fn list_certificates( &self, request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; let req = request.into_inner(); if req.loadbalancer_id.is_empty() { return Err(Status::invalid_argument("loadbalancer_id is required")); } let lb_id = parse_lb_id(&req.loadbalancer_id)?; let lb = self .metadata .load_lb_by_id(&lb_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("load balancer not found"))?; if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id { return Err(Status::permission_denied("load balancer not in tenant scope")); } self.auth .authorize( &tenant, ACTION_CERTS_LIST, &resource_for_tenant("certificate", "*", &lb.org_id, &lb.project_id), ) .await?; let certificates = self .metadata .list_certificates(&lb_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))?; let page_size = if req.page_size == 0 { 50 } else { req.page_size as usize }; let offset = if req.page_token.is_empty() { 0 } else { let decoded = base64::engine::general_purpose::STANDARD .decode(&req.page_token) .map_err(|_| Status::invalid_argument("invalid page_token"))?; let offset_str = String::from_utf8(decoded) .map_err(|_| Status::invalid_argument("invalid page_token encoding"))?; offset_str .parse::() .map_err(|_| Status::invalid_argument("invalid page_token format"))? }; let total = certificates.len(); let end = std::cmp::min(offset + page_size, total); let paginated = certificates.iter().skip(offset).take(page_size); let proto_certs: Vec = paginated.map(certificate_to_proto).collect(); let next_page_token = if end < total { base64::engine::general_purpose::STANDARD.encode(end.to_string().as_bytes()) } else { String::new() }; Ok(Response::new(ListCertificatesResponse { certificates: proto_certs, next_page_token, })) } async fn delete_certificate( &self, request: Request, ) -> Result, Status> { let tenant = get_tenant_context(&request)?; let req = request.into_inner(); if req.id.is_empty() { return Err(Status::invalid_argument("id is required")); } let cert_id = parse_certificate_id(&req.id)?; // Load certificate to verify it exists let certificate = self .metadata .find_certificate_by_id(&cert_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("certificate not found"))?; let lb = self .metadata .load_lb_by_id(&certificate.loadbalancer_id) .await .map_err(|e| Status::internal(format!("metadata error: {}", e)))? .ok_or_else(|| Status::not_found("load balancer not found"))?; if lb.org_id != tenant.org_id || lb.project_id != tenant.project_id { return Err(Status::permission_denied("load balancer not in tenant scope")); } self.auth .authorize( &tenant, ACTION_CERTS_DELETE, &resource_for_tenant( "certificate", &certificate.id.to_string(), &lb.org_id, &lb.project_id, ), ) .await?; // Delete certificate self.metadata .delete_certificate(&certificate) .await .map_err(|e| Status::internal(format!("failed to delete certificate: {}", e)))?; Ok(Response::new(DeleteCertificateResponse {})) } }