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}