Skip to main content

camel_auth/
registry.rs

1use crate::authn_cache::AuthnCache;
2use crate::permission::PermissionEvaluator;
3use crate::token_authenticator::TokenAuthenticator;
4use camel_api::security_policy::SecurityPolicy;
5use dashmap::DashMap;
6use std::sync::Arc;
7
8pub struct NamedRegistry<T: ?Sized> {
9    entries: DashMap<String, Arc<T>>,
10}
11
12impl<T: ?Sized> Default for NamedRegistry<T> {
13    fn default() -> Self {
14        Self::new()
15    }
16}
17
18impl<T: ?Sized> NamedRegistry<T> {
19    pub fn new() -> Self {
20        Self {
21            entries: DashMap::new(),
22        }
23    }
24
25    pub fn register(&self, name: impl Into<String>, entry: Arc<T>) {
26        self.entries.insert(name.into(), entry);
27    }
28
29    pub fn get(&self, name: &str) -> Option<Arc<T>> {
30        self.entries.get(name).map(|e| Arc::clone(&*e))
31    }
32
33    pub fn len(&self) -> usize {
34        self.entries.len()
35    }
36
37    pub fn entries(&self) -> Vec<(String, Arc<T>)> {
38        self.entries
39            .iter()
40            .map(|entry| (entry.key().clone(), Arc::clone(entry.value())))
41            .collect()
42    }
43
44    pub fn is_empty(&self) -> bool {
45        self.entries.is_empty()
46    }
47}
48
49pub type SecurityPolicyRegistry = NamedRegistry<dyn SecurityPolicy>;
50pub type PermissionEvaluatorRegistry = NamedRegistry<dyn PermissionEvaluator>;
51
52/// A single named authentication provider: its token authenticator plus the
53/// (reserved) audience binding. `audience_binding` is `None` in Phase 1 and is
54/// populated in Task 1.6.
55///
56/// Invariant: JWT-backed providers MUST populate `issuers` in their binding. A
57/// binding with `audiences` but empty `issuers` silently drops the validator's
58/// constructor-fixed issuer check on the kernel path (the request's non-empty
59/// audience set bypasses both constructor checks via REPLACEMENT semantics).
60pub struct ProviderEntry {
61    pub authenticator: Arc<dyn TokenAuthenticator>,
62    pub audience_binding: Option<camel_api::security_policy::AudienceBinding>,
63}
64
65/// Named registry of authentication providers.
66///
67/// Entries are stored as `Arc<ProviderEntry>`; [`ProviderRegistry::resolve`]
68/// clones the `Arc` out of the DashMap so callers hold their own strong
69/// reference independent of the map guard's lifetime.
70pub struct ProviderRegistry {
71    inner: NamedRegistry<ProviderEntry>,
72    authn_cache: Option<Arc<AuthnCache>>,
73}
74
75impl Default for ProviderRegistry {
76    fn default() -> Self {
77        Self::new()
78    }
79}
80
81impl ProviderRegistry {
82    pub fn new() -> Self {
83        Self {
84            inner: NamedRegistry::new(),
85            authn_cache: None,
86        }
87    }
88
89    /// Attach the authn result cache (Task 3.2). [`kernel_authenticate`]
90    /// consults it via [`Self::authn_cache`] before calling a provider.
91    ///
92    /// [`kernel_authenticate`]: crate::kernel::kernel_authenticate
93    pub fn with_authn_cache(mut self, cache: Arc<AuthnCache>) -> Self {
94        self.authn_cache = Some(cache);
95        self
96    }
97
98    /// The attached authn result cache, when present.
99    pub fn authn_cache(&self) -> Option<&Arc<AuthnCache>> {
100        self.authn_cache.as_ref()
101    }
102
103    pub fn register(&self, name: impl Into<String>, entry: ProviderEntry) {
104        self.inner.register(name, Arc::new(entry));
105    }
106
107    pub fn resolve(&self, name: &str) -> Option<Arc<ProviderEntry>> {
108        self.inner.get(name)
109    }
110
111    pub fn len(&self) -> usize {
112        self.inner.len()
113    }
114
115    pub fn is_empty(&self) -> bool {
116        self.inner.is_empty()
117    }
118
119    pub fn names(&self) -> Vec<String> {
120        self.inner
121            .entries()
122            .into_iter()
123            .map(|(name, _)| name)
124            .collect()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131    use async_trait::async_trait;
132    use camel_api::security_policy::{
133        AuthContext, AuthPrincipal, AuthorizationDecision, Principal, TransportId,
134    };
135    use camel_api::{CamelError, Exchange, Message};
136
137    struct AllowPolicy;
138    struct DenyPolicy;
139
140    #[async_trait]
141    impl SecurityPolicy for AllowPolicy {
142        async fn evaluate(
143            &self,
144            _exchange: &mut Exchange,
145            _auth: &AuthContext<'_>,
146        ) -> Result<AuthorizationDecision, CamelError> {
147            Ok(AuthorizationDecision::Granted {
148                principal: Principal {
149                    subject: "allow-user".into(),
150                    issuer: "test".into(),
151                    audience: vec![],
152                    scopes: vec![],
153                    roles: vec![],
154                    claims: serde_json::Value::Null,
155                },
156            })
157        }
158    }
159
160    #[async_trait]
161    impl SecurityPolicy for DenyPolicy {
162        async fn evaluate(
163            &self,
164            _exchange: &mut Exchange,
165            _auth: &AuthContext<'_>,
166        ) -> Result<AuthorizationDecision, CamelError> {
167            Ok(AuthorizationDecision::Denied {
168                reason: "deny".into(),
169                required: vec![],
170                actual: vec![],
171            })
172        }
173    }
174
175    struct TestPrincipal(Principal);
176
177    impl AuthPrincipal for TestPrincipal {
178        fn principal(&self) -> &Principal {
179            &self.0
180        }
181        fn provider_id(&self) -> &str {
182            "test"
183        }
184    }
185
186    fn auth_ctx<'a>(principal: &'a TestPrincipal) -> AuthContext<'a> {
187        AuthContext {
188            principal,
189            transport: TransportId::Http,
190        }
191    }
192
193    fn test_principal() -> TestPrincipal {
194        TestPrincipal(Principal {
195            subject: "allow-user".into(),
196            issuer: "test".into(),
197            audience: vec![],
198            scopes: vec![],
199            roles: vec![],
200            claims: serde_json::Value::Null,
201        })
202    }
203
204    #[test]
205    fn register_and_get() {
206        let registry = SecurityPolicyRegistry::new();
207        registry.register("admin-policy", Arc::new(AllowPolicy));
208        let policy = registry.get("admin-policy");
209        assert!(policy.is_some());
210    }
211
212    #[test]
213    fn get_missing_returns_none() {
214        let registry = SecurityPolicyRegistry::new();
215        assert!(registry.get("nonexistent").is_none());
216    }
217
218    #[tokio::test]
219    async fn register_replaces_existing() {
220        let registry = SecurityPolicyRegistry::new();
221        registry.register("my-policy", Arc::new(AllowPolicy));
222        registry.register("my-policy", Arc::new(DenyPolicy));
223        let policy = registry.get("my-policy").unwrap();
224        let mut ex = Exchange::new(Message::default());
225        let principal = test_principal();
226        let auth = auth_ctx(&principal);
227        // DenyPolicy was registered last — must be returned
228        let decision = policy.evaluate(&mut ex, &auth).await.unwrap();
229        assert!(matches!(decision, AuthorizationDecision::Denied { .. }));
230    }
231
232    // --- PermissionEvaluatorRegistry tests ---
233
234    use crate::permission::{PermissionDecision, PermissionRequest};
235
236    struct GrantEvaluator;
237
238    #[async_trait]
239    impl PermissionEvaluator for GrantEvaluator {
240        async fn evaluate(
241            &self,
242            _request: PermissionRequest,
243        ) -> Result<PermissionDecision, crate::types::AuthError> {
244            Ok(PermissionDecision::Granted)
245        }
246    }
247
248    struct DenyEvaluator {
249        reason: String,
250    }
251
252    #[async_trait]
253    impl PermissionEvaluator for DenyEvaluator {
254        async fn evaluate(
255            &self,
256            _request: PermissionRequest,
257        ) -> Result<PermissionDecision, crate::types::AuthError> {
258            Ok(PermissionDecision::Denied {
259                reason: self.reason.clone(),
260            })
261        }
262    }
263
264    #[test]
265    fn evaluator_register_and_get() {
266        let registry = PermissionEvaluatorRegistry::new();
267        registry.register("keycloak-uma", Arc::new(GrantEvaluator));
268        let evaluator = registry.get("keycloak-uma");
269        assert!(evaluator.is_some());
270    }
271
272    #[test]
273    fn evaluator_get_missing_returns_none() {
274        let registry = PermissionEvaluatorRegistry::new();
275        assert!(registry.get("nonexistent").is_none());
276    }
277
278    #[test]
279    fn entries_returns_registered_items() {
280        let registry = SecurityPolicyRegistry::new();
281        registry.register("admin-policy", Arc::new(AllowPolicy));
282        let entries = registry.entries();
283        assert_eq!(entries.len(), 1);
284        assert_eq!(entries[0].0, "admin-policy");
285    }
286
287    #[test]
288    fn is_empty_returns_true_when_no_entries() {
289        let registry = SecurityPolicyRegistry::new();
290        assert!(registry.is_empty());
291    }
292
293    #[test]
294    fn is_empty_returns_false_when_entries_exist() {
295        let registry = SecurityPolicyRegistry::new();
296        registry.register("p1", Arc::new(AllowPolicy));
297        assert!(!registry.is_empty());
298    }
299
300    #[tokio::test]
301    async fn evaluator_register_replaces_existing() {
302        let registry = PermissionEvaluatorRegistry::new();
303        registry.register("my-evaluator", Arc::new(GrantEvaluator));
304        registry.register(
305            "my-evaluator",
306            Arc::new(DenyEvaluator {
307                reason: "replaced".into(),
308            }),
309        );
310        let evaluator = registry.get("my-evaluator").unwrap();
311        let request = PermissionRequest {
312            principal: Principal {
313                subject: "test".into(),
314                issuer: "test".into(),
315                audience: vec![],
316                scopes: vec![],
317                roles: vec![],
318                claims: serde_json::Value::Null,
319            },
320            resource: "/test".into(),
321            action: "read".into(),
322            requested_scopes: vec![],
323            context: serde_json::Value::Null,
324        };
325        let decision = evaluator.evaluate(request).await.unwrap();
326        assert!(
327            matches!(decision, PermissionDecision::Denied { .. }),
328            "expected Denied, got {decision:?}"
329        );
330    }
331
332    // --- ProviderRegistry tests ---
333
334    struct StaticAuth;
335
336    #[async_trait]
337    impl TokenAuthenticator for StaticAuth {
338        async fn authenticate_bearer(&self, _token: &str) -> Result<Principal, CamelError> {
339            Ok(Principal {
340                subject: "static-user".into(),
341                issuer: "test".into(),
342                audience: vec![],
343                scopes: vec![],
344                roles: vec![],
345                claims: serde_json::Value::Null,
346            })
347        }
348    }
349
350    fn provider_entry() -> ProviderEntry {
351        ProviderEntry {
352            authenticator: Arc::new(StaticAuth),
353            audience_binding: None,
354        }
355    }
356
357    #[test]
358    fn provider_registry_registers_and_resolves() {
359        let registry = ProviderRegistry::new();
360        registry.register("idp-a", provider_entry());
361        assert!(registry.resolve("idp-a").is_some());
362        assert!(registry.resolve("ghost").is_none());
363    }
364
365    #[test]
366    fn sole_and_multiple_provider_counts() {
367        let registry = ProviderRegistry::new();
368        registry.register("idp-a", provider_entry());
369        assert_eq!(registry.len(), 1);
370        registry.register("idp-b", provider_entry());
371        assert_eq!(registry.len(), 2);
372    }
373}