Skip to content
This repository has been archived by the owner on Dec 5, 2024. It is now read-only.

Commit

Permalink
feat: Moved the CertificatesCache to its own module and added a test …
Browse files Browse the repository at this point in the history
…for it
  • Loading branch information
schoenenberg committed Sep 27, 2024
1 parent 0fd4ac5 commit ca71942
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 38 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ edition = "2021"
name = "ids-daps-client"
description = "A client to connect with the IDS DAPS."
version = "0.1.0"
license-file = "LICENSE"
license = "Apache-2.0"
repository = "https://github.com/truzzt/ids-daps-client-rs"
readme = "README.md"
authors = ["Maximilian Schoenenberg <[email protected]>"]
Expand All @@ -20,7 +20,7 @@ derive_builder = "0.20.0"
jsonwebtoken = "9.3.0"
openssl = "0.10.66"
reqwest = { version = "0.12.5", features = ["json", "http2"], optional = true}
serde = { version = "1.0.204", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
thiserror = "1"
tracing = "0.1.40"
url = "2.5.2"
Expand Down
84 changes: 84 additions & 0 deletions src/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
//! Cache for the certificates of the DAPS Client.
/// An error that can occur when accessing the `CertificatesCache`.
#[derive(thiserror::Error, Debug, PartialEq)]
pub enum CertificatesCacheError {
#[error("Cache is outdated")]
Outdated,
#[error("Cache is empty")]
Empty,
}

/// A cache for the certificates of the DAPS.
#[derive(Debug, Default)]
pub(crate) struct CertificatesCache {
/// The cache of certificates.
inner: async_lock::RwLock<Option<CertificatesCacheStorage>>,
/// The TTL of the cache.
ttl: std::time::Duration,
}

impl CertificatesCache {
#[must_use]
pub(crate) fn new(ttl: std::time::Duration) -> Self {
Self {
ttl,
..Default::default()
}
}

pub(crate) async fn get(&self) -> Result<jsonwebtoken::jwk::JwkSet, CertificatesCacheError> {
let cache = self.inner.read().await;
if let Some(cache) = &*cache {
if cache.stored + self.ttl < chrono::Utc::now() {
Err(CertificatesCacheError::Outdated)
} else {
Ok(cache.jwks.clone())
}
} else {
Err(CertificatesCacheError::Empty)
}
}

pub(crate) async fn update(
&self,
jwks: jsonwebtoken::jwk::JwkSet,
) -> Result<jsonwebtoken::jwk::JwkSet, CertificatesCacheError> {
let mut cache = self.inner.write().await;
let new_cache = CertificatesCacheStorage {
jwks: jwks.clone(),
stored: chrono::Utc::now(),
};
*cache = Some(new_cache);

Ok(jwks)
}
}

/// The storage of the cache.
#[derive(Debug)]
struct CertificatesCacheStorage {
/// Timestamp of the last storage of the certificates.
stored: chrono::DateTime<chrono::Utc>,
/// The cache of certificates.
jwks: jsonwebtoken::jwk::JwkSet,
}

#[cfg(test)]
mod test {

#[tokio::test]
async fn certificate_cache() {
use super::*;

let cache = CertificatesCache::new(std::time::Duration::from_secs(1));
assert_eq!(Err(CertificatesCacheError::Empty), cache.get().await);

let jwks = jsonwebtoken::jwk::JwkSet { keys: vec![] };
cache.update(jwks.clone()).await.unwrap();
assert_eq!(Ok(jwks), cache.get().await);

tokio::time::sleep(std::time::Duration::from_secs(2)).await;
assert_eq!(Err(CertificatesCacheError::Outdated), cache.get().await);
}
}
2 changes: 1 addition & 1 deletion src/cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub fn ski_aki<'a>(
}

#[cfg(test)]
mod tests {
mod test {
use super::*;

/// Loads a certificate and extracts the SKI:AKI
Expand Down
63 changes: 28 additions & 35 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
//! .private_key(std::path::Path::new("./testdata/connector-certificate.p12"))
//! .private_key_password(Some(std::borrow::Cow::from("Password1")))
//! .scope(std::borrow::Cow::from("idsc:IDS_CONNECTORS_ALL"))
//! .certs_cache_ttl(1)
//! .certs_cache_ttl(1_u64)
//! .build()
//! .expect("Failed to build DAPS-Config");
//!
Expand All @@ -49,7 +49,7 @@
//!
//! // Request a DAT token
//! let dat = client.request_dat().await?;
//! println!("DAT Token: {:?}", dat);
//! println!("DAT Token: {dat}");
//!
//! // Validate the DAT token
//! if client.validate_dat(&dat).await.is_ok() {
Expand All @@ -63,6 +63,7 @@
#![warn(rust_2024_compatibility, clippy::pedantic)]
#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]

mod cache;
pub mod cert;
mod http_client;

Expand Down Expand Up @@ -135,6 +136,8 @@ pub enum DapsError {
DapsHttpClient(#[from] http_client::DapsHttpClientError),
#[error("jwt error")]
InvalidToken,
#[error("cache error: {0}")]
CacheError(#[from] cache::CertificatesCacheError),
}

/// Configuration for the DAPS client.
Expand All @@ -152,7 +155,7 @@ pub struct DapsConfig<'a> {
/// The scope for the DAPS token.
scope: Cow<'a, str>,
/// The time-to-live for the certificates cache in seconds.
certs_cache_ttl: i64,
certs_cache_ttl: u64,
}

impl DapsConfigBuilder<'_> {
Expand Down Expand Up @@ -185,12 +188,6 @@ impl DapsConfigBuilder<'_> {
}
}

/// A cache for the certificates of the DAPS.
struct CertificatesCache {
stored: chrono::DateTime<chrono::Utc>,
jwks: jsonwebtoken::jwk::JwkSet,
}

/// An alias for the DAPS client using the Reqwest HTTP client.
pub type ReqwestDapsClient<'a> = DapsClient<'a, http_client::reqwest_client::ReqwestDapsClient>;

Expand All @@ -212,9 +209,7 @@ pub struct DapsClient<'a, C> {
/// The UUID context for the JWT. To generate ordered UUIDs (v7).
uuid_context: uuid::ContextV7,
/// A cache for the certificates of the DAPS.
certs_cache: async_lock::RwLock<CertificatesCache>,
/// TTL for the cache.
cache_ttl: chrono::Duration,
certs_cache: cache::CertificatesCache,
}

impl<C> DapsClient<'_, C>
Expand Down Expand Up @@ -242,11 +237,9 @@ where
token_url: config.token_url.to_string(),
encoding_key,
uuid_context: uuid::ContextV7::new(),
certs_cache: async_lock::RwLock::new(CertificatesCache {
stored: chrono::DateTime::from_timestamp(0, 0).expect("This is a valid timestamp"),
jwks: jsonwebtoken::jwk::JwkSet { keys: Vec::new() },
}),
cache_ttl: chrono::Duration::seconds(config.certs_cache_ttl),
certs_cache: cache::CertificatesCache::new(std::time::Duration::from_secs(
config.certs_cache_ttl,
)),
}
}

Expand Down Expand Up @@ -338,29 +331,29 @@ where
/// Updates the certificate cache with the Certificates requested from the DAPS.
async fn update_cert_cache(&self) -> Result<jsonwebtoken::jwk::JwkSet, DapsError> {
let jwks = self.client.get_certs(self.certs_url.as_ref()).await?;
let mut cache = self.certs_cache.write().await;
cache.jwks = jwks;
cache.stored = chrono::Utc::now();
tracing::debug!("Cache updated");
Ok(cache.jwks.clone())
self.certs_cache
.update(jwks.clone())
.await
.map_err(DapsError::from)
}

/// Returns the certificates from the cache or updates the cache if it is outdated.
async fn get_certs(&self) -> Result<jsonwebtoken::jwk::JwkSet, DapsError> {
let now = chrono::Utc::now();
tracing::debug!("Checking cache...");

let cache = self.certs_cache.read().await;
if cache.stored + self.cache_ttl < now {
tracing::info!("Cache is outdated, updating...");
// Cache is outdated, drop lock
drop(cache);

// Update cache & return jwks
self.update_cert_cache().await
} else {
tracing::debug!("Cache is up-to-date");
Ok(cache.jwks.clone())
match self.certs_cache.get().await {
Ok(cert) => {
tracing::debug!("Cache is up-to-date");
Ok(cert)
}
Err(cache::CertificatesCacheError::Outdated) => {
tracing::info!("Cache is outdated, updating...");
self.update_cert_cache().await
}
Err(cache::CertificatesCacheError::Empty) => {
tracing::info!("Cache is empty, updating...");
self.update_cert_cache().await
}
}
}
}
Expand Down Expand Up @@ -407,7 +400,7 @@ mod test {
.private_key(std::path::Path::new("./testdata/connector-certificate.p12"))
.private_key_password(Some(Cow::from("Password1")))
.scope(Cow::from("idsc:IDS_CONNECTORS_ALL"))
.certs_cache_ttl(1)
.certs_cache_ttl(1_u64)
.build()
.expect("Failed to build DAPS-Config");

Expand Down

0 comments on commit ca71942

Please sign in to comment.