Skip to main content

camel_auth/
permission.rs

1//! Permission evaluation contracts for authorization decisions.
2//!
3//! [`PermissionRequest`] captures who is asking, what resource, and what action.
4//! [`PermissionEvaluator`] is the async trait that evaluates requests into
5//! [`PermissionDecision`] results.
6
7use async_trait::async_trait;
8use camel_api::security_policy::Principal;
9use serde::Deserialize;
10use std::fmt;
11
12use crate::types::AuthError;
13
14/// A request to evaluate whether a principal may perform an action on a resource.
15#[derive(Debug, Clone, Deserialize, PartialEq)]
16pub struct PermissionRequest {
17    pub principal: Principal,
18    pub resource: String,
19    pub action: String,
20    #[serde(default)]
21    pub requested_scopes: Vec<String>,
22    #[serde(default)]
23    pub context: serde_json::Value,
24}
25
26/// The outcome of a permission evaluation.
27#[derive(Debug, Clone, PartialEq)]
28pub enum PermissionDecision {
29    Granted,
30    Denied { reason: String },
31}
32
33impl fmt::Display for PermissionDecision {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        match self {
36            Self::Granted => write!(f, "Permission granted"),
37            Self::Denied { reason } => write!(f, "Permission denied: {reason}"),
38        }
39    }
40}
41
42/// Async trait for evaluating permission requests.
43#[async_trait]
44pub trait PermissionEvaluator: Send + Sync {
45    async fn evaluate(&self, request: PermissionRequest) -> Result<PermissionDecision, AuthError>;
46}
47
48/// Describes where a permission value comes from.
49#[derive(Debug, Clone, PartialEq)]
50pub enum PermissionValueSource {
51    Literal(String),
52    Header(String),
53    Property(String),
54}
55
56/// Configuration for building permission evaluation context.
57#[derive(Debug, Clone, Default, PartialEq)]
58pub struct PermissionContextConfig {
59    pub include_headers: Vec<String>,
60    pub include_properties: Vec<String>,
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use serde_json::json;
67
68    fn test_principal() -> Principal {
69        Principal {
70            subject: "alice".into(),
71            issuer: "https://keycloak.example.com/realms/test".into(),
72            audience: vec!["camel-api".into()],
73            roles: vec!["admin".into()],
74            scopes: vec!["read".into()],
75            claims: json!({}),
76        }
77    }
78
79    #[test]
80    fn permission_request_deserializes_with_context() {
81        let json = r#"{
82            "principal":{"subject":"alice","issuer":"test","audience":[],"scopes":[],"roles":[],"claims":{}},
83            "resource":"/orders/123",
84            "action":"read",
85            "requested_scopes":["read"],
86            "context":{"source":"api"}
87        }"#;
88        let req: PermissionRequest =
89            serde_json::from_str(json).expect("deserialization should succeed");
90        assert_eq!(req.resource, "/orders/123");
91        assert_eq!(req.action, "read");
92        assert_eq!(req.requested_scopes, vec!["read".to_string()]);
93    }
94
95    #[test]
96    fn permission_request_deserializes_with_defaults() {
97        let json = r#"{
98            "principal":{"subject":"bob","issuer":"test","audience":[],"scopes":[],"roles":[],"claims":{}},
99            "resource":"/orders",
100            "action":"write"
101        }"#;
102        let req: PermissionRequest =
103            serde_json::from_str(json).expect("deserialization should succeed");
104        assert!(req.requested_scopes.is_empty());
105        assert!(req.context.is_null());
106    }
107
108    #[test]
109    fn permission_decision_granted_display() {
110        let decision = PermissionDecision::Granted;
111        assert_eq!(format!("{decision}"), "Permission granted");
112    }
113
114    #[test]
115    fn permission_decision_denied_display() {
116        let decision = PermissionDecision::Denied {
117            reason: "insufficient scope".into(),
118        };
119        assert_eq!(
120            format!("{decision}"),
121            "Permission denied: insufficient scope"
122        );
123    }
124
125    struct AlwaysGrant;
126
127    #[async_trait]
128    impl PermissionEvaluator for AlwaysGrant {
129        async fn evaluate(
130            &self,
131            _request: PermissionRequest,
132        ) -> Result<PermissionDecision, AuthError> {
133            Ok(PermissionDecision::Granted)
134        }
135    }
136
137    #[tokio::test]
138    async fn mock_evaluator_grants() {
139        let evaluator = AlwaysGrant;
140        let request = PermissionRequest {
141            principal: test_principal(),
142            resource: "/orders".into(),
143            action: "read".into(),
144            requested_scopes: vec![],
145            context: json!({}),
146        };
147        let result = evaluator.evaluate(request).await.expect("should succeed");
148        assert_eq!(result, PermissionDecision::Granted);
149    }
150}