1use std::fmt;
2use uuid::Uuid;
3
4#[derive(Clone, Debug, PartialEq, Eq, Hash)]
10pub enum ScopeValue {
11 Uuid(Uuid),
13 String(String),
15 Int(i64),
17 Bool(bool),
19}
20
21impl ScopeValue {
22 #[must_use]
27 pub fn as_uuid(&self) -> Option<Uuid> {
28 match self {
29 Self::Uuid(u) => Some(*u),
30 Self::String(s) => Uuid::parse_str(s).ok(),
31 Self::Int(_) | Self::Bool(_) => None,
32 }
33 }
34}
35
36impl fmt::Display for ScopeValue {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 Self::Uuid(u) => write!(f, "{u}"),
40 Self::String(s) => write!(f, "{s}"),
41 Self::Int(n) => write!(f, "{n}"),
42 Self::Bool(b) => write!(f, "{b}"),
43 }
44 }
45}
46
47impl From<Uuid> for ScopeValue {
48 #[inline]
49 fn from(u: Uuid) -> Self {
50 Self::Uuid(u)
51 }
52}
53
54impl From<&Uuid> for ScopeValue {
55 #[inline]
56 fn from(u: &Uuid) -> Self {
57 Self::Uuid(*u)
58 }
59}
60
61impl From<String> for ScopeValue {
62 #[inline]
63 fn from(s: String) -> Self {
64 Self::String(s)
65 }
66}
67
68impl From<&str> for ScopeValue {
69 #[inline]
70 fn from(s: &str) -> Self {
71 Self::String(s.to_owned())
72 }
73}
74
75impl From<i64> for ScopeValue {
76 #[inline]
77 fn from(n: i64) -> Self {
78 Self::Int(n)
79 }
80}
81
82impl From<bool> for ScopeValue {
83 #[inline]
84 fn from(b: bool) -> Self {
85 Self::Bool(b)
86 }
87}
88
89pub mod pep_properties {
95 pub const OWNER_TENANT_ID: &str = "owner_tenant_id";
97
98 pub const RESOURCE_ID: &str = "id";
100
101 pub const OWNER_ID: &str = "owner_id";
103}
104
105pub mod rg_tables {
114 pub const MEMBERSHIP_TABLE: &str = "resource_group_membership";
116 pub const MEMBERSHIP_RESOURCE_ID: &str = "resource_id";
118 pub const MEMBERSHIP_GROUP_ID: &str = "group_id";
120
121 pub const CLOSURE_TABLE: &str = "resource_group_closure";
123 pub const CLOSURE_ANCESTOR_ID: &str = "ancestor_id";
125 pub const CLOSURE_DESCENDANT_ID: &str = "descendant_id";
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
140pub enum ScopeFilter {
141 Eq(EqScopeFilter),
143 In(InScopeFilter),
145 InGroup(InGroupScopeFilter),
147 InGroupSubtree(InGroupSubtreeScopeFilter),
149}
150
151#[derive(Clone, Debug, PartialEq, Eq, Hash)]
153pub struct EqScopeFilter {
154 property: String,
156 value: ScopeValue,
158}
159
160#[derive(Clone, Debug, PartialEq, Eq)]
162pub struct InScopeFilter {
163 property: String,
165 values: Vec<ScopeValue>,
167}
168
169impl EqScopeFilter {
170 #[must_use]
172 pub fn new(property: impl Into<String>, value: impl Into<ScopeValue>) -> Self {
173 Self {
174 property: property.into(),
175 value: value.into(),
176 }
177 }
178
179 #[inline]
181 #[must_use]
182 pub fn property(&self) -> &str {
183 &self.property
184 }
185
186 #[inline]
188 #[must_use]
189 pub fn value(&self) -> &ScopeValue {
190 &self.value
191 }
192}
193
194impl InScopeFilter {
195 #[must_use]
197 pub fn new(property: impl Into<String>, values: Vec<ScopeValue>) -> Self {
198 Self {
199 property: property.into(),
200 values,
201 }
202 }
203
204 #[must_use]
206 pub fn from_values<V: Into<ScopeValue>>(
207 property: impl Into<String>,
208 values: impl IntoIterator<Item = V>,
209 ) -> Self {
210 Self {
211 property: property.into(),
212 values: values.into_iter().map(Into::into).collect(),
213 }
214 }
215
216 #[inline]
218 #[must_use]
219 pub fn property(&self) -> &str {
220 &self.property
221 }
222
223 #[inline]
225 #[must_use]
226 pub fn values(&self) -> &[ScopeValue] {
227 &self.values
228 }
229}
230
231#[derive(Clone, Debug, PartialEq, Eq)]
233pub struct InGroupScopeFilter {
234 property: String,
235 group_ids: Vec<ScopeValue>,
236}
237
238impl InGroupScopeFilter {
239 #[must_use]
241 pub fn new(property: impl Into<String>, group_ids: Vec<ScopeValue>) -> Self {
242 Self {
243 property: property.into(),
244 group_ids,
245 }
246 }
247
248 #[inline]
250 #[must_use]
251 pub fn property(&self) -> &str {
252 &self.property
253 }
254
255 #[inline]
257 #[must_use]
258 pub fn group_ids(&self) -> &[ScopeValue] {
259 &self.group_ids
260 }
261}
262
263#[derive(Clone, Debug, PartialEq, Eq)]
265pub struct InGroupSubtreeScopeFilter {
266 property: String,
267 ancestor_ids: Vec<ScopeValue>,
268}
269
270impl InGroupSubtreeScopeFilter {
271 #[must_use]
273 pub fn new(property: impl Into<String>, ancestor_ids: Vec<ScopeValue>) -> Self {
274 Self {
275 property: property.into(),
276 ancestor_ids,
277 }
278 }
279
280 #[inline]
282 #[must_use]
283 pub fn property(&self) -> &str {
284 &self.property
285 }
286
287 #[inline]
289 #[must_use]
290 pub fn ancestor_ids(&self) -> &[ScopeValue] {
291 &self.ancestor_ids
292 }
293}
294
295impl ScopeFilter {
296 #[must_use]
298 pub fn eq(property: impl Into<String>, value: impl Into<ScopeValue>) -> Self {
299 Self::Eq(EqScopeFilter::new(property, value))
300 }
301
302 #[must_use]
304 pub fn r#in(property: impl Into<String>, values: Vec<ScopeValue>) -> Self {
305 Self::In(InScopeFilter::new(property, values))
306 }
307
308 #[must_use]
310 pub fn in_uuids(property: impl Into<String>, uuids: Vec<Uuid>) -> Self {
311 Self::In(InScopeFilter::new(
312 property,
313 uuids.into_iter().map(ScopeValue::Uuid).collect(),
314 ))
315 }
316
317 #[must_use]
319 pub fn in_group(property: impl Into<String>, group_ids: Vec<ScopeValue>) -> Self {
320 Self::InGroup(InGroupScopeFilter::new(property, group_ids))
321 }
322
323 #[must_use]
325 pub fn in_group_subtree(property: impl Into<String>, ancestor_ids: Vec<ScopeValue>) -> Self {
326 Self::InGroupSubtree(InGroupSubtreeScopeFilter::new(property, ancestor_ids))
327 }
328
329 #[must_use]
331 pub fn property(&self) -> &str {
332 match self {
333 Self::Eq(f) => f.property(),
334 Self::In(f) => f.property(),
335 Self::InGroup(f) => f.property(),
336 Self::InGroupSubtree(f) => f.property(),
337 }
338 }
339
340 #[must_use]
347 pub fn values(&self) -> ScopeFilterValues<'_> {
348 match self {
349 Self::Eq(f) => ScopeFilterValues::Single(&f.value),
350 Self::In(f) => ScopeFilterValues::Multiple(&f.values),
351 Self::InGroup(_) | Self::InGroupSubtree(_) => ScopeFilterValues::Multiple(&[]),
352 }
353 }
354
355 #[must_use]
360 pub fn uuid_values(&self) -> Vec<Uuid> {
361 self.values()
362 .iter()
363 .filter_map(ScopeValue::as_uuid)
364 .collect()
365 }
366}
367
368#[derive(Clone, Debug)]
373pub enum ScopeFilterValues<'a> {
374 Single(&'a ScopeValue),
376 Multiple(&'a [ScopeValue]),
378}
379
380impl<'a> ScopeFilterValues<'a> {
381 #[must_use]
383 pub fn iter(&self) -> ScopeFilterValuesIter<'a> {
384 match self {
385 Self::Single(v) => ScopeFilterValuesIter::Single(Some(v)),
386 Self::Multiple(vs) => ScopeFilterValuesIter::Multiple(vs.iter()),
387 }
388 }
389
390 #[must_use]
392 pub fn contains(&self, value: &ScopeValue) -> bool {
393 self.iter().any(|v| v == value)
394 }
395}
396
397impl<'a> IntoIterator for ScopeFilterValues<'a> {
398 type Item = &'a ScopeValue;
399 type IntoIter = ScopeFilterValuesIter<'a>;
400
401 fn into_iter(self) -> Self::IntoIter {
402 self.iter()
403 }
404}
405
406impl<'a> IntoIterator for &ScopeFilterValues<'a> {
407 type Item = &'a ScopeValue;
408 type IntoIter = ScopeFilterValuesIter<'a>;
409
410 fn into_iter(self) -> Self::IntoIter {
411 self.iter()
412 }
413}
414
415pub enum ScopeFilterValuesIter<'a> {
417 Single(Option<&'a ScopeValue>),
419 Multiple(std::slice::Iter<'a, ScopeValue>),
421}
422
423impl<'a> Iterator for ScopeFilterValuesIter<'a> {
424 type Item = &'a ScopeValue;
425
426 fn next(&mut self) -> Option<Self::Item> {
427 match self {
428 Self::Single(v) => v.take(),
429 Self::Multiple(iter) => iter.next(),
430 }
431 }
432}
433
434#[derive(Clone, Debug, PartialEq)]
439pub struct ScopeConstraint {
440 filters: Vec<ScopeFilter>,
441}
442
443impl ScopeConstraint {
444 #[must_use]
446 pub fn new(filters: Vec<ScopeFilter>) -> Self {
447 Self { filters }
448 }
449
450 #[inline]
452 #[must_use]
453 pub fn filters(&self) -> &[ScopeFilter] {
454 &self.filters
455 }
456
457 #[inline]
459 #[must_use]
460 pub fn is_empty(&self) -> bool {
461 self.filters.is_empty()
462 }
463}
464
465#[derive(Clone, Debug, PartialEq)]
487pub struct AccessScope {
488 constraints: Vec<ScopeConstraint>,
489 unconstrained: bool,
490}
491
492impl Default for AccessScope {
493 fn default() -> Self {
495 Self::deny_all()
496 }
497}
498
499impl AccessScope {
500 #[must_use]
504 pub fn from_constraints(constraints: Vec<ScopeConstraint>) -> Self {
505 Self {
506 constraints,
507 unconstrained: false,
508 }
509 }
510
511 #[must_use]
513 pub fn single(constraint: ScopeConstraint) -> Self {
514 Self::from_constraints(vec![constraint])
515 }
516
517 #[must_use]
522 pub fn allow_all() -> Self {
523 Self {
524 constraints: Vec::new(),
525 unconstrained: true,
526 }
527 }
528
529 #[must_use]
531 pub fn deny_all() -> Self {
532 Self {
533 constraints: Vec::new(),
534 unconstrained: false,
535 }
536 }
537
538 #[must_use]
542 pub fn for_tenants(ids: Vec<Uuid>) -> Self {
543 Self::single(ScopeConstraint::new(vec![ScopeFilter::in_uuids(
544 pep_properties::OWNER_TENANT_ID,
545 ids,
546 )]))
547 }
548
549 #[must_use]
551 pub fn for_tenant(id: Uuid) -> Self {
552 Self::for_tenants(vec![id])
553 }
554
555 #[must_use]
557 pub fn for_resources(ids: Vec<Uuid>) -> Self {
558 Self::single(ScopeConstraint::new(vec![ScopeFilter::in_uuids(
559 pep_properties::RESOURCE_ID,
560 ids,
561 )]))
562 }
563
564 #[must_use]
566 pub fn for_resource(id: Uuid) -> Self {
567 Self::for_resources(vec![id])
568 }
569
570 #[inline]
574 #[must_use]
575 pub fn constraints(&self) -> &[ScopeConstraint] {
576 &self.constraints
577 }
578
579 #[inline]
581 #[must_use]
582 pub fn is_unconstrained(&self) -> bool {
583 self.unconstrained
584 }
585
586 #[must_use]
590 pub fn is_deny_all(&self) -> bool {
591 !self.unconstrained && self.constraints.is_empty()
592 }
593
594 #[must_use]
596 pub fn all_values_for(&self, property: &str) -> Vec<&ScopeValue> {
597 let mut result = Vec::new();
598 for constraint in &self.constraints {
599 for filter in constraint.filters() {
600 if filter.property() == property {
601 result.extend(filter.values());
602 }
603 }
604 }
605 result
606 }
607
608 #[must_use]
612 pub fn all_uuid_values_for(&self, property: &str) -> Vec<Uuid> {
613 let mut result = Vec::new();
614 for constraint in &self.constraints {
615 for filter in constraint.filters() {
616 if filter.property() == property {
617 result.extend(filter.uuid_values());
618 }
619 }
620 }
621 result
622 }
623
624 #[must_use]
626 pub fn contains_value(&self, property: &str, value: &ScopeValue) -> bool {
627 self.constraints.iter().any(|c| {
628 c.filters()
629 .iter()
630 .any(|f| f.property() == property && f.values().contains(value))
631 })
632 }
633
634 #[must_use]
641 pub fn contains_uuid(&self, property: &str, id: Uuid) -> bool {
642 self.constraints.iter().any(|c| {
643 c.filters().iter().any(|f| {
644 f.property() == property && f.values().iter().any(|v| v.as_uuid() == Some(id))
645 })
646 })
647 }
648
649 #[must_use]
651 pub fn has_property(&self, property: &str) -> bool {
652 self.constraints
653 .iter()
654 .any(|c| c.filters().iter().any(|f| f.property() == property))
655 }
656
657 #[must_use]
667 pub fn tenant_only(&self) -> Self {
668 self.retain_properties(&[pep_properties::OWNER_TENANT_ID])
669 }
670
671 #[must_use]
680 pub fn tenant_and_owner(&self) -> Self {
681 self.retain_properties(&[pep_properties::OWNER_TENANT_ID, pep_properties::OWNER_ID])
682 }
683
684 #[must_use]
704 pub fn ensure_owner(&self, owner_id: Uuid) -> Self {
705 if self.is_deny_all() {
706 return Self::deny_all();
707 }
708
709 let owner_filter = ScopeFilter::eq(pep_properties::OWNER_ID, owner_id);
710
711 if self.unconstrained {
712 return Self::single(ScopeConstraint::new(vec![owner_filter]));
713 }
714
715 let constraints = self
716 .constraints
717 .iter()
718 .filter_map(|c| {
719 let owner_filters: Vec<&ScopeFilter> = c
720 .filters()
721 .iter()
722 .filter(|f| f.property() == pep_properties::OWNER_ID)
723 .collect();
724
725 if owner_filters.is_empty() {
726 let mut filters = c.filters().to_vec();
727 filters.push(owner_filter.clone());
728 return Some(ScopeConstraint::new(filters));
729 }
730
731 let all_match = owner_filters
734 .iter()
735 .all(|f| f.values().iter().any(|v| v.as_uuid() == Some(owner_id)));
736 if !all_match {
737 return None;
738 }
739
740 if owner_filters.len() == 1 && matches!(owner_filters[0], ScopeFilter::Eq(_)) {
742 return Some(c.clone());
743 }
744
745 let mut filters: Vec<ScopeFilter> = c
747 .filters()
748 .iter()
749 .filter(|f| f.property() != pep_properties::OWNER_ID)
750 .cloned()
751 .collect();
752 filters.push(owner_filter.clone());
753 Some(ScopeConstraint::new(filters))
754 })
755 .collect();
756
757 Self::from_constraints(constraints)
758 }
759
760 fn retain_properties(&self, properties: &[&str]) -> Self {
763 if self.unconstrained {
764 return Self::deny_all();
765 }
766
767 let constraints = self
768 .constraints
769 .iter()
770 .filter_map(|c| {
771 let kept: Vec<ScopeFilter> = c
772 .filters()
773 .iter()
774 .filter(|f| properties.contains(&f.property()))
775 .cloned()
776 .collect();
777
778 if kept.is_empty() {
779 None
780 } else {
781 Some(ScopeConstraint::new(kept))
782 }
783 })
784 .collect();
785
786 Self::from_constraints(constraints)
787 }
788}
789
790#[cfg(test)]
791#[cfg_attr(coverage_nightly, coverage(off))]
792mod tests {
793 use super::*;
794 use uuid::Uuid;
795
796 const T1: &str = "11111111-1111-1111-1111-111111111111";
797 const T2: &str = "22222222-2222-2222-2222-222222222222";
798
799 fn uid(s: &str) -> Uuid {
800 Uuid::parse_str(s).unwrap()
801 }
802
803 #[test]
806 fn scope_filter_eq_constructor() {
807 let f = ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1));
808 assert_eq!(f.property(), pep_properties::OWNER_TENANT_ID);
809 assert!(matches!(f, ScopeFilter::Eq(_)));
810 assert!(f.values().contains(&ScopeValue::Uuid(uid(T1))));
811 }
812
813 #[test]
814 fn all_values_for_works_with_eq() {
815 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
816 pep_properties::OWNER_TENANT_ID,
817 uid(T1),
818 )]));
819 assert_eq!(
820 scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
821 &[uid(T1)]
822 );
823 }
824
825 #[test]
826 fn all_values_for_works_with_mixed_eq_and_in() {
827 let scope = AccessScope::from_constraints(vec![
828 ScopeConstraint::new(vec![ScopeFilter::eq(
829 pep_properties::OWNER_TENANT_ID,
830 uid(T1),
831 )]),
832 ScopeConstraint::new(vec![ScopeFilter::in_uuids(
833 pep_properties::OWNER_TENANT_ID,
834 vec![uid(T2)],
835 )]),
836 ]);
837 let values = scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID);
838 assert_eq!(values, &[uid(T1), uid(T2)]);
839 }
840
841 #[test]
842 fn contains_value_works_with_eq() {
843 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
844 pep_properties::OWNER_TENANT_ID,
845 uid(T1),
846 )]));
847 assert!(scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
848 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T2)));
849 }
850
851 #[test]
854 fn tenant_only_strips_owner_id() {
855 let scope = AccessScope::single(ScopeConstraint::new(vec![
856 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
857 ScopeFilter::eq(pep_properties::OWNER_ID, uid(T2)),
858 ]));
859
860 let tenant_scope = scope.tenant_only();
861 assert!(tenant_scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
862 assert!(!tenant_scope.has_property(pep_properties::OWNER_ID));
863 }
864
865 #[test]
866 fn tenant_only_unconstrained_becomes_deny_all() {
867 let scope = AccessScope::allow_all();
868 let tenant_scope = scope.tenant_only();
869 assert!(tenant_scope.is_deny_all());
870 }
871
872 #[test]
873 fn tenant_only_deny_all_when_no_tenant_filters() {
874 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
875 pep_properties::OWNER_ID,
876 uid(T1),
877 )]));
878
879 let tenant_scope = scope.tenant_only();
880 assert!(tenant_scope.is_deny_all());
881 }
882
883 #[test]
884 fn tenant_only_on_deny_all_stays_deny_all() {
885 let scope = AccessScope::deny_all();
886 let tenant_scope = scope.tenant_only();
887 assert!(tenant_scope.is_deny_all());
888 }
889
890 #[test]
893 fn tenant_and_owner_keeps_both_properties() {
894 let scope = AccessScope::single(ScopeConstraint::new(vec![
895 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
896 ScopeFilter::eq(pep_properties::OWNER_ID, uid(T2)),
897 ScopeFilter::eq(pep_properties::RESOURCE_ID, uid(T1)),
898 ]));
899
900 let narrowed = scope.tenant_and_owner();
901 assert!(narrowed.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
902 assert!(narrowed.contains_uuid(pep_properties::OWNER_ID, uid(T2)));
903 assert!(!narrowed.has_property(pep_properties::RESOURCE_ID));
904 }
905
906 #[test]
907 fn tenant_and_owner_unconstrained_becomes_deny_all() {
908 let scope = AccessScope::allow_all();
909 assert!(scope.tenant_and_owner().is_deny_all());
910 }
911
912 #[test]
913 fn tenant_and_owner_deny_all_when_no_matching_filters() {
914 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
915 pep_properties::RESOURCE_ID,
916 uid(T1),
917 )]));
918 assert!(scope.tenant_and_owner().is_deny_all());
919 }
920
921 #[test]
924 fn ensure_owner_adds_owner_when_missing() {
925 let scope = AccessScope::for_tenant(uid(T1));
926 let owner_id = uid(T2);
927
928 let scoped = scope.ensure_owner(owner_id);
929 assert!(scoped.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
930 assert!(scoped.contains_uuid(pep_properties::OWNER_ID, owner_id));
931 }
932
933 #[test]
934 fn ensure_owner_keeps_existing_owner() {
935 let existing_owner = uid(T2);
936 let scope = AccessScope::single(ScopeConstraint::new(vec![
937 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
938 ScopeFilter::eq(pep_properties::OWNER_ID, existing_owner),
939 ]));
940
941 let scoped = scope.ensure_owner(existing_owner);
942 assert_eq!(
943 scoped.all_uuid_values_for(pep_properties::OWNER_ID),
944 &[existing_owner]
945 );
946 }
947
948 #[test]
949 fn ensure_owner_on_unconstrained_creates_owner_scope() {
950 let scope = AccessScope::allow_all();
951 let owner_id = uid(T1);
952
953 let scoped = scope.ensure_owner(owner_id);
954 assert!(!scoped.is_unconstrained());
955 assert!(scoped.contains_uuid(pep_properties::OWNER_ID, owner_id));
956 }
957
958 #[test]
959 fn ensure_owner_on_deny_all_stays_deny_all() {
960 let scope = AccessScope::deny_all();
961 let scoped = scope.ensure_owner(uid(T1));
962 assert!(scoped.is_deny_all());
963 }
964
965 #[test]
966 fn ensure_owner_narrows_existing_owner_to_subject() {
967 let user_a = uid(T1);
968 let user_b = uid(T2);
969 let scope = AccessScope::single(ScopeConstraint::new(vec![
970 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
971 ScopeFilter::in_uuids(pep_properties::OWNER_ID, vec![user_a, user_b]),
972 ]));
973
974 let scoped = scope.ensure_owner(user_a);
975 assert_eq!(
976 scoped.all_uuid_values_for(pep_properties::OWNER_ID),
977 &[user_a],
978 "Must narrow to exactly the subject's owner_id"
979 );
980 assert!(scoped.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
981 }
982
983 #[test]
984 fn ensure_owner_drops_constraint_when_subject_not_in_pdp() {
985 let user_x = uid(T1);
986 let user_y = uid(T2);
987 let scope = AccessScope::single(ScopeConstraint::new(vec![
988 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
989 ScopeFilter::eq(pep_properties::OWNER_ID, user_x),
990 ]));
991
992 let scoped = scope.ensure_owner(user_y);
993 assert!(
994 scoped.is_deny_all(),
995 "Must be deny-all when subject not in PDP's owner set"
996 );
997 }
998
999 #[test]
1000 fn ensure_owner_checks_all_owner_filters_in_constraint() {
1001 let alice = uid(T1);
1002 let bob = uid(T2);
1003 let scope = AccessScope::single(ScopeConstraint::new(vec![
1006 ScopeFilter::in_uuids(pep_properties::OWNER_ID, vec![alice, bob]),
1007 ScopeFilter::in_uuids(pep_properties::OWNER_ID, vec![bob]),
1008 ]));
1009
1010 let scoped = scope.ensure_owner(alice);
1011 assert!(
1012 scoped.is_deny_all(),
1013 "Must deny when subject is missing from any owner_id filter"
1014 );
1015
1016 let scoped = scope.ensure_owner(bob);
1018 assert!(!scoped.is_deny_all());
1019 assert_eq!(
1020 scoped.all_uuid_values_for(pep_properties::OWNER_ID),
1021 &[bob],
1022 "Must narrow to single Eq for the matching owner"
1023 );
1024 }
1025
1026 #[test]
1027 fn ensure_owner_multi_constraint_keeps_only_matching() {
1028 let alice = uid(T1);
1029 let bob = uid(T2);
1030 let tenant = uid(T1);
1031
1032 let c1 = ScopeConstraint::new(vec![
1034 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, tenant),
1035 ScopeFilter::eq(pep_properties::OWNER_ID, alice),
1036 ]);
1037 let c2 = ScopeConstraint::new(vec![
1039 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, tenant),
1040 ScopeFilter::eq(pep_properties::OWNER_ID, bob),
1041 ]);
1042
1043 let scope = AccessScope::from_constraints(vec![c1, c2]);
1044 let scoped = scope.ensure_owner(alice);
1045
1046 assert!(
1047 !scoped.is_deny_all(),
1048 "Must not be deny-all - one constraint matches"
1049 );
1050 assert_eq!(
1051 scoped.all_uuid_values_for(pep_properties::OWNER_ID),
1052 &[alice],
1053 "Must keep only the constraint matching alice"
1054 );
1055 assert!(
1056 scoped.contains_uuid(pep_properties::OWNER_TENANT_ID, tenant),
1057 "Tenant filter must be preserved"
1058 );
1059 }
1060
1061 #[test]
1064 fn scope_filter_in_group_constructor() {
1065 let f = ScopeFilter::in_group(
1066 pep_properties::OWNER_TENANT_ID,
1067 vec![ScopeValue::Uuid(uid(T1))],
1068 );
1069 assert_eq!(f.property(), pep_properties::OWNER_TENANT_ID);
1070 assert!(matches!(f, ScopeFilter::InGroup(_)));
1071 assert_eq!(f.values().iter().count(), 0);
1072 }
1073
1074 #[test]
1077 fn scope_filter_in_group_subtree_constructor() {
1078 let f = ScopeFilter::in_group_subtree(
1079 pep_properties::OWNER_TENANT_ID,
1080 vec![ScopeValue::Uuid(uid(T1))],
1081 );
1082 assert_eq!(f.property(), pep_properties::OWNER_TENANT_ID);
1083 assert!(matches!(f, ScopeFilter::InGroupSubtree(_)));
1084 assert_eq!(f.values().iter().count(), 0);
1085 }
1086
1087 #[test]
1088 fn in_group_scope_contains_uuid_returns_false() {
1089 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::in_group(
1090 pep_properties::OWNER_TENANT_ID,
1091 vec![ScopeValue::Uuid(uid(T1))],
1092 )]));
1093 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1094 }
1095
1096 #[test]
1097 fn in_group_subtree_scope_contains_uuid_returns_false() {
1098 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::in_group_subtree(
1099 pep_properties::OWNER_TENANT_ID,
1100 vec![ScopeValue::Uuid(uid(T1))],
1101 )]));
1102 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1103 }
1104
1105 #[test]
1108 fn contains_uuid_matches_string_variant() {
1109 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
1110 pep_properties::OWNER_TENANT_ID,
1111 ScopeValue::String(T1.to_owned()),
1112 )]));
1113 assert!(scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1114 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T2)));
1115 }
1116
1117 #[test]
1118 fn contains_uuid_does_not_match_invalid_string() {
1119 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
1120 pep_properties::OWNER_TENANT_ID,
1121 ScopeValue::String("not-a-uuid".to_owned()),
1122 )]));
1123 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1124 }
1125}