337 lines
11 KiB
Rust
337 lines
11 KiB
Rust
//! 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<LbMetadataStore>,
|
|
auth: Arc<AuthService>,
|
|
}
|
|
|
|
impl CertificateServiceImpl {
|
|
/// Create a new CertificateServiceImpl
|
|
pub fn new(metadata: Arc<LbMetadataStore>, auth: Arc<AuthService>) -> 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<CertificateId, Status> {
|
|
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<LoadBalancerId, Status> {
|
|
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<CreateCertificateRequest>,
|
|
) -> Result<Response<CreateCertificateResponse>, 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<GetCertificateRequest>,
|
|
) -> Result<Response<GetCertificateResponse>, 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<ListCertificatesRequest>,
|
|
) -> Result<Response<ListCertificatesResponse>, 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::<usize>()
|
|
.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<ProtoCertificate> = 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<DeleteCertificateRequest>,
|
|
) -> Result<Response<DeleteCertificateResponse>, 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 {}))
|
|
}
|
|
}
|