use std::ops::ControlFlow;
use std::path::PathBuf;
use auths_id::error::StorageError;
use auths_verifier::core::{Attestation, VerifiedAttestation};
use auths_verifier::types::DeviceDID;
#[cfg(feature = "indexed-storage")]
use auths_verifier::types::IdentityDID;
use auths_id::attestation::AttestationSink;
use auths_id::storage::attestation::AttestationSource;
use super::adapter::GitRegistryBackend;
use super::config::RegistryConfig;
use auths_id::ports::registry::RegistryBackend;
pub struct RegistryAttestationStorage {
backend: GitRegistryBackend,
}
impl RegistryAttestationStorage {
pub fn new(repo_path: impl Into<PathBuf>) -> Self {
let backend =
GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(repo_path));
Self { backend }
}
pub fn init_if_needed(&self) -> Result<(), StorageError> {
self.backend
.init_if_needed()
.map(|_| ())
.map_err(|e| StorageError::InvalidData(format!("Failed to initialize registry: {}", e)))
}
pub fn backend(&self) -> &GitRegistryBackend {
&self.backend
}
}
impl AttestationSource for RegistryAttestationStorage {
fn load_attestations_for_device(
&self,
device_did: &DeviceDID,
) -> Result<Vec<Attestation>, StorageError> {
let mut attestations = Vec::new();
self.backend
.visit_attestation_history(device_did, &mut |att| {
attestations.push(att.clone());
ControlFlow::Continue(())
})
.map_err(|e| {
StorageError::InvalidData(format!("Failed to load attestation history: {}", e))
})?;
if attestations.is_empty()
&& let Ok(Some(att)) = self.backend.load_attestation(device_did)
{
attestations.push(att);
}
Ok(attestations)
}
fn load_all_attestations(&self) -> Result<Vec<Attestation>, StorageError> {
self.load_all_attestations_paginated(usize::MAX, 0)
}
fn load_all_attestations_paginated(
&self,
limit: usize,
offset: usize,
) -> Result<Vec<Attestation>, StorageError> {
let mut all_attestations = Vec::new();
let devices = self.discover_device_dids()?;
for device_did in devices.into_iter().skip(offset).take(limit) {
match self.load_attestations_for_device(&device_did) {
Ok(device_attestations) => {
all_attestations.extend(device_attestations);
}
Err(e) => {
log::warn!(
"Failed to load attestations for device {}: {}",
device_did,
e
);
}
}
}
Ok(all_attestations)
}
fn discover_device_dids(&self) -> Result<Vec<DeviceDID>, StorageError> {
let mut devices = Vec::new();
self.backend
.visit_devices(&mut |did| {
devices.push(did.clone());
ControlFlow::Continue(())
})
.map_err(|e| StorageError::InvalidData(format!("Failed to discover devices: {}", e)))?;
Ok(devices)
}
}
impl AttestationSink for RegistryAttestationStorage {
fn export(&self, attestation: &VerifiedAttestation) -> Result<(), StorageError> {
self.backend
.store_attestation(attestation.inner())
.map_err(|e| StorageError::InvalidData(format!("Failed to store attestation: {}", e)))
}
fn sync_index(&self, attestation: &Attestation) {
#[cfg(feature = "indexed-storage")]
{
use auths_id::storage::layout::StorageLayoutConfig;
use auths_index::{AttestationIndex, IndexedAttestation};
let config = StorageLayoutConfig::default();
let index_path = self.backend.repo_path().join(".auths-index.db");
let index = match AttestationIndex::open_or_create(&index_path) {
Ok(idx) => idx,
Err(e) => {
log::info!(
"Could not open index (this is OK if index hasn't been created yet): {}",
e
);
return;
}
};
let device_did_sanitized = attestation.subject.to_string().replace([':', '/'], "_");
let git_ref = format!(
"{}/{}/signatures",
config.device_attestation_prefix, device_did_sanitized
);
#[allow(clippy::disallowed_methods)]
let indexed = IndexedAttestation {
rid: attestation.rid.clone(),
issuer_did: IdentityDID::new_unchecked(attestation.issuer.as_str()),
device_did: attestation.subject.clone(),
git_ref,
commit_oid: None,
revoked_at: attestation.revoked_at,
expires_at: attestation.expires_at,
updated_at: attestation.timestamp.unwrap_or_else(chrono::Utc::now),
};
if let Err(e) = index.upsert_attestation(&indexed) {
log::warn!("Failed to update index: {}", e);
} else {
log::info!("Updated index for attestation {}", attestation.rid);
}
}
#[cfg(not(feature = "indexed-storage"))]
let _ = attestation;
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use git2::Repository;
use tempfile::TempDir;
fn setup_test_repo() -> (TempDir, RegistryAttestationStorage) {
let dir = TempDir::new().unwrap();
Repository::init(dir.path()).unwrap();
let storage = RegistryAttestationStorage::new(dir.path());
storage.init_if_needed().unwrap();
(dir, storage)
}
fn create_test_attestation(
subject: &str,
revoked_at: Option<chrono::DateTime<chrono::Utc>>,
) -> Attestation {
use auths_verifier::AttestationBuilder;
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
AttestationBuilder::default()
.rid(format!("test-rid-{}", seq))
.subject(subject)
.revoked_at(revoked_at)
.timestamp(Some(
chrono::Utc::now() + chrono::Duration::seconds(seq as i64),
))
.build()
}
#[test]
fn test_discover_device_dids_empty() {
let (_dir, storage) = setup_test_repo();
let devices = storage.discover_device_dids().unwrap();
assert!(devices.is_empty());
}
#[test]
fn test_store_and_load_attestation() {
let (_dir, storage) = setup_test_repo();
let att = create_test_attestation("did:key:zTestDevice1", None);
storage
.export(&VerifiedAttestation::dangerous_from_unchecked(att.clone()))
.unwrap();
let devices = storage.discover_device_dids().unwrap();
assert_eq!(devices.len(), 1);
assert_eq!(devices[0].to_string(), "did:key:zTestDevice1");
let loaded = storage.load_attestations_for_device(&devices[0]).unwrap();
assert_eq!(loaded.len(), 1);
assert_eq!(loaded[0].subject, att.subject);
}
#[test]
fn test_load_all_attestations() {
let (_dir, storage) = setup_test_repo();
let att1 = create_test_attestation("did:key:zDevice1", None);
let att2 = create_test_attestation("did:key:zDevice2", None);
storage
.export(&VerifiedAttestation::dangerous_from_unchecked(att1))
.unwrap();
storage
.export(&VerifiedAttestation::dangerous_from_unchecked(att2))
.unwrap();
let all = storage.load_all_attestations().unwrap();
assert_eq!(all.len(), 2);
}
#[test]
fn test_attestation_history() {
let (_dir, storage) = setup_test_repo();
let device_did = DeviceDID::new_unchecked("did:key:zHistoryDevice");
let att1 = create_test_attestation("did:key:zHistoryDevice", None);
storage
.export(&VerifiedAttestation::dangerous_from_unchecked(att1))
.unwrap();
let att2 = create_test_attestation("did:key:zHistoryDevice", Some(chrono::Utc::now()));
storage
.export(&VerifiedAttestation::dangerous_from_unchecked(att2))
.unwrap();
let loaded = storage.load_attestations_for_device(&device_did).unwrap();
assert!(!loaded.is_empty());
assert!(loaded.last().unwrap().is_revoked());
}
}