1use async_trait::async_trait;
8use camel_api::security_policy::Principal;
9use serde::Deserialize;
10use std::fmt;
11
12use crate::types::AuthError;
13
14#[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#[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]
44pub trait PermissionEvaluator: Send + Sync {
45 async fn evaluate(&self, request: PermissionRequest) -> Result<PermissionDecision, AuthError>;
46}
47
48#[derive(Debug, Clone, PartialEq)]
50pub enum PermissionValueSource {
51 Literal(String),
52 Header(String),
53 Property(String),
54}
55
56#[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}