1use std::collections::HashMap;
11use std::sync::Arc;
12
13use modkit_security::{AccessScope, SecurityContext};
14
15use super::IntoPropertyValue;
16use uuid::Uuid;
17
18use crate::api::AuthZResolverClient;
19use crate::error::AuthZResolverError;
20use crate::models::{
21 Action, BarrierMode, Capability, EvaluationRequest, EvaluationRequestContext, Resource,
22 Subject, TenantContext, TenantMode,
23};
24use crate::pep::compiler::{ConstraintCompileError, compile_to_access_scope};
25
26#[derive(Debug, thiserror::Error)]
28pub enum EnforcerError {
29 #[error("access denied by PDP")]
31 Denied {
32 deny_reason: Option<crate::models::DenyReason>,
34 },
35
36 #[error("authorization evaluation failed: {0}")]
38 EvaluationFailed(#[from] AuthZResolverError),
39
40 #[error("constraint compilation failed: {0}")]
42 CompileFailed(#[from] ConstraintCompileError),
43}
44
45#[derive(Debug, Clone, Default)]
74pub struct AccessRequest {
75 resource_properties: HashMap<String, serde_json::Value>,
76 tenant_context: Option<TenantContext>,
77 require_constraints: Option<bool>,
78}
79
80impl AccessRequest {
81 #[must_use]
83 pub fn new() -> Self {
84 Self::default()
85 }
86
87 #[must_use]
89 pub fn resource_property(
90 mut self,
91 key: impl Into<String>,
92 value: impl IntoPropertyValue,
93 ) -> Self {
94 self.resource_properties
95 .insert(key.into(), value.into_filter_value());
96 self
97 }
98
99 #[must_use]
101 pub fn resource_properties(mut self, props: HashMap<String, serde_json::Value>) -> Self {
102 self.resource_properties = props;
103 self
104 }
105
106 #[must_use]
108 pub fn context_tenant_id(mut self, id: Uuid) -> Self {
109 self.tenant_context.get_or_insert_default().root_id = Some(id);
110 self
111 }
112
113 #[must_use]
115 pub fn tenant_mode(mut self, mode: TenantMode) -> Self {
116 self.tenant_context.get_or_insert_default().mode = mode;
117 self
118 }
119
120 #[must_use]
122 pub fn barrier_mode(mut self, mode: BarrierMode) -> Self {
123 self.tenant_context.get_or_insert_default().barrier_mode = mode;
124 self
125 }
126
127 #[must_use]
129 pub fn tenant_status(mut self, statuses: Vec<String>) -> Self {
130 self.tenant_context.get_or_insert_default().tenant_status = Some(statuses);
131 self
132 }
133
134 #[must_use]
136 pub fn tenant_context(mut self, tc: TenantContext) -> Self {
137 self.tenant_context = Some(tc);
138 self
139 }
140
141 #[must_use]
154 pub fn require_constraints(mut self, require: bool) -> Self {
155 self.require_constraints = Some(require);
156 self
157 }
158}
159
160#[derive(Debug, Clone, Copy)]
165pub struct ResourceType {
166 pub name: &'static str,
168 pub supported_properties: &'static [&'static str],
170}
171
172#[derive(Clone)]
197pub struct PolicyEnforcer {
198 authz: Arc<dyn AuthZResolverClient>,
199 capabilities: Vec<Capability>,
200}
201
202impl PolicyEnforcer {
203 pub fn new(authz: Arc<dyn AuthZResolverClient>) -> Self {
205 Self {
206 authz,
207 capabilities: Vec::new(),
208 }
209 }
210
211 #[must_use]
213 pub fn with_capabilities(mut self, capabilities: Vec<Capability>) -> Self {
214 self.capabilities = capabilities;
215 self
216 }
217
218 #[must_use]
223 pub fn build_request(
224 &self,
225 ctx: &SecurityContext,
226 resource: &ResourceType,
227 action: &str,
228 resource_id: Option<Uuid>,
229 require_constraints: bool,
230 ) -> EvaluationRequest {
231 self.build_request_with(
232 ctx,
233 resource,
234 action,
235 resource_id,
236 require_constraints,
237 &AccessRequest::default(),
238 )
239 }
240
241 #[must_use]
243 pub fn build_request_with(
244 &self,
245 ctx: &SecurityContext,
246 resource: &ResourceType,
247 action: &str,
248 resource_id: Option<Uuid>,
249 require_constraints: bool,
250 request: &AccessRequest,
251 ) -> EvaluationRequest {
252 let tenant_context = request.tenant_context.clone();
255
256 let mut subject_properties = HashMap::new();
258 subject_properties.insert(
259 "tenant_id".to_owned(),
260 serde_json::Value::String(ctx.subject_tenant_id().to_string()),
261 );
262
263 let bearer_token = ctx.bearer_token().cloned();
264
265 EvaluationRequest {
266 subject: Subject {
267 id: ctx.subject_id(),
268 subject_type: ctx.subject_type().map(ToOwned::to_owned),
269 properties: subject_properties,
270 },
271 action: Action {
272 name: action.to_owned(),
273 },
274 resource: Resource {
275 resource_type: resource.name.to_owned(),
276 id: resource_id,
277 properties: request.resource_properties.clone(),
278 },
279 context: EvaluationRequestContext {
280 tenant_context,
281 token_scopes: ctx.token_scopes().to_vec(),
282 require_constraints,
283 capabilities: self.capabilities.clone(),
284 supported_properties: resource
285 .supported_properties
286 .iter()
287 .map(|s| (*s).to_owned())
288 .collect(),
289 bearer_token,
290 },
291 }
292 }
293
294 pub async fn access_scope(
307 &self,
308 ctx: &SecurityContext,
309 resource: &ResourceType,
310 action: &str,
311 resource_id: Option<Uuid>,
312 ) -> Result<AccessScope, EnforcerError> {
313 self.access_scope_with(
314 ctx,
315 resource,
316 action,
317 resource_id,
318 &AccessRequest::default(),
319 )
320 .await
321 }
322
323 pub async fn access_scope_with(
334 &self,
335 ctx: &SecurityContext,
336 resource: &ResourceType,
337 action: &str,
338 resource_id: Option<Uuid>,
339 request: &AccessRequest,
340 ) -> Result<AccessScope, EnforcerError> {
341 let require = request.require_constraints.unwrap_or(true);
342 let eval_request =
343 self.build_request_with(ctx, resource, action, resource_id, require, request);
344 let response = self.authz.evaluate(eval_request).await?;
345
346 if !response.decision {
349 return Err(EnforcerError::Denied {
350 deny_reason: response.context.deny_reason,
351 });
352 }
353
354 Ok(compile_to_access_scope(
355 &response,
356 require,
357 resource.supported_properties,
358 )?)
359 }
360}
361
362impl std::fmt::Debug for PolicyEnforcer {
363 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
364 f.debug_struct("PolicyEnforcer")
365 .field("capabilities", &self.capabilities)
366 .finish_non_exhaustive()
367 }
368}
369
370#[cfg(test)]
371#[cfg_attr(coverage_nightly, coverage(off))]
372mod tests {
373 use async_trait::async_trait;
374
375 use super::*;
376 use crate::constraints::{Constraint, InPredicate, Predicate};
377 use crate::models::{EvaluationResponse, EvaluationResponseContext};
378 use modkit_security::pep_properties;
379
380 fn uuid(s: &str) -> Uuid {
381 Uuid::parse_str(s).expect("valid test UUID")
382 }
383
384 const TENANT: &str = "11111111-1111-1111-1111-111111111111";
385 const SUBJECT: &str = "22222222-2222-2222-2222-222222222222";
386 const RESOURCE: &str = "33333333-3333-3333-3333-333333333333";
387
388 struct AllowAllMock;
392
393 #[async_trait]
394 impl AuthZResolverClient for AllowAllMock {
395 async fn evaluate(
396 &self,
397 req: EvaluationRequest,
398 ) -> Result<EvaluationResponse, AuthZResolverError> {
399 let constraints = if let Some(ref tc) = req.context.tenant_context {
400 if let Some(root_id) = tc.root_id {
401 vec![Constraint {
402 predicates: vec![Predicate::In(InPredicate::new(
403 pep_properties::OWNER_TENANT_ID,
404 [root_id],
405 ))],
406 }]
407 } else {
408 vec![]
409 }
410 } else {
411 vec![]
412 };
413 Ok(EvaluationResponse {
414 decision: true,
415 context: EvaluationResponseContext {
416 constraints,
417 ..Default::default()
418 },
419 })
420 }
421 }
422
423 struct DenyMock {
425 deny_reason: Option<crate::models::DenyReason>,
426 }
427
428 impl DenyMock {
429 fn new() -> Self {
430 Self { deny_reason: None }
431 }
432
433 fn with_reason(error_code: &str, details: Option<&str>) -> Self {
434 Self {
435 deny_reason: Some(crate::models::DenyReason {
436 error_code: error_code.to_owned(),
437 details: details.map(ToOwned::to_owned),
438 }),
439 }
440 }
441 }
442
443 #[async_trait]
444 impl AuthZResolverClient for DenyMock {
445 async fn evaluate(
446 &self,
447 _req: EvaluationRequest,
448 ) -> Result<EvaluationResponse, AuthZResolverError> {
449 Ok(EvaluationResponse {
450 decision: false,
451 context: EvaluationResponseContext {
452 deny_reason: self.deny_reason.clone(),
453 ..Default::default()
454 },
455 })
456 }
457 }
458
459 struct FailMock;
461
462 #[async_trait]
463 impl AuthZResolverClient for FailMock {
464 async fn evaluate(
465 &self,
466 _req: EvaluationRequest,
467 ) -> Result<EvaluationResponse, AuthZResolverError> {
468 Err(AuthZResolverError::Internal("boom".to_owned()))
469 }
470 }
471
472 fn test_ctx() -> SecurityContext {
473 SecurityContext::builder()
474 .subject_id(uuid(SUBJECT))
475 .subject_tenant_id(uuid(TENANT))
476 .build()
477 .unwrap()
478 }
479
480 const TEST_RESOURCE: ResourceType = ResourceType {
481 name: "gts.x.core.users.user.v1~",
482 supported_properties: &[pep_properties::OWNER_TENANT_ID, pep_properties::RESOURCE_ID],
483 };
484
485 fn enforcer(mock: impl AuthZResolverClient + 'static) -> PolicyEnforcer {
486 PolicyEnforcer::new(Arc::new(mock))
487 }
488
489 #[test]
492 fn build_request_populates_fields() {
493 let e = enforcer(AllowAllMock);
494 let ctx = test_ctx();
495 let req = e.build_request(&ctx, &TEST_RESOURCE, "get", Some(uuid(RESOURCE)), true);
496
497 assert_eq!(req.resource.resource_type, "gts.x.core.users.user.v1~");
498 assert_eq!(req.action.name, "get");
499 assert_eq!(req.resource.id, Some(uuid(RESOURCE)));
500 assert!(req.context.require_constraints);
501 assert!(req.context.tenant_context.is_none());
503 }
504
505 #[test]
506 fn build_request_with_overrides_tenant() {
507 let custom_tenant = uuid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
508 let e = enforcer(AllowAllMock);
509 let ctx = test_ctx();
510 let req = e.build_request_with(
511 &ctx,
512 &TEST_RESOURCE,
513 "list",
514 None,
515 false,
516 &AccessRequest::new().context_tenant_id(custom_tenant),
517 );
518
519 assert_eq!(
520 req.context
521 .tenant_context
522 .as_ref()
523 .and_then(|tc| tc.root_id),
524 Some(custom_tenant),
525 );
526 assert!(!req.context.require_constraints);
527 }
528
529 #[tokio::test]
532 async fn access_scope_no_explicit_tenant_returns_compile_error() {
533 let e = enforcer(AllowAllMock);
534 let ctx = test_ctx();
535 let result = e
538 .access_scope(&ctx, &TEST_RESOURCE, "get", Some(uuid(RESOURCE)))
539 .await;
540
541 assert!(matches!(result, Err(EnforcerError::CompileFailed(_))));
542 }
543
544 #[tokio::test]
545 async fn access_scope_with_explicit_tenant_returns_scope() {
546 let e = enforcer(AllowAllMock);
547 let ctx = test_ctx();
548 let scope = e
549 .access_scope_with(
550 &ctx,
551 &TEST_RESOURCE,
552 "get",
553 Some(uuid(RESOURCE)),
554 &AccessRequest::new().context_tenant_id(uuid(TENANT)),
555 )
556 .await
557 .expect("should succeed");
558
559 assert_eq!(
560 scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
561 &[uuid(TENANT)]
562 );
563 }
564
565 #[tokio::test]
566 async fn access_scope_with_for_create() {
567 let e = enforcer(AllowAllMock);
568 let ctx = test_ctx();
569 let scope = e
570 .access_scope_with(
571 &ctx,
572 &TEST_RESOURCE,
573 "create",
574 None,
575 &AccessRequest::new()
576 .context_tenant_id(uuid(TENANT))
577 .tenant_mode(TenantMode::RootOnly),
578 )
579 .await
580 .expect("should succeed");
581
582 assert_eq!(
583 scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
584 &[uuid(TENANT)]
585 );
586 }
587
588 #[tokio::test]
589 async fn access_scope_denied_returns_denied_error() {
590 let e = enforcer(DenyMock::new());
591 let ctx = test_ctx();
592 let result = e.access_scope(&ctx, &TEST_RESOURCE, "get", None).await;
593
594 assert!(matches!(
595 result,
596 Err(EnforcerError::Denied { deny_reason: None })
597 ));
598 }
599
600 #[tokio::test]
601 async fn access_scope_denied_with_reason() {
602 let e = enforcer(DenyMock::with_reason(
603 "INSUFFICIENT_PERMISSIONS",
604 Some("Missing admin role"),
605 ));
606 let ctx = test_ctx();
607 let result = e.access_scope(&ctx, &TEST_RESOURCE, "get", None).await;
608
609 match result {
610 Err(EnforcerError::Denied { deny_reason }) => {
611 let reason = deny_reason.expect("should have deny_reason");
612 assert_eq!(reason.error_code, "INSUFFICIENT_PERMISSIONS");
613 assert_eq!(reason.details.as_deref(), Some("Missing admin role"));
614 }
615 other => panic!("Expected Denied with reason, got: {other:?}"),
616 }
617 }
618
619 #[tokio::test]
620 async fn access_scope_evaluation_failure() {
621 let e = enforcer(FailMock);
622 let ctx = test_ctx();
623 let result = e.access_scope(&ctx, &TEST_RESOURCE, "get", None).await;
624
625 assert!(matches!(result, Err(EnforcerError::EvaluationFailed(_))));
626 }
627
628 #[tokio::test]
629 async fn access_scope_anonymous_no_tenant_returns_compile_error() {
630 let e = enforcer(AllowAllMock);
631 let ctx = SecurityContext::anonymous();
632 let result = e.access_scope(&ctx, &TEST_RESOURCE, "list", None).await;
634
635 assert!(matches!(result, Err(EnforcerError::CompileFailed(_))));
636 }
637
638 #[test]
641 fn with_capabilities() {
642 let e = enforcer(AllowAllMock).with_capabilities(vec![Capability::TenantHierarchy]);
643
644 assert_eq!(e.capabilities, vec![Capability::TenantHierarchy]);
645 }
646
647 #[test]
648 fn debug_impl() {
649 let e = enforcer(AllowAllMock);
650 let dbg = format!("{e:?}");
651 assert!(dbg.contains("PolicyEnforcer"));
652 }
653
654 #[test]
657 fn access_request_default_is_empty() {
658 let req = AccessRequest::new();
659 assert!(req.resource_properties.is_empty());
660 assert!(req.tenant_context.is_none());
661 }
662
663 #[test]
664 fn access_request_builder_chain() {
665 let tid = uuid(TENANT);
666 let req = AccessRequest::new()
667 .resource_property(pep_properties::OWNER_TENANT_ID, tid)
668 .context_tenant_id(tid)
669 .tenant_mode(TenantMode::RootOnly)
670 .barrier_mode(BarrierMode::Ignore)
671 .tenant_status(vec!["active".to_owned()]);
672
673 assert_eq!(req.resource_properties.len(), 1);
674 let tc = req.tenant_context.as_ref().expect("tenant context");
675 assert_eq!(tc.root_id, Some(tid));
676 assert_eq!(tc.mode, TenantMode::RootOnly);
677 assert_eq!(tc.barrier_mode, BarrierMode::Ignore);
678 assert_eq!(tc.tenant_status, Some(vec!["active".to_owned()]));
679 }
680
681 #[test]
682 fn access_request_tenant_context_setter() {
683 let tid = uuid(TENANT);
684 let req = AccessRequest::new().tenant_context(TenantContext {
685 mode: TenantMode::RootOnly,
686 root_id: Some(tid),
687 ..Default::default()
688 });
689
690 let tc = req.tenant_context.as_ref().expect("tenant context");
691 assert_eq!(tc.root_id, Some(tid));
692 assert_eq!(tc.mode, TenantMode::RootOnly);
693 assert_eq!(tc.barrier_mode, BarrierMode::Respect);
694 }
695
696 #[test]
697 fn access_request_resource_properties_replaces() {
698 let mut props = HashMap::new();
699 props.insert("a".to_owned(), serde_json::json!("1"));
700 props.insert("b".to_owned(), serde_json::json!("2"));
701
702 let req = AccessRequest::new()
703 .resource_property("old_key", serde_json::json!("old"))
704 .resource_properties(props);
705
706 assert_eq!(req.resource_properties.len(), 2);
707 assert!(!req.resource_properties.contains_key("old_key"));
708 }
709
710 #[test]
711 fn into_property_value_implementations() {
712 let uuid_val = uuid(TENANT);
713 let req = AccessRequest::new()
714 .resource_property("uuid_prop", uuid_val)
715 .resource_property("uuid_ref_prop", uuid_val)
716 .resource_property("string_prop", "test".to_owned())
717 .resource_property("str_prop", "test")
718 .resource_property("int_prop", 42i64)
719 .resource_property("json_prop", serde_json::json!({"key": "value"}));
720
721 assert_eq!(req.resource_properties.len(), 6);
722 assert_eq!(
723 req.resource_properties.get("uuid_prop"),
724 Some(&serde_json::json!(uuid_val.to_string())),
725 );
726 assert_eq!(
727 req.resource_properties.get("string_prop"),
728 Some(&serde_json::json!("test")),
729 );
730 assert_eq!(
731 req.resource_properties.get("int_prop"),
732 Some(&serde_json::json!(42)),
733 );
734 }
735
736 #[test]
739 fn build_request_with_applies_resource_properties() {
740 let e = enforcer(AllowAllMock);
741 let ctx = test_ctx();
742 let tid = uuid(TENANT);
743 let req = e.build_request_with(
744 &ctx,
745 &TEST_RESOURCE,
746 "create",
747 None,
748 false,
749 &AccessRequest::new().resource_property(pep_properties::OWNER_TENANT_ID, tid),
750 );
751
752 assert_eq!(
753 req.resource.properties.get(pep_properties::OWNER_TENANT_ID),
754 Some(&serde_json::json!(tid.to_string())),
755 );
756 }
757
758 #[test]
759 fn build_request_with_applies_tenant_mode_and_barrier() {
760 let e = enforcer(AllowAllMock);
761 let ctx = test_ctx();
762 let req = e.build_request_with(
763 &ctx,
764 &TEST_RESOURCE,
765 "list",
766 None,
767 true,
768 &AccessRequest::new()
769 .tenant_mode(TenantMode::RootOnly)
770 .barrier_mode(BarrierMode::Ignore)
771 .tenant_status(vec!["active".to_owned()]),
772 );
773
774 let tc = req.context.tenant_context.as_ref().expect("tenant context");
775 assert_eq!(tc.mode, TenantMode::RootOnly);
776 assert_eq!(tc.barrier_mode, BarrierMode::Ignore);
777 assert_eq!(tc.tenant_status, Some(vec!["active".to_owned()]));
778 }
779
780 #[test]
781 fn build_request_with_default_has_no_tenant_context() {
782 let e = enforcer(AllowAllMock);
783 let ctx = test_ctx();
784 let req = e.build_request_with(
785 &ctx,
786 &TEST_RESOURCE,
787 "get",
788 None,
789 true,
790 &AccessRequest::default(),
791 );
792
793 assert!(req.context.tenant_context.is_none());
795 }
796
797 #[tokio::test]
800 async fn access_scope_with_custom_tenant() {
801 let custom_tenant = uuid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
802 let e = enforcer(AllowAllMock);
803 let ctx = test_ctx();
804 let scope = e
805 .access_scope_with(
806 &ctx,
807 &TEST_RESOURCE,
808 "list",
809 None,
810 &AccessRequest::new().context_tenant_id(custom_tenant),
811 )
812 .await
813 .expect("should succeed");
814
815 assert_eq!(
816 scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
817 &[custom_tenant]
818 );
819 }
820
821 #[tokio::test]
822 async fn access_scope_with_resource_properties() {
823 let e = enforcer(AllowAllMock);
824 let ctx = test_ctx();
825 let scope = e
826 .access_scope_with(
827 &ctx,
828 &TEST_RESOURCE,
829 "get",
830 None,
831 &AccessRequest::new()
832 .resource_property(
833 pep_properties::OWNER_TENANT_ID,
834 serde_json::json!(uuid(TENANT).to_string()),
835 )
836 .context_tenant_id(uuid(TENANT))
837 .tenant_mode(TenantMode::RootOnly),
838 )
839 .await
840 .expect("should succeed");
841
842 assert_eq!(
843 scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
844 &[uuid(TENANT)]
845 );
846 }
847
848 #[test]
851 fn builds_request_with_all_fields() {
852 const USERS_RESOURCE: ResourceType = ResourceType {
853 name: "gts.x.core.users.user.v1~",
854 supported_properties: &[pep_properties::OWNER_TENANT_ID],
855 };
856
857 let context_tenant_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
858 let subject_id = Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap();
859 let subject_tenant_id = Uuid::parse_str("33333333-3333-3333-3333-333333333333").unwrap();
860 let resource_id = Uuid::parse_str("44444444-4444-4444-4444-444444444444").unwrap();
861
862 let ctx = SecurityContext::builder()
863 .subject_id(subject_id)
864 .subject_tenant_id(subject_tenant_id)
865 .subject_type("user")
866 .token_scopes(vec!["admin".to_owned()])
867 .bearer_token("test-token".to_owned())
868 .build()
869 .unwrap();
870
871 let e = PolicyEnforcer::new(Arc::new(AllowAllMock))
872 .with_capabilities(vec![Capability::TenantHierarchy]);
873
874 let access_req = AccessRequest::new().tenant_context(TenantContext {
875 root_id: Some(context_tenant_id),
876 ..Default::default()
877 });
878
879 let request = e.build_request_with(
880 &ctx,
881 &USERS_RESOURCE,
882 "get",
883 Some(resource_id),
884 true,
885 &access_req,
886 );
887
888 assert_eq!(request.subject.id, subject_id);
889 assert_eq!(
890 request.subject.properties.get("tenant_id").unwrap(),
891 &serde_json::Value::String(subject_tenant_id.to_string())
892 );
893 assert_eq!(request.subject.subject_type.as_deref(), Some("user"));
894 assert_eq!(request.action.name, "get");
895 assert_eq!(request.resource.resource_type, "gts.x.core.users.user.v1~");
896 assert_eq!(request.resource.id, Some(resource_id));
897 assert!(request.context.require_constraints);
898 assert_eq!(
899 request.context.tenant_context.as_ref().unwrap().root_id,
900 Some(context_tenant_id)
901 );
902 assert_eq!(request.context.token_scopes, vec!["admin"]);
903 assert_eq!(
904 request.context.capabilities,
905 vec![Capability::TenantHierarchy]
906 );
907 assert!(request.context.bearer_token.is_some());
908 assert_eq!(
909 request.context.supported_properties,
910 vec![pep_properties::OWNER_TENANT_ID]
911 );
912 }
913
914 #[test]
915 fn builds_request_without_tenant_context() {
916 let ctx = SecurityContext::anonymous();
917
918 let e = enforcer(AllowAllMock);
919
920 let request = e.build_request_with(
921 &ctx,
922 &TEST_RESOURCE,
923 "create",
924 None,
925 false,
926 &AccessRequest::default(),
927 );
928
929 assert!(request.context.tenant_context.is_none());
930 assert!(!request.context.require_constraints);
931 assert_eq!(request.resource.id, None);
932 assert!(request.context.capabilities.is_empty());
933 assert!(request.context.bearer_token.is_none());
934 }
935
936 #[test]
937 fn applies_resource_properties() {
938 let tenant_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
939 let ctx = SecurityContext::builder()
940 .subject_id(Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap())
941 .subject_tenant_id(tenant_id)
942 .build()
943 .unwrap();
944
945 let e = enforcer(AllowAllMock);
946 let access_req = AccessRequest::new()
947 .resource_property(
948 pep_properties::OWNER_TENANT_ID,
949 serde_json::Value::String(tenant_id.to_string()),
950 )
951 .context_tenant_id(tenant_id);
952
953 let request =
954 e.build_request_with(&ctx, &TEST_RESOURCE, "create", None, false, &access_req);
955
956 assert_eq!(
957 request
958 .resource
959 .properties
960 .get(pep_properties::OWNER_TENANT_ID),
961 Some(&serde_json::Value::String(tenant_id.to_string())),
962 );
963 }
964
965 #[test]
966 fn applies_tenant_mode_and_barrier_mode() {
967 let tenant_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
968 let ctx = SecurityContext::builder()
969 .subject_id(Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap())
970 .subject_tenant_id(tenant_id)
971 .build()
972 .unwrap();
973
974 let e = enforcer(AllowAllMock);
975 let access_req = AccessRequest::new().tenant_context(TenantContext {
976 mode: TenantMode::RootOnly,
977 root_id: Some(tenant_id),
978 barrier_mode: BarrierMode::Ignore,
979 tenant_status: Some(vec!["active".to_owned()]),
980 });
981
982 let request = e.build_request_with(&ctx, &TEST_RESOURCE, "list", None, true, &access_req);
983
984 let tc = request.context.tenant_context.as_ref().unwrap();
985 assert_eq!(tc.mode, TenantMode::RootOnly);
986 assert_eq!(tc.barrier_mode, BarrierMode::Ignore);
987 assert_eq!(tc.tenant_status, Some(vec!["active".to_owned()]));
988 }
989
990 #[test]
991 fn no_implicit_fallback_to_subject_tenant_id() {
992 let subject_tenant = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
993 let ctx = SecurityContext::builder()
994 .subject_id(Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap())
995 .subject_tenant_id(subject_tenant)
996 .build()
997 .unwrap();
998
999 let e = enforcer(AllowAllMock);
1000
1001 let request = e.build_request_with(
1003 &ctx,
1004 &TEST_RESOURCE,
1005 "list",
1006 None,
1007 true,
1008 &AccessRequest::default(),
1009 );
1010
1011 assert!(request.context.tenant_context.is_none());
1012 }
1013
1014 #[test]
1015 fn explicit_root_id_overrides_subject_tenant() {
1016 let subject_tenant = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
1017 let explicit_tenant = Uuid::parse_str("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa").unwrap();
1018 let ctx = SecurityContext::builder()
1019 .subject_id(Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap())
1020 .subject_tenant_id(subject_tenant)
1021 .build()
1022 .unwrap();
1023
1024 let e = enforcer(AllowAllMock);
1025 let access_req = AccessRequest::new().context_tenant_id(explicit_tenant);
1026
1027 let request = e.build_request_with(&ctx, &TEST_RESOURCE, "get", None, true, &access_req);
1028
1029 let tc = request.context.tenant_context.as_ref().unwrap();
1030 assert_eq!(tc.root_id, Some(explicit_tenant));
1031 }
1032}