static_authz_plugin/domain/
service.rs1use 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#[domain_model]
17#[derive(Default)]
18pub struct Service;
19
20impl Service {
21 #[must_use]
22 pub fn new() -> Self {
23 Self
24 }
25
26 #[must_use]
28 #[allow(clippy::unused_self)] pub fn evaluate(&self, request: &EvaluationRequest) -> EvaluationResponse {
30 let tenant_id = request
32 .context
33 .tenant_context
34 .as_ref()
35 .and_then(|t| t.root_id)
36 .or_else(|| {
37 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 return EvaluationResponse {
49 decision: false,
50 context: EvaluationResponseContext::default(),
51 };
52 };
53
54 if tid == Uuid::default() {
55 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 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(), },
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}