Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/auth/validation/
entry.rs

1//! Core entry validation for authentication
2//!
3//! This module provides the main entry point for validating entries
4//! and the AuthValidator struct that coordinates all validation operations.
5
6use std::collections::HashMap;
7
8use tracing::debug;
9
10use super::resolver::KeyResolver;
11use crate::{
12    Entry, Instance, Result,
13    auth::{
14        crypto::verify_entry_signature,
15        settings::AuthSettings,
16        types::{KeyStatus, Operation, ResolvedAuth, SigKey},
17    },
18    constants::SETTINGS,
19};
20
21/// Authentication validator for validating entries and resolving auth information
22pub struct AuthValidator {
23    /// Cache for resolved authentication data to improve performance
24    auth_cache: HashMap<String, ResolvedAuth>,
25    /// Key resolver for handling key resolution
26    pub(crate) resolver: KeyResolver,
27}
28
29impl AuthValidator {
30    /// Create a new authentication validator
31    pub fn new() -> Self {
32        Self {
33            auth_cache: HashMap::new(),
34            resolver: KeyResolver::new(),
35        }
36    }
37
38    /// Validate an entry's authentication
39    ///
40    /// This method answers: "Is this entry valid?" which includes:
41    /// 1. Is the signature valid (or is unsigned allowed)?
42    /// 2. Does the signing key have permission for what this entry does?
43    ///
44    /// For entries with name hints that match multiple keys, this method
45    /// tries signature verification against each matching key until one succeeds.
46    ///
47    /// # Returns
48    /// - `Ok(true)` - Entry is valid (signature verified with sufficient permissions, or unsigned allowed)
49    /// - `Ok(false)` - Entry is invalid (malformed, bad signature, insufficient permissions, etc.)
50    /// - `Err(...)` - Actual error (I/O, database failures)
51    ///
52    /// # Arguments
53    /// * `entry` - The entry to validate
54    /// * `auth_settings` - Authentication settings for key lookup
55    /// * `instance` - Instance for loading delegated trees (optional for direct keys)
56    pub async fn validate_entry(
57        &mut self,
58        entry: &Entry,
59        auth_settings: &AuthSettings,
60        instance: Option<&Instance>,
61    ) -> Result<bool> {
62        // Malformed entries fail validation
63        if entry.sig.malformed_reason().is_some() {
64            debug!("Malformed entry detected");
65            return Ok(false);
66        }
67
68        // Check if auth is configured (keys or global permission)
69        let has_auth =
70            !auth_settings.get_all_keys()?.is_empty() || auth_settings.has_global_permission();
71
72        // Handle unsigned entries
73        if entry.sig.is_unsigned() {
74            if has_auth {
75                // Auth is configured but entry is unsigned - invalid
76                debug!("Unsigned entry in authenticated database");
77                return Ok(false);
78            }
79            // No auth configured, unsigned is valid
80            debug!("Unsigned entry allowed (no auth configured)");
81            return Ok(true);
82        }
83
84        // Entry is signed but no auth configured - invalid
85        if !has_auth {
86            debug!("Signed entry but no auth configured");
87            return Ok(false);
88        }
89
90        // Resolve all matching keys
91        let resolved_auths = match self
92            .resolver
93            .resolve_sig_key(&entry.sig.key, auth_settings, instance)
94            .await
95        {
96            Ok(auths) => auths,
97            Err(e) => {
98                debug!("Key resolution failed: {:?}", e);
99                return Ok(false);
100            }
101        };
102
103        // FIXME(security): The claimed tips in a delegation SigKey are mostly
104        // unused. DelegationResolver::validate_tip_ancestry only checks that
105        // claimed tips exist as entries in the backend (not even tree-scoped),
106        // and then the code loads the delegated tree's *current* auth settings
107        // regardless of what tips were claimed. The claimed tips should instead
108        // determine which auth settings snapshot is used for resolution, so that
109        // permissions are evaluated at the state the signer actually observed.
110
111        // Determine operation type from entry content
112        let operation = if entry.subtrees().contains(&SETTINGS.to_string()) {
113            Operation::WriteSettings
114        } else {
115            Operation::WriteData
116        };
117
118        // Try signature verification + permission check against each candidate
119        for resolved_auth in resolved_auths {
120            // Skip keys that are not active
121            if resolved_auth.key_status != KeyStatus::Active {
122                debug!("Skipping inactive key: {:?}", resolved_auth.key_status);
123                continue;
124            }
125
126            // Try to verify the signature with this key
127            if verify_entry_signature(entry, &resolved_auth.public_key).is_ok() {
128                debug!("Signature verified, checking permissions");
129                // Signature verified - now check permissions
130                if self.check_permissions(&resolved_auth, &operation)? {
131                    debug!("Entry valid: signature verified with sufficient permissions");
132                    return Ok(true);
133                }
134                debug!("Signature valid but insufficient permissions, trying next key");
135                // Continue to try other keys that might have higher permissions
136            }
137        }
138
139        // No key verified with sufficient permissions
140        debug!("Entry invalid: no key verified with sufficient permissions");
141        Ok(false)
142    }
143
144    /// Resolve authentication identifier to concrete authentication information
145    ///
146    /// Returns all matching ResolvedAuth entries. For name hints that match
147    /// multiple keys, all matches are returned so the caller can try signature
148    /// verification against each.
149    ///
150    /// # Arguments
151    /// * `sig_key` - The signature key identifier to resolve
152    /// * `auth_settings` - Authentication settings containing auth configuration
153    /// * `instance` - Instance for loading delegated trees (required for Delegation sig_key)
154    pub async fn resolve_sig_key(
155        &mut self,
156        sig_key: &SigKey,
157        auth_settings: &AuthSettings,
158        instance: Option<&Instance>,
159    ) -> Result<Vec<ResolvedAuth>> {
160        // Delegate to the resolver
161        self.resolver
162            .resolve_sig_key(sig_key, auth_settings, instance)
163            .await
164    }
165
166    /// Check if a resolved authentication has sufficient permissions for an operation
167    pub fn check_permissions(
168        &self,
169        resolved: &ResolvedAuth,
170        operation: &Operation,
171    ) -> Result<bool> {
172        super::permissions::check_permissions(resolved, operation)
173    }
174
175    /// Clear the authentication cache
176    pub fn clear_cache(&mut self) {
177        self.auth_cache.clear();
178        self.resolver.clear_cache();
179    }
180}
181
182impl Default for AuthValidator {
183    fn default() -> Self {
184        Self::new()
185    }
186}