gloves 0.5.11

seamless secret manager and handoff
Documentation
use std::collections::HashSet;
use std::path::Path;

use chrono::{Duration, Utc};
use ed25519_dalek::SigningKey;
use uuid::Uuid;

use crate::{
    agent::{backend::AgentBackend, meta::MetadataStore},
    audit::{AuditEvent, AuditLog},
    error::{GlovesError, Result},
    human::{backend::HumanBackend, pending::PendingRequestStore},
    types::{AgentId, Owner, PendingRequest, SecretId, SecretMeta, SecretValue},
};

/// Aggregated item returned from listing API.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ListItem {
    /// One secret metadata entry.
    Secret(SecretMeta),
    /// One pending human request.
    Pending(PendingRequest),
}

/// Unified router over agent and human backends.
pub struct SecretsManager {
    /// Agent backend.
    pub agent_backend: AgentBackend,
    /// Human backend.
    pub human_backend: HumanBackend,
    /// Secret metadata store.
    pub metadata_store: MetadataStore,
    /// Pending request store.
    pub pending_store: PendingRequestStore,
    /// Audit writer.
    pub audit_log: AuditLog,
}

/// Input options for storing a new secret.
pub struct SetSecretOptions {
    /// Secret owner domain.
    pub owner: Owner,
    /// Secret lifetime. `None` means the secret never expires.
    pub ttl: Option<Duration>,
    /// Creator identity.
    pub created_by: AgentId,
    /// Authorized recipients by logical id.
    pub recipients: HashSet<AgentId>,
    /// Recipient public keys in age format.
    pub recipient_keys: Vec<String>,
}

impl SecretsManager {
    /// Creates a new manager.
    pub fn new(
        agent_backend: AgentBackend,
        human_backend: HumanBackend,
        metadata_store: MetadataStore,
        pending_store: PendingRequestStore,
        audit_log: AuditLog,
    ) -> Self {
        Self {
            agent_backend,
            human_backend,
            metadata_store,
            pending_store,
            audit_log,
        }
    }

    /// Stores an agent secret and metadata.
    pub fn set(
        &self,
        secret_id: SecretId,
        secret_value: SecretValue,
        options: SetSecretOptions,
    ) -> Result<SecretId> {
        if options.owner != Owner::Agent {
            return Err(GlovesError::Forbidden);
        }

        self.agent_backend
            .encrypt(&secret_id, &secret_value, options.recipient_keys)?;
        let checksum = self.agent_backend.ciphertext_checksum(&secret_id)?;

        let now = Utc::now();
        let meta = SecretMeta {
            id: secret_id.clone(),
            owner: options.owner,
            created_at: now,
            expires_at: options.ttl.map(|ttl| now + ttl),
            recipients: options.recipients,
            created_by: options.created_by.clone(),
            last_accessed: None,
            access_count: 0,
            checksum,
        };
        if let Err(error) = self.metadata_store.save(&meta) {
            let _ = self.agent_backend.delete(&secret_id);
            return Err(error);
        }
        if let Err(error) = self.audit_log.log(AuditEvent::SecretCreated {
            secret_id: secret_id.clone(),
            by: options.created_by,
        }) {
            let _ = self.metadata_store.delete(&secret_id);
            let _ = self.agent_backend.delete(&secret_id);
            return Err(error);
        }
        Ok(secret_id)
    }

    /// Gets a secret by id and caller identity.
    pub fn get(
        &self,
        secret_id: &SecretId,
        caller: &AgentId,
        caller_identity_file: Option<&Path>,
    ) -> Result<SecretValue> {
        let mut meta = self.metadata_store.load(secret_id)?;

        if matches!(meta.expires_at, Some(expires_at) if expires_at <= Utc::now()) {
            return Err(GlovesError::Expired);
        }

        let secret = match meta.owner {
            Owner::Agent => {
                if !meta.recipients.contains(caller) {
                    return Err(GlovesError::Unauthorized);
                }
                if !meta.checksum.is_empty() {
                    let actual_checksum = self.agent_backend.ciphertext_checksum(secret_id)?;
                    if actual_checksum != meta.checksum {
                        return Err(GlovesError::IntegrityViolation);
                    }
                }

                let identity_file = caller_identity_file.ok_or(GlovesError::Unauthorized)?;
                self.agent_backend.decrypt(secret_id, identity_file)?
            }
            Owner::Human => {
                if !self.pending_store.is_fulfilled(secret_id, caller)? {
                    return Err(GlovesError::Forbidden);
                }
                self.human_backend.get(secret_id.as_str())?
            }
        };

        meta.last_accessed = Some(Utc::now());
        meta.access_count += 1;
        self.metadata_store.save(&meta)?;
        self.audit_log.log(AuditEvent::SecretAccessed {
            secret_id: secret_id.clone(),
            by: caller.clone(),
        })?;
        Ok(secret)
    }

    /// Creates a pending human request.
    pub fn request(
        &self,
        secret_id: SecretId,
        requested_by: AgentId,
        reason: String,
        ttl: Duration,
        signing_key: &SigningKey,
    ) -> Result<PendingRequest> {
        let request =
            self.pending_store
                .create(secret_id, requested_by, reason, ttl, signing_key)?;
        let _ = self.audit_log.log(AuditEvent::RequestCreated {
            request_id: request.id,
            secret_id: request.secret_name.clone(),
            requested_by: request.requested_by.clone(),
            reason: request.reason.clone(),
            expires_at: request.expires_at,
        });
        Ok(request)
    }

    /// Grants access to an agent secret by adding a recipient.
    pub fn grant(
        &self,
        secret_id: &SecretId,
        granter: &AgentId,
        granter_identity_file: &Path,
        new_recipient: AgentId,
        all_recipient_keys: &[String],
    ) -> Result<()> {
        let mut meta = self.metadata_store.load(secret_id)?;
        if meta.owner != Owner::Agent {
            return Err(GlovesError::Forbidden);
        }
        if meta.created_by != *granter {
            return Err(GlovesError::Forbidden);
        }

        meta.recipients.insert(new_recipient);
        self.agent_backend.grant(
            secret_id,
            granter_identity_file,
            all_recipient_keys.to_vec(),
        )?;
        meta.checksum = self.agent_backend.ciphertext_checksum(secret_id)?;
        self.metadata_store.save(&meta)
    }

    /// Revokes a secret owned by caller.
    pub fn revoke(&self, secret_id: &SecretId, caller: &AgentId) -> Result<()> {
        let meta = self.metadata_store.load(secret_id)?;
        if meta.created_by != *caller {
            return Err(GlovesError::Forbidden);
        }
        self.agent_backend.delete(secret_id)?;
        self.metadata_store.delete(secret_id)?;
        self.audit_log.log(AuditEvent::SecretRevoked {
            secret_id: secret_id.clone(),
            by: caller.clone(),
        })?;
        Ok(())
    }

    /// Lists secrets and pending requests.
    pub fn list_all(&self) -> Result<Vec<ListItem>> {
        let mut entries = self
            .metadata_store
            .list()?
            .into_iter()
            .map(ListItem::Secret)
            .collect::<Vec<_>>();
        entries.extend(
            self.pending_store
                .load_all()?
                .into_iter()
                .map(ListItem::Pending),
        );
        Ok(entries)
    }

    /// Approves a pending human request.
    pub fn approve_request(
        &self,
        request_id: Uuid,
        approved_by: AgentId,
    ) -> Result<PendingRequest> {
        let request = self
            .pending_store
            .approve(request_id, approved_by.clone())?;
        let _ = self.audit_log.log(AuditEvent::RequestApproved {
            request_id: request.id,
            secret_id: request.secret_name.clone(),
            requested_by: request.requested_by.clone(),
            approved_by,
        });
        Ok(request)
    }

    /// Denies a pending human request.
    pub fn deny_request(&self, request_id: Uuid, denied_by: AgentId) -> Result<PendingRequest> {
        let request = self.pending_store.deny(request_id, denied_by.clone())?;
        let _ = self.audit_log.log(AuditEvent::RequestDenied {
            request_id: request.id,
            secret_id: request.secret_name.clone(),
            requested_by: request.requested_by.clone(),
            denied_by,
        });
        Ok(request)
    }
}