hessra_api/
lib.rs

1//! # Hessra API
2//!
3//! HTTP client for Hessra authentication services.
4//!
5//! This crate provides a client for making HTTP requests to the Hessra
6//! authorization service. It supports both HTTP/1.1 and HTTP/3 (as an optional feature)
7//! and implements the OpenAPI specification for the Hessra service.
8//!
9//! ## Features
10//!
11//! - HTTP/1.1 client for Hessra services
12//! - Optional HTTP/3 support
13//! - Implementation of all Hessra API endpoints
14//! - Mutual TLS (mTLS) for secure client authentication
15
16use serde::{Deserialize, Serialize};
17use thiserror::Error;
18
19use hessra_config::{HessraConfig, Protocol};
20
21// Error type for the API client
22#[derive(Error, Debug)]
23pub enum ApiError {
24    #[error("HTTP client error: {0}")]
25    HttpClient(#[from] reqwest::Error),
26
27    #[error("SSL configuration error: {0}")]
28    SslConfig(String),
29
30    #[error("Invalid response: {0}")]
31    InvalidResponse(String),
32
33    #[error("Token request error: {0}")]
34    TokenRequest(String),
35
36    #[error("Token verification error: {0}")]
37    TokenVerification(String),
38
39    #[error("Service chain error: {0}")]
40    ServiceChain(String),
41
42    #[error("Internal error: {0}")]
43    Internal(String),
44}
45
46// Request and response structures
47/// Request payload for requesting an authorization token
48#[derive(Serialize, Deserialize)]
49pub struct TokenRequest {
50    /// The resource identifier to request authorization for
51    pub resource: String,
52    /// The operation to request authorization for
53    pub operation: String,
54}
55
56/// Request payload for verifying an authorization token
57#[derive(Serialize, Deserialize)]
58pub struct VerifyTokenRequest {
59    /// The authorization token to verify
60    pub token: String,
61    /// The subject identifier to verify against
62    pub subject: String,
63    /// The resource identifier to verify authorization against
64    pub resource: String,
65    /// The operation to verify authorization for
66    pub operation: String,
67}
68
69/// Response from a token request operation
70#[derive(Serialize, Deserialize)]
71pub struct TokenResponse {
72    /// Response message from the server
73    pub response_msg: String,
74    /// The issued token, if successful
75    pub token: Option<String>,
76}
77
78/// Response from a token verification operation
79#[derive(Serialize, Deserialize)]
80pub struct VerifyTokenResponse {
81    /// Response message from the server
82    pub response_msg: String,
83}
84
85/// Response from a public key request
86#[derive(Serialize, Deserialize)]
87pub struct PublicKeyResponse {
88    pub response_msg: String,
89    pub public_key: String,
90}
91
92/// Request payload for verifying a service chain token
93#[derive(Serialize, Deserialize)]
94pub struct VerifyServiceChainTokenRequest {
95    pub token: String,
96    pub subject: String,
97    pub resource: String,
98    pub component: Option<String>,
99}
100
101/// Base configuration for Hessra clients
102#[derive(Clone)]
103pub struct BaseConfig {
104    /// Base URL of the Hessra service (without protocol scheme)
105    pub base_url: String,
106    /// Optional port to connect to
107    pub port: Option<u16>,
108    /// mTLS private key in PEM format
109    pub mtls_key: String,
110    /// mTLS client certificate in PEM format
111    pub mtls_cert: String,
112    /// Server CA certificate in PEM format
113    pub server_ca: String,
114    /// Public key for token verification in PEM format
115    pub public_key: Option<String>,
116    /// Personal keypair for service chain attestation
117    pub personal_keypair: Option<String>,
118}
119
120impl BaseConfig {
121    /// Get the formatted base URL, with port if specified
122    pub fn get_base_url(&self) -> String {
123        match self.port {
124            Some(port) => format!("{}:{}", self.base_url, port),
125            None => self.base_url.clone(),
126        }
127    }
128}
129
130/// HTTP/1.1 client implementation
131pub struct Http1Client {
132    /// Base configuration
133    config: BaseConfig,
134    /// reqwest HTTP client with mTLS configured
135    client: reqwest::Client,
136}
137
138impl Http1Client {
139    /// Create a new HTTP/1.1 client with the given configuration
140    pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
141        // First try the simple approach with rustls-tls
142        // Create identity from combined PEM certificate and key
143        let identity_str = format!("{}{}", config.mtls_cert, config.mtls_key);
144
145        let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
146            ApiError::SslConfig(format!(
147                "Failed to create identity from certificate and key: {}",
148                e
149            ))
150        })?;
151
152        // Parse the CA certificate
153        let cert_der = reqwest::Certificate::from_pem(config.server_ca.as_bytes())
154            .map_err(|e| ApiError::SslConfig(format!("Failed to parse CA certificate: {}", e)))?;
155
156        // Build the reqwest client with mTLS configuration
157        let client = reqwest::ClientBuilder::new()
158            .use_rustls_tls() // Explicitly use rustls TLS
159            .identity(identity)
160            .add_root_certificate(cert_der)
161            .build()
162            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
163
164        Ok(Self { config, client })
165    }
166
167    /// Send a request to the Hessra service
168    pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
169    where
170        T: Serialize,
171        R: for<'de> Deserialize<'de>,
172    {
173        let base_url = self.config.get_base_url();
174        let url = format!("https://{}/{}", base_url, endpoint);
175
176        let response = self
177            .client
178            .post(&url)
179            .json(request_body)
180            .send()
181            .await
182            .map_err(ApiError::HttpClient)?;
183
184        if !response.status().is_success() {
185            let status = response.status();
186            let error_text = response.text().await.unwrap_or_default();
187            return Err(ApiError::InvalidResponse(format!(
188                "HTTP error: {} - {}",
189                status, error_text
190            )));
191        }
192
193        let result = response
194            .json::<R>()
195            .await
196            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
197
198        Ok(result)
199    }
200}
201
202/// HTTP/3 client implementation (only available with the "http3" feature)
203#[cfg(feature = "http3")]
204pub struct Http3Client {
205    /// Base configuration
206    config: BaseConfig,
207    /// QUIC endpoint for HTTP/3 connections
208    client: reqwest::Client,
209}
210
211#[cfg(feature = "http3")]
212impl Http3Client {
213    /// Create a new HTTP/3 client with the given configuration
214    pub fn new(config: BaseConfig) -> Result<Self, ApiError> {
215        // First try the simple approach with rustls-tls
216        // Create identity from combined PEM certificate and key
217        let identity_str = format!("{}{}", config.mtls_cert, config.mtls_key);
218
219        let identity = reqwest::Identity::from_pem(identity_str.as_bytes()).map_err(|e| {
220            ApiError::SslConfig(format!(
221                "Failed to create identity from certificate and key: {}",
222                e
223            ))
224        })?;
225
226        // Parse the CA certificate
227        let cert_der = reqwest::Certificate::from_pem(config.server_ca.as_bytes())
228            .map_err(|e| ApiError::SslConfig(format!("Failed to parse CA certificate: {}", e)))?;
229
230        // Build the reqwest client with mTLS configuration
231        let client = reqwest::ClientBuilder::new()
232            .use_rustls_tls() // Explicitly use rustls TLS
233            .http3_prior_knowledge()
234            .identity(identity)
235            .add_root_certificate(cert_der)
236            .build()
237            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
238
239        Ok(Self { config, client })
240    }
241
242    /// Send a request to the Hessra service
243    pub async fn send_request<T, R>(&self, endpoint: &str, request_body: &T) -> Result<R, ApiError>
244    where
245        T: Serialize,
246        R: for<'de> Deserialize<'de>,
247    {
248        let base_url = self.config.get_base_url();
249        let url = format!("https://{}/{}", base_url, endpoint);
250
251        let response = self
252            .client
253            .post(&url)
254            .version(http::Version::HTTP_3)
255            .json(request_body)
256            .send()
257            .await
258            .map_err(ApiError::HttpClient)?;
259
260        if !response.status().is_success() {
261            let status = response.status();
262            let error_text = response.text().await.unwrap_or_default();
263            return Err(ApiError::InvalidResponse(format!(
264                "HTTP error: {} - {}",
265                status, error_text
266            )));
267        }
268
269        let result = response
270            .json::<R>()
271            .await
272            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
273
274        Ok(result)
275    }
276}
277
278/// The main Hessra client type providing token request and verification
279pub enum HessraClient {
280    /// HTTP/1.1 client
281    Http1(Http1Client),
282    /// HTTP/3 client (only available with the "http3" feature)
283    #[cfg(feature = "http3")]
284    Http3(Http3Client),
285}
286
287/// Builder for creating Hessra clients
288pub struct HessraClientBuilder {
289    /// Base configuration being built
290    config: BaseConfig,
291    /// Protocol to use for the client
292    protocol: hessra_config::Protocol,
293}
294
295impl HessraClientBuilder {
296    /// Create a new client builder with default values
297    pub fn new() -> Self {
298        Self {
299            config: BaseConfig {
300                base_url: String::new(),
301                port: None,
302                mtls_key: String::new(),
303                mtls_cert: String::new(),
304                server_ca: String::new(),
305                public_key: None,
306                personal_keypair: None,
307            },
308            protocol: Protocol::Http1,
309        }
310    }
311
312    /// Create a client builder from a HessraConfig
313    pub fn from_config(mut self, config: &HessraConfig) -> Self {
314        self.config.base_url = config.base_url.clone();
315        self.config.port = config.port;
316        self.config.mtls_key = config.mtls_key.clone();
317        self.config.mtls_cert = config.mtls_cert.clone();
318        self.config.server_ca = config.server_ca.clone();
319        self.config.public_key = config.public_key.clone();
320        self.config.personal_keypair = config.personal_keypair.clone();
321        self.protocol = config.protocol.clone();
322        self
323    }
324
325    /// Set the base URL for the client
326    pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
327        self.config.base_url = base_url.into();
328        self
329    }
330
331    /// Set the mTLS private key for the client
332    pub fn mtls_key(mut self, mtls_key: impl Into<String>) -> Self {
333        self.config.mtls_key = mtls_key.into();
334        self
335    }
336
337    /// Set the mTLS certificate for the client
338    pub fn mtls_cert(mut self, mtls_cert: impl Into<String>) -> Self {
339        self.config.mtls_cert = mtls_cert.into();
340        self
341    }
342
343    /// Set the server CA certificate for the client
344    pub fn server_ca(mut self, server_ca: impl Into<String>) -> Self {
345        self.config.server_ca = server_ca.into();
346        self
347    }
348
349    /// Set the port for the client
350    pub fn port(mut self, port: u16) -> Self {
351        self.config.port = Some(port);
352        self
353    }
354
355    /// Set the protocol for the client
356    pub fn protocol(mut self, protocol: Protocol) -> Self {
357        self.protocol = protocol;
358        self
359    }
360
361    /// Set the public key for token verification
362    pub fn public_key(mut self, public_key: impl Into<String>) -> Self {
363        self.config.public_key = Some(public_key.into());
364        self
365    }
366
367    /// Set the personal keypair for service chain attestation
368    pub fn personal_keypair(mut self, keypair: impl Into<String>) -> Self {
369        self.config.personal_keypair = Some(keypair.into());
370        self
371    }
372
373    /// Build the HTTP/1.1 client
374    fn build_http1(&self) -> Result<Http1Client, ApiError> {
375        Http1Client::new(self.config.clone())
376    }
377
378    /// Build the HTTP/3 client
379    #[cfg(feature = "http3")]
380    fn build_http3(&self) -> Result<Http3Client, ApiError> {
381        Http3Client::new(self.config.clone())
382    }
383
384    /// Build the client
385    pub fn build(self) -> Result<HessraClient, ApiError> {
386        match self.protocol {
387            Protocol::Http1 => Ok(HessraClient::Http1(self.build_http1()?)),
388            #[cfg(feature = "http3")]
389            Protocol::Http3 => Ok(HessraClient::Http3(self.build_http3()?)),
390            #[allow(unreachable_patterns)]
391            _ => Err(ApiError::Internal("Unsupported protocol".to_string())),
392        }
393    }
394}
395
396impl Default for HessraClientBuilder {
397    fn default() -> Self {
398        Self::new()
399    }
400}
401
402impl HessraClient {
403    /// Create a new client builder
404    pub fn builder() -> HessraClientBuilder {
405        HessraClientBuilder::new()
406    }
407
408    /// Fetch the public key from the Hessra service without creating a client
409    pub async fn fetch_public_key(
410        base_url: impl Into<String>,
411        port: Option<u16>,
412        server_ca: impl Into<String>,
413    ) -> Result<String, ApiError> {
414        let base_url = base_url.into();
415        let server_ca = server_ca.into();
416
417        // Create a regular reqwest client (no mTLS)
418        let cert_pem = server_ca.as_bytes();
419        let cert_der = reqwest::Certificate::from_pem(cert_pem)
420            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
421
422        let client = reqwest::ClientBuilder::new()
423            .use_rustls_tls()
424            .add_root_certificate(cert_der)
425            .build()
426            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
427
428        // Format the URL
429        let url = match port {
430            Some(port) => format!("https://{}:{}/public_key", base_url, port),
431            None => format!("https://{}/public_key", base_url),
432        };
433
434        // Make the request
435        let response = client
436            .get(&url)
437            .send()
438            .await
439            .map_err(ApiError::HttpClient)?;
440
441        if !response.status().is_success() {
442            let status = response.status();
443            let error_text = response.text().await.unwrap_or_default();
444            return Err(ApiError::InvalidResponse(format!(
445                "HTTP error: {} - {}",
446                status, error_text
447            )));
448        }
449
450        // Parse the response
451        let result = response
452            .json::<PublicKeyResponse>()
453            .await
454            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
455
456        Ok(result.public_key)
457    }
458
459    #[cfg(feature = "http3")]
460    pub async fn fetch_public_key_http3(
461        base_url: impl Into<String>,
462        port: Option<u16>,
463        server_ca: impl Into<String>,
464    ) -> Result<String, ApiError> {
465        let base_url = base_url.into();
466        let server_ca = server_ca.into();
467
468        // Create a regular reqwest client (no mTLS)
469        let cert_pem = server_ca.as_bytes();
470        let cert_der = reqwest::Certificate::from_pem(cert_pem)
471            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
472
473        let client = reqwest::ClientBuilder::new()
474            .use_rustls_tls()
475            .add_root_certificate(cert_der)
476            .http3_prior_knowledge()
477            .build()
478            .map_err(|e| ApiError::SslConfig(e.to_string()))?;
479
480        // Format the URL
481        let url = match port {
482            Some(port) => format!("https://{}:{}/public_key", base_url, port),
483            None => format!("https://{}/public_key", base_url),
484        };
485
486        // Make the request
487        let response = client
488            .get(&url)
489            .version(http::Version::HTTP_3)
490            .send()
491            .await
492            .map_err(ApiError::HttpClient)?;
493
494        if !response.status().is_success() {
495            let status = response.status();
496            let error_text = response.text().await.unwrap_or_default();
497            return Err(ApiError::InvalidResponse(format!(
498                "HTTP error: {} - {}",
499                status, error_text
500            )));
501        }
502
503        // Parse the response
504        let result = response
505            .json::<PublicKeyResponse>()
506            .await
507            .map_err(|e| ApiError::InvalidResponse(format!("Failed to parse response: {}", e)))?;
508
509        Ok(result.public_key)
510    }
511
512    /// Request a token for a resource
513    pub async fn request_token(
514        &self,
515        resource: String,
516        operation: String,
517    ) -> Result<String, ApiError> {
518        let request = TokenRequest {
519            resource,
520            operation,
521        };
522
523        let response = match self {
524            HessraClient::Http1(client) => {
525                client
526                    .send_request::<_, TokenResponse>("request_token", &request)
527                    .await?
528            }
529            #[cfg(feature = "http3")]
530            HessraClient::Http3(client) => {
531                client
532                    .send_request::<_, TokenResponse>("request_token", &request)
533                    .await?
534            }
535        };
536
537        match response.token {
538            Some(token) => Ok(token),
539            None => Err(ApiError::TokenRequest(format!(
540                "Failed to get token: {}",
541                response.response_msg
542            ))),
543        }
544    }
545
546    /// Verify a token for a subject and resource
547    pub async fn verify_token(
548        &self,
549        token: String,
550        subject: String,
551        resource: String,
552        operation: String,
553    ) -> Result<String, ApiError> {
554        let request = VerifyTokenRequest {
555            token,
556            subject,
557            resource,
558            operation,
559        };
560
561        let response = match self {
562            HessraClient::Http1(client) => {
563                client
564                    .send_request::<_, VerifyTokenResponse>("verify_token", &request)
565                    .await?
566            }
567            #[cfg(feature = "http3")]
568            HessraClient::Http3(client) => {
569                client
570                    .send_request::<_, VerifyTokenResponse>("verify_token", &request)
571                    .await?
572            }
573        };
574
575        Ok(response.response_msg)
576    }
577
578    /// Verify a service chain token
579    pub async fn verify_service_chain_token(
580        &self,
581        token: String,
582        subject: String,
583        resource: String,
584        component: Option<String>,
585    ) -> Result<String, ApiError> {
586        let request = VerifyServiceChainTokenRequest {
587            token,
588            subject,
589            resource,
590            component,
591        };
592
593        let response = match self {
594            HessraClient::Http1(client) => {
595                client
596                    .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
597                    .await?
598            }
599            #[cfg(feature = "http3")]
600            HessraClient::Http3(client) => {
601                client
602                    .send_request::<_, VerifyTokenResponse>("verify_service_chain_token", &request)
603                    .await?
604            }
605        };
606
607        Ok(response.response_msg)
608    }
609
610    /// Get the public key from the server
611    pub async fn get_public_key(&self) -> Result<String, ApiError> {
612        let url_path = "public_key";
613
614        let response = match self {
615            HessraClient::Http1(client) => {
616                // For this endpoint, we just need a GET request, not a POST with a body
617                let base_url = client.config.get_base_url();
618                let full_url = format!("https://{}/{}", base_url, url_path);
619
620                let response = client
621                    .client
622                    .get(&full_url)
623                    .send()
624                    .await
625                    .map_err(ApiError::HttpClient)?;
626
627                if !response.status().is_success() {
628                    let status = response.status();
629                    let error_text = response.text().await.unwrap_or_default();
630                    return Err(ApiError::InvalidResponse(format!(
631                        "HTTP error: {} - {}",
632                        status, error_text
633                    )));
634                }
635
636                response.json::<PublicKeyResponse>().await.map_err(|e| {
637                    ApiError::InvalidResponse(format!("Failed to parse response: {}", e))
638                })?
639            }
640            #[cfg(feature = "http3")]
641            HessraClient::Http3(client) => {
642                let base_url = client.config.get_base_url();
643                let full_url = format!("https://{}/{}", base_url, url_path);
644
645                let response = client
646                    .client
647                    .get(&full_url)
648                    .version(http::Version::HTTP_3)
649                    .send()
650                    .await
651                    .map_err(ApiError::HttpClient)?;
652
653                if !response.status().is_success() {
654                    let status = response.status();
655                    let error_text = response.text().await.unwrap_or_default();
656                    return Err(ApiError::InvalidResponse(format!(
657                        "HTTP error: {} - {}",
658                        status, error_text
659                    )));
660                }
661
662                response.json::<PublicKeyResponse>().await.map_err(|e| {
663                    ApiError::InvalidResponse(format!("Failed to parse response: {}", e))
664                })?
665            }
666        };
667
668        Ok(response.public_key)
669    }
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675
676    // Test BaseConfig get_base_url method
677    #[test]
678    fn test_base_config_get_base_url_with_port() {
679        let config = BaseConfig {
680            base_url: "test.hessra.net".to_string(),
681            port: Some(443),
682            mtls_key: "".to_string(),
683            mtls_cert: "".to_string(),
684            server_ca: "".to_string(),
685            public_key: None,
686            personal_keypair: None,
687        };
688
689        assert_eq!(config.get_base_url(), "test.hessra.net:443");
690    }
691
692    #[test]
693    fn test_base_config_get_base_url_without_port() {
694        let config = BaseConfig {
695            base_url: "test.hessra.net".to_string(),
696            port: None,
697            mtls_key: "".to_string(),
698            mtls_cert: "".to_string(),
699            server_ca: "".to_string(),
700            public_key: None,
701            personal_keypair: None,
702        };
703
704        assert_eq!(config.get_base_url(), "test.hessra.net");
705    }
706
707    // Test HessraClientBuilder methods
708    #[test]
709    fn test_client_builder_methods() {
710        let builder = HessraClientBuilder::new()
711            .base_url("test.hessra.net")
712            .port(443)
713            .protocol(Protocol::Http1)
714            .mtls_cert("CERT")
715            .mtls_key("KEY")
716            .server_ca("CA")
717            .public_key("PUBKEY")
718            .personal_keypair("KEYPAIR");
719
720        assert_eq!(builder.config.base_url, "test.hessra.net");
721        assert_eq!(builder.config.port, Some(443));
722        assert_eq!(builder.config.mtls_cert, "CERT");
723        assert_eq!(builder.config.mtls_key, "KEY");
724        assert_eq!(builder.config.server_ca, "CA");
725        assert_eq!(builder.config.public_key, Some("PUBKEY".to_string()));
726        assert_eq!(builder.config.personal_keypair, Some("KEYPAIR".to_string()));
727    }
728}