Skip to main content

static_authz_plugin/domain/
service.rs

1//! Service implementation for the static `AuthZ` resolver plugin.
2
3use authz_resolver_sdk::{
4    Constraint, EvaluationRequest, EvaluationResponse, EvaluationResponseContext, InPredicate,
5    Predicate,
6};
7use modkit_macros::domain_model;
8use modkit_security::pep_properties;
9use uuid::Uuid;
10
11/// Static `AuthZ` resolver service.
12///
13/// - Returns `decision: true` with an `in` predicate on `pep_properties::OWNER_TENANT_ID`
14///   scoped to the context tenant from the request (for all operations including CREATE).
15/// - Denies access (`decision: false`) when no valid tenant can be resolved.
16#[domain_model]
17#[derive(Default)]
18pub struct Service;
19
20impl Service {
21    #[must_use]
22    pub fn new() -> Self {
23        Self
24    }
25
26    /// Evaluate an authorization request.
27    #[must_use]
28    #[allow(clippy::unused_self)] // &self reserved for future config/state
29    pub fn evaluate(&self, request: &EvaluationRequest) -> EvaluationResponse {
30        // Always scope to context tenant (all CRUD operations get constraints)
31        let tenant_id = request
32            .context
33            .tenant_context
34            .as_ref()
35            .and_then(|t| t.root_id)
36            .or_else(|| {
37                // Fallback: extract tenant_id from subject properties
38                request
39                    .subject
40                    .properties
41                    .get("tenant_id")
42                    .and_then(|v| v.as_str())
43                    .and_then(|s| Uuid::parse_str(s).ok())
44            });
45
46        let Some(tid) = tenant_id else {
47            // No tenant resolvable from context or subject — deny access.
48            return EvaluationResponse {
49                decision: false,
50                context: EvaluationResponseContext::default(),
51            };
52        };
53
54        if tid == Uuid::default() {
55            // Nil UUID tenant — deny rather than grant unrestricted access.
56            return EvaluationResponse {
57                decision: false,
58                context: EvaluationResponseContext::default(),
59            };
60        }
61
62        EvaluationResponse {
63            decision: true,
64            context: EvaluationResponseContext {
65                constraints: vec![Constraint {
66                    predicates: vec![Predicate::In(InPredicate::new(
67                        pep_properties::OWNER_TENANT_ID,
68                        [tid],
69                    ))],
70                }],
71                ..Default::default()
72            },
73        }
74    }
75}
76
77#[cfg(test)]
78#[cfg_attr(coverage_nightly, coverage(off))]
79mod tests {
80    use super::*;
81    use authz_resolver_sdk::pep::IntoPropertyValue;
82    use authz_resolver_sdk::{Action, EvaluationRequestContext, Resource, Subject, TenantContext};
83    use std::collections::HashMap;
84
85    fn make_request(require_constraints: bool, tenant_id: Option<Uuid>) -> EvaluationRequest {
86        let mut subject_properties = HashMap::new();
87        subject_properties.insert(
88            "tenant_id".to_owned(),
89            serde_json::Value::String("22222222-2222-2222-2222-222222222222".to_owned()),
90        );
91
92        EvaluationRequest {
93            subject: Subject {
94                id: Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap(),
95                subject_type: None,
96                properties: subject_properties,
97            },
98            action: Action {
99                name: "list".to_owned(),
100            },
101            resource: Resource {
102                resource_type: "gts.x.core.users.user.v1~".to_owned(),
103                id: None,
104                properties: HashMap::new(),
105            },
106            context: EvaluationRequestContext {
107                tenant_context: tenant_id.map(|id| TenantContext {
108                    root_id: Some(id),
109                    ..TenantContext::default()
110                }),
111                token_scopes: vec!["*".to_owned()],
112                require_constraints,
113                capabilities: vec![],
114                supported_properties: vec![],
115                bearer_token: None,
116            },
117        }
118    }
119
120    #[test]
121    fn list_operation_with_tenant_context() {
122        let tenant_id = Uuid::parse_str("33333333-3333-3333-3333-333333333333").unwrap();
123        let service = Service::new();
124        let response = service.evaluate(&make_request(true, Some(tenant_id)));
125
126        assert!(response.decision);
127        assert_eq!(response.context.constraints.len(), 1);
128
129        let constraint = &response.context.constraints[0];
130        assert_eq!(constraint.predicates.len(), 1);
131
132        match &constraint.predicates[0] {
133            Predicate::In(in_pred) => {
134                assert_eq!(in_pred.property, pep_properties::OWNER_TENANT_ID);
135                assert_eq!(in_pred.values, vec![tenant_id.into_filter_value()]);
136            }
137            other @ Predicate::Eq(_) => panic!("Expected In predicate, got: {other:?}"),
138        }
139    }
140
141    #[test]
142    fn list_operation_without_tenant_falls_back_to_subject_properties() {
143        let service = Service::new();
144        let response = service.evaluate(&make_request(true, None));
145
146        // Falls back to subject.properties["tenant_id"]
147        assert!(response.decision);
148        assert_eq!(response.context.constraints.len(), 1);
149
150        match &response.context.constraints[0].predicates[0] {
151            Predicate::In(in_pred) => {
152                assert_eq!(
153                    in_pred.values,
154                    vec![
155                        Uuid::parse_str("22222222-2222-2222-2222-222222222222")
156                            .unwrap()
157                            .into_filter_value()
158                    ]
159                );
160            }
161            other @ Predicate::Eq(_) => panic!("Expected In predicate, got: {other:?}"),
162        }
163    }
164
165    #[test]
166    fn nil_tenant_is_denied() {
167        let service = Service::new();
168        let response = service.evaluate(&make_request(true, Some(Uuid::default())));
169
170        assert!(!response.decision);
171        assert!(response.context.constraints.is_empty());
172    }
173
174    #[test]
175    fn missing_tenant_context_and_subject_property_is_denied() {
176        let request = EvaluationRequest {
177            subject: Subject {
178                id: Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap(),
179                subject_type: None,
180                properties: HashMap::new(), // no tenant_id property
181            },
182            action: Action {
183                name: "list".to_owned(),
184            },
185            resource: Resource {
186                resource_type: "gts.x.core.users.user.v1~".to_owned(),
187                id: None,
188                properties: HashMap::new(),
189            },
190            context: EvaluationRequestContext {
191                tenant_context: None,
192                token_scopes: vec!["*".to_owned()],
193                require_constraints: true,
194                capabilities: vec![],
195                supported_properties: vec![],
196                bearer_token: None,
197            },
198        };
199
200        let service = Service::new();
201        let response = service.evaluate(&request);
202
203        assert!(!response.decision);
204        assert!(response.context.constraints.is_empty());
205    }
206}