Skip to main content

secrets_core/
lease.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5use crate::storage::{StorageBackend, StorageEntry, StorageError};
6
7const LEASE_PREFIX: &str = "sys/leases/";
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Lease {
11    pub id: Uuid,
12    pub token_id_hash: String,
13    /// The `EngineMount::prefix` (see `router.rs`) that owns this lease and
14    /// must be asked to `revoke()` it.
15    pub engine_mount: String,
16    pub internal_data: serde_json::Value,
17    pub issued_at: DateTime<Utc>,
18    pub expires_at: DateTime<Utc>,
19}
20
21pub async fn store_lease(storage: &dyn StorageBackend, lease: &Lease) -> Result<(), StorageError> {
22    let path = format!("{LEASE_PREFIX}{}", lease.id);
23    let value = serde_json::to_vec(lease).map_err(|e| StorageError::Backend(e.to_string()))?;
24    storage
25        .put(
26            &path,
27            StorageEntry {
28                value,
29                expires_at: Some(lease.expires_at),
30            },
31        )
32        .await
33}
34
35pub async fn get_lease(storage: &dyn StorageBackend, id: Uuid) -> Result<Option<Lease>, StorageError> {
36    let path = format!("{LEASE_PREFIX}{id}");
37    let Some(entry) = storage.get(&path).await? else {
38        return Ok(None);
39    };
40    let lease: Lease =
41        serde_json::from_slice(&entry.value).map_err(|e| StorageError::Backend(e.to_string()))?;
42    Ok(Some(lease))
43}
44
45/// Only the leases whose expiry has passed, asked of the backend rather than
46/// filtered in memory. This is the reaper's hot path.
47pub async fn list_expired_leases(
48    storage: &dyn StorageBackend,
49    now: DateTime<Utc>,
50) -> Result<Vec<Lease>, StorageError> {
51    let keys = storage.list_expired(LEASE_PREFIX, now).await?;
52    let mut leases = Vec::with_capacity(keys.len());
53    for key in keys {
54        if let Some(entry) = storage.get(&key).await?
55            && let Ok(lease) = serde_json::from_slice::<Lease>(&entry.value)
56        {
57            leases.push(lease);
58        }
59    }
60    Ok(leases)
61}
62
63pub async fn delete_lease(storage: &dyn StorageBackend, id: Uuid) -> Result<(), StorageError> {
64    storage.delete(&format!("{LEASE_PREFIX}{id}")).await
65}
66
67/// Lists every lease currently persisted. Fine at v1 scale (single node, no
68/// distributed lock needed); a busier deployment would want this paginated
69/// or index-scanned by `expires_at` instead.
70pub async fn list_leases(storage: &dyn StorageBackend) -> Result<Vec<Lease>, StorageError> {
71    let keys = storage.list(LEASE_PREFIX).await?;
72    let mut leases = Vec::with_capacity(keys.len());
73    for key in keys {
74        if let Some(entry) = storage.get(&key).await?
75            && let Ok(lease) = serde_json::from_slice::<Lease>(&entry.value)
76        {
77            leases.push(lease);
78        }
79    }
80    Ok(leases)
81}