Skip to main content

krishiv_plan/
governance.rs

1#![forbid(unsafe_code)]
2//! Minimal authentication and access-control interfaces for Krishiv.
3
4// ─── AuthProvider ─────────────────────────────────────────────────────────────
5
6/// Authenticate an API key and return the subject string, if known.
7pub trait AuthProvider: Send + Sync {
8    /// Return `Some(subject)` if the key is valid, `None` otherwise.
9    fn authenticate(&self, api_key: &str) -> Option<String>;
10}
11
12/// API-key → subject mapping loaded from configuration.
13pub struct StaticApiKeyAuthProvider {
14    keys: std::collections::HashMap<String, String>,
15}
16
17impl StaticApiKeyAuthProvider {
18    /// Build from a map of `api_key -> subject` entries.
19    pub fn new(keys: std::collections::HashMap<String, String>) -> Self {
20        Self { keys }
21    }
22}
23
24impl AuthProvider for StaticApiKeyAuthProvider {
25    fn authenticate(&self, api_key: &str) -> Option<String> {
26        use constant_time_eq::constant_time_eq;
27        let candidate = api_key.as_bytes();
28        // Iterate every entry without short-circuiting so elapsed time is
29        // independent of which key matched — prevents timing oracle attacks.
30        let mut result: Option<String> = None;
31        for (stored, subject) in &self.keys {
32            if constant_time_eq(stored.as_bytes(), candidate) {
33                result = Some(subject.clone());
34            }
35        }
36        result
37    }
38}
39
40// ─── PolicyHook ───────────────────────────────────────────────────────────────
41
42/// Pluggable table-level access control hook.
43pub trait PolicyHook: Send + Sync {
44    /// Return `false` to deny access to the named table.
45    fn check_table_access(&self, table_name: &str) -> bool;
46}
47
48/// Allow-all policy hook (default for embedded and test contexts).
49pub struct AllowAllPolicyHook;
50
51impl PolicyHook for AllowAllPolicyHook {
52    fn check_table_access(&self, _table_name: &str) -> bool {
53        true
54    }
55}
56
57// ─── Tests ────────────────────────────────────────────────────────────────────
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn static_auth_provider_known_key() {
65        let mut keys = std::collections::HashMap::new();
66        keys.insert("key1".to_string(), "alice".to_string());
67        let provider = StaticApiKeyAuthProvider::new(keys);
68        let subject = provider.authenticate("key1");
69        assert_eq!(subject.as_deref(), Some("alice"));
70    }
71
72    #[test]
73    fn static_auth_provider_unknown_key() {
74        let mut keys = std::collections::HashMap::new();
75        keys.insert("key1".to_string(), "alice".to_string());
76        let provider = StaticApiKeyAuthProvider::new(keys);
77        assert!(provider.authenticate("unknown").is_none());
78    }
79
80    #[test]
81    fn static_auth_provider_no_prefix_timing_oracle() {
82        let mut keys = std::collections::HashMap::new();
83        keys.insert("secretXXX".to_string(), "alice".to_string());
84        let provider = StaticApiKeyAuthProvider::new(keys);
85        assert!(provider.authenticate("secret").is_none());
86        assert!(provider.authenticate("secretXXXextra").is_none());
87        assert!(provider.authenticate("secretXXX").is_some());
88        assert!(provider.authenticate("").is_none());
89    }
90
91    #[test]
92    fn allow_all_policy_hook_allows_all() {
93        let hook = AllowAllPolicyHook;
94        assert!(hook.check_table_access("any_table"));
95        assert!(hook.check_table_access("internal_accounts"));
96    }
97}