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            auth_settings.get_global_permission().ok_or_else(|| {
48                Error::Auth(Box::new(AuthError::InvalidAuthConfiguration {
49                    reason: "Global '*' permission not configured".to_string(),
50                }))
51            })
52        }
53        SigKey::Direct { hint } => match (&hint.pubkey, &hint.name) {
54            (Some(claimed_pubkey), _) => {
55                if *claimed_pubkey != *pubkey {
56                    return Err(Error::Auth(Box::new(AuthError::SigningKeyMismatch {
57                        reason: format!("pubkey '{pubkey}' but identity claims '{claimed_pubkey}'"),
58                    })));
59                }
60                // Direct membership wins; otherwise fall back to the
61                // wildcard ('*') permission slot, which represents the
62                // tree's grant to "any key not otherwise listed". The
63                // caller already proved possession of `pubkey` (the
64                // connection's session keyset check on the wire path,
65                // signature verification on the local path), so accepting
66                // the wildcard level here is the structural intent of a
67                // global grant — no extra trust required.
68                if let Ok(auth_key) = auth_settings.get_key_by_pubkey(pubkey) {
69                    return Ok(*auth_key.permissions());
70                }
71                if let Some(global) = auth_settings.get_global_permission() {
72                    return Ok(global);
73                }
74                Err(Error::Auth(Box::new(AuthError::KeyNotFound {
75                    key_name: pubkey.to_string(),
76                })))
77            }
78            (_, Some(name)) => {
79                let matches = auth_settings.find_keys_by_name(name);
80                if matches.is_empty() {
81                    // Named identity with no direct match falls back to
82                    // the wildcard slot, same as the pubkey-only branch
83                    // — the structural intent matches.
84                    if let Some(global) = auth_settings.get_global_permission() {
85                        return Ok(global);
86                    }
87                    return Err(Error::Auth(Box::new(AuthError::KeyNotFound {
88                        key_name: name.clone(),
89                    })));
90                }
91                let pubkey_str = pubkey.to_string();
92                let (_, auth_key) = matches
93                    .iter()
94                    .find(|(pk, _)| *pk == pubkey_str)
95                    .ok_or_else(|| {
96                        Error::Auth(Box::new(AuthError::SigningKeyMismatch {
97                            reason: format!(
98                                "pubkey '{pubkey}' but no key named '{name}' has that pubkey"
99                            ),
100                        }))
101                    })?;
102                Ok(*auth_key.permissions())
103            }
104            _ => Err(Error::Auth(Box::new(AuthError::InvalidAuthConfiguration {
105                reason: "identity has empty hint".to_string(),
106            }))),
107        },
108        SigKey::Delegation { .. } => {
109            let mut validator = AuthValidator::new();
110            let resolved_auths = validator
111                .resolve_sig_key(identity, auth_settings, instance)
112                .await
113                .map_err(|e| {
114                    Error::Auth(Box::new(AuthError::InvalidAuthConfiguration {
115                        reason: format!("Delegation resolution failed: {e}"),
116                    }))
117                })?;
118
119            resolved_auths
120                .into_iter()
121                .find(|ra| ra.public_key == *pubkey)
122                .map(|ra| ra.effective_permission)
123                .ok_or_else(|| {
124                    Error::Auth(Box::new(AuthError::SigningKeyMismatch {
125                        reason: format!("no resolved delegation key matches pubkey '{pubkey}'"),
126                    }))
127                })
128        }
129    }
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}