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

eidetica/auth/validation/
permissions.rs

1//! Permission checking for authentication operations
2//!
3//! This module provides utilities for checking if resolved authentication
4//! has sufficient permissions for specific operations.
5
6use crate::{
7    Error, Result,
8    auth::{
9        crypto::PublicKey,
10        errors::AuthError,
11        settings::AuthSettings,
12        types::{Operation, Permission, ResolvedAuth, SigKey},
13        validation::AuthValidator,
14    },
15};
16
17/// Resolve the permission level for a pubkey + identity against auth settings.
18///
19/// Shared validation logic used by both the local path (`Database::validate_key`,
20/// which holds a `DatabaseKey` that bundles signing key + identity) and the
21/// remote path (the service server, which has the pubkey from the session
22/// challenge-response and the identity from the request's authenticated scope).
23///
24/// # Arguments
25/// * `pubkey` - The public key to validate
26/// * `identity` - The `SigKey` identity claiming access
27/// * `auth_settings` - The database's auth configuration
28/// * `instance` - Optional `Instance` for delegation resolution; required when
29///   `identity` is a `SigKey::Delegation`
30pub async fn resolve_identity_permission(
31    pubkey: &PublicKey,
32    identity: &SigKey,
33    auth_settings: &AuthSettings,
34    instance: Option<&crate::Instance>,
35) -> Result<Permission> {
36    match identity {
37        SigKey::Direct { hint } if hint.is_global() => {
38            if let Some(embedded_pubkey) = &hint.pubkey
39                && *embedded_pubkey != *pubkey
40            {
41                return Err(Error::Auth(Box::new(AuthError::SigningKeyMismatch {
42                    reason: format!(
43                        "pubkey '{pubkey}' but global identity claims '{embedded_pubkey}'"
44                    ),
45                })));
46            }
47            let global = auth_settings.get_global_key().map_err(|_| {
48                Error::Auth(Box::new(AuthError::InvalidAuthConfiguration {
49                    reason: "Global '*' permission not configured".to_string(),
50                }))
51            })?;
52            if !global.is_active() {
53                return Err(Error::Auth(Box::new(AuthError::InvalidAuthConfiguration {
54                    reason: "Global '*' permission is not active".to_string(),
55                })));
56            }
57            Ok(*global.permissions())
58        }
59        SigKey::Direct { hint } => {
60            // Anti-spoof: a pubkey-bearing hint must claim the proven key.
61            if let Some(claimed_pubkey) = &hint.pubkey
62                && *claimed_pubkey != *pubkey
63            {
64                return Err(Error::Auth(Box::new(AuthError::SigningKeyMismatch {
65                    reason: format!("pubkey '{pubkey}' but identity claims '{claimed_pubkey}'"),
66                })));
67            }
68            if hint.pubkey.is_none() && hint.name.is_none() {
69                return Err(Error::Auth(Box::new(AuthError::InvalidAuthConfiguration {
70                    reason: "identity has empty hint".to_string(),
71                })));
72            }
73            // Resolve the hint through the shared resolver, then take the
74            // highest active grant that belongs to the proven pubkey. Direct
75            // membership wins; otherwise fall back to the wildcard ('*') slot —
76            // the tree's grant to "any key not otherwise listed". The caller
77            // already proved possession of `pubkey` (session keyset check on
78            // the wire path, signature verification locally), so accepting the
79            // wildcard level is the structural intent of a global grant.
80            if let Ok(candidates) = auth_settings.resolve_hint(hint)
81                && let Some(permission) = select_effective_permission(&candidates, pubkey)
82            {
83                return Ok(permission);
84            }
85            if let Ok(global) = auth_settings.get_global_key()
86                && global.is_active()
87            {
88                return Ok(*global.permissions());
89            }
90            Err(Error::Auth(Box::new(AuthError::KeyNotFound {
91                key_name: hint.name.clone().unwrap_or_else(|| pubkey.to_string()),
92            })))
93        }
94        SigKey::Delegation { .. } => {
95            let mut validator = AuthValidator::new();
96            let resolved_auths = validator
97                .resolve_sig_key(identity, auth_settings, instance)
98                .await
99                .map_err(|e| {
100                    Error::Auth(Box::new(AuthError::InvalidAuthConfiguration {
101                        reason: format!("Delegation resolution failed: {e}"),
102                    }))
103                })?;
104
105            select_effective_permission(&resolved_auths, pubkey).ok_or_else(|| {
106                Error::Auth(Box::new(AuthError::SigningKeyMismatch {
107                    reason: format!("no active resolved delegation key matches pubkey '{pubkey}'"),
108                }))
109            })
110        }
111    }
112}
113
114/// Highest permission among resolved candidates that belong to `pubkey` and
115/// currently grant access.
116///
117/// The single place auth paths turn resolved candidates into an effective
118/// permission: resolvers return candidates regardless of key status, so this is
119/// where revocation is honoured — identically for direct, wildcard, and
120/// delegated authority. Returns `None` when no active candidate matches.
121pub(crate) fn select_effective_permission(
122    candidates: &[ResolvedAuth],
123    pubkey: &PublicKey,
124) -> Option<Permission> {
125    candidates
126        .iter()
127        .filter(|ra| ra.public_key == *pubkey && ra.grants_access())
128        .map(|ra| ra.effective_permission)
129        .max()
130}
131
132/// Check if a resolved authentication has sufficient permissions for an operation
133pub fn check_permissions(resolved: &ResolvedAuth, operation: &Operation) -> Result<bool> {
134    match operation {
135        Operation::WriteData => {
136            Ok(resolved.effective_permission.can_write()
137                || resolved.effective_permission.can_admin())
138        }
139        Operation::WriteSettings => Ok(resolved.effective_permission.can_admin()),
140    }
141}