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
105#[derive(Clone, Debug, PartialEq, Eq)]
124#[non_exhaustive]
125pub enum ScopeFilter {
126 Eq(EqScopeFilter),
128 In(InScopeFilter),
130 InGroup(InGroupScopeFilter),
132 InGroupSubtree(InGroupSubtreeScopeFilter),
134 InTenantSubtree(InTenantSubtreeScopeFilter),
136}
137
138#[derive(Clone, Debug, PartialEq, Eq, Hash)]
140pub struct EqScopeFilter {
141 property: String,
143 value: ScopeValue,
145}
146
147#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct InScopeFilter {
150 property: String,
152 values: Vec<ScopeValue>,
154}
155
156impl EqScopeFilter {
157 #[must_use]
159 pub fn new(property: impl Into<String>, value: impl Into<ScopeValue>) -> Self {
160 Self {
161 property: property.into(),
162 value: value.into(),
163 }
164 }
165
166 #[inline]
168 #[must_use]
169 pub fn property(&self) -> &str {
170 &self.property
171 }
172
173 #[inline]
175 #[must_use]
176 pub fn value(&self) -> &ScopeValue {
177 &self.value
178 }
179}
180
181impl InScopeFilter {
182 #[must_use]
184 pub fn new(property: impl Into<String>, values: Vec<ScopeValue>) -> Self {
185 Self {
186 property: property.into(),
187 values,
188 }
189 }
190
191 #[must_use]
193 pub fn from_values<V: Into<ScopeValue>>(
194 property: impl Into<String>,
195 values: impl IntoIterator<Item = V>,
196 ) -> Self {
197 Self {
198 property: property.into(),
199 values: values.into_iter().map(Into::into).collect(),
200 }
201 }
202
203 #[inline]
205 #[must_use]
206 pub fn property(&self) -> &str {
207 &self.property
208 }
209
210 #[inline]
212 #[must_use]
213 pub fn values(&self) -> &[ScopeValue] {
214 &self.values
215 }
216}
217
218#[derive(Clone, Debug, PartialEq, Eq)]
220pub struct InGroupScopeFilter {
221 property: String,
222 group_ids: Vec<ScopeValue>,
223}
224
225impl InGroupScopeFilter {
226 #[must_use]
228 pub fn new(property: impl Into<String>, group_ids: Vec<ScopeValue>) -> Self {
229 Self {
230 property: property.into(),
231 group_ids,
232 }
233 }
234
235 #[inline]
237 #[must_use]
238 pub fn property(&self) -> &str {
239 &self.property
240 }
241
242 #[inline]
244 #[must_use]
245 pub fn group_ids(&self) -> &[ScopeValue] {
246 &self.group_ids
247 }
248}
249
250#[derive(Clone, Debug, PartialEq, Eq)]
252pub struct InGroupSubtreeScopeFilter {
253 property: String,
254 ancestor_ids: Vec<ScopeValue>,
255}
256
257impl InGroupSubtreeScopeFilter {
258 #[must_use]
260 pub fn new(property: impl Into<String>, ancestor_ids: Vec<ScopeValue>) -> Self {
261 Self {
262 property: property.into(),
263 ancestor_ids,
264 }
265 }
266
267 #[inline]
269 #[must_use]
270 pub fn property(&self) -> &str {
271 &self.property
272 }
273
274 #[inline]
276 #[must_use]
277 pub fn ancestor_ids(&self) -> &[ScopeValue] {
278 &self.ancestor_ids
279 }
280}
281
282#[derive(Clone, Debug, PartialEq, Eq)]
305pub struct InTenantSubtreeScopeFilter {
306 property: String,
307 root_tenant_id: ScopeValue,
308 respect_barriers: bool,
309 descendant_status: Vec<ScopeValue>,
310}
311
312impl InTenantSubtreeScopeFilter {
313 #[must_use]
319 pub fn new(property: impl Into<String>, root_tenant_id: impl Into<ScopeValue>) -> Self {
320 Self::with_respect_barriers(property, root_tenant_id, true)
321 }
322
323 #[must_use]
326 pub fn with_respect_barriers(
327 property: impl Into<String>,
328 root_tenant_id: impl Into<ScopeValue>,
329 respect_barriers: bool,
330 ) -> Self {
331 Self::with_descendant_status(property, root_tenant_id, respect_barriers, Vec::new())
332 }
333
334 #[must_use]
338 pub fn with_descendant_status(
339 property: impl Into<String>,
340 root_tenant_id: impl Into<ScopeValue>,
341 respect_barriers: bool,
342 descendant_status: Vec<ScopeValue>,
343 ) -> Self {
344 Self {
345 property: property.into(),
346 root_tenant_id: root_tenant_id.into(),
347 respect_barriers,
348 descendant_status,
349 }
350 }
351
352 #[inline]
354 #[must_use]
355 pub fn property(&self) -> &str {
356 &self.property
357 }
358
359 #[inline]
361 #[must_use]
362 pub fn root_tenant_id(&self) -> &ScopeValue {
363 &self.root_tenant_id
364 }
365
366 #[inline]
369 #[must_use]
370 pub fn respect_barriers(&self) -> bool {
371 self.respect_barriers
372 }
373
374 #[inline]
381 #[must_use]
382 pub fn descendant_status(&self) -> &[ScopeValue] {
383 &self.descendant_status
384 }
385}
386
387impl ScopeFilter {
388 #[must_use]
390 pub fn eq(property: impl Into<String>, value: impl Into<ScopeValue>) -> Self {
391 Self::Eq(EqScopeFilter::new(property, value))
392 }
393
394 #[must_use]
396 pub fn r#in(property: impl Into<String>, values: Vec<ScopeValue>) -> Self {
397 Self::In(InScopeFilter::new(property, values))
398 }
399
400 #[must_use]
402 pub fn in_uuids(property: impl Into<String>, uuids: Vec<Uuid>) -> Self {
403 Self::In(InScopeFilter::new(
404 property,
405 uuids.into_iter().map(ScopeValue::Uuid).collect(),
406 ))
407 }
408
409 #[must_use]
411 pub fn in_group(property: impl Into<String>, group_ids: Vec<ScopeValue>) -> Self {
412 Self::InGroup(InGroupScopeFilter::new(property, group_ids))
413 }
414
415 #[must_use]
417 pub fn in_group_subtree(property: impl Into<String>, ancestor_ids: Vec<ScopeValue>) -> Self {
418 Self::InGroupSubtree(InGroupSubtreeScopeFilter::new(property, ancestor_ids))
419 }
420
421 #[must_use]
428 pub fn in_tenant_subtree(
429 property: impl Into<String>,
430 root_tenant_id: impl Into<ScopeValue>,
431 respect_barriers: bool,
432 descendant_status: Vec<ScopeValue>,
433 ) -> Self {
434 Self::InTenantSubtree(InTenantSubtreeScopeFilter::with_descendant_status(
435 property,
436 root_tenant_id,
437 respect_barriers,
438 descendant_status,
439 ))
440 }
441
442 #[must_use]
444 pub fn property(&self) -> &str {
445 match self {
446 Self::Eq(f) => f.property(),
447 Self::In(f) => f.property(),
448 Self::InGroup(f) => f.property(),
449 Self::InGroupSubtree(f) => f.property(),
450 Self::InTenantSubtree(f) => f.property(),
451 }
452 }
453
454 #[must_use]
468 pub fn values(&self) -> ScopeFilterValues<'_> {
469 match self {
470 Self::Eq(f) => ScopeFilterValues::Single(&f.value),
471 Self::In(f) => ScopeFilterValues::Multiple(&f.values),
472 Self::InGroup(_) | Self::InGroupSubtree(_) | Self::InTenantSubtree(_) => {
473 ScopeFilterValues::Multiple(&[])
474 }
475 }
476 }
477
478 #[must_use]
489 pub fn is_representable_in_memory(&self) -> bool {
490 match self {
491 Self::Eq(_) | Self::In(_) => true,
492 Self::InGroup(_) | Self::InGroupSubtree(_) | Self::InTenantSubtree(_) => false,
493 }
494 }
495
496 #[must_use]
501 pub fn uuid_values(&self) -> Vec<Uuid> {
502 self.values()
503 .iter()
504 .filter_map(ScopeValue::as_uuid)
505 .collect()
506 }
507}
508
509#[derive(Clone, Debug)]
514pub enum ScopeFilterValues<'a> {
515 Single(&'a ScopeValue),
517 Multiple(&'a [ScopeValue]),
519}
520
521impl<'a> ScopeFilterValues<'a> {
522 #[must_use]
524 pub fn iter(&self) -> ScopeFilterValuesIter<'a> {
525 match self {
526 Self::Single(v) => ScopeFilterValuesIter(ScopeFilterValuesIterInner::Single(Some(v))),
527 Self::Multiple(vs) => {
528 ScopeFilterValuesIter(ScopeFilterValuesIterInner::Multiple(vs.iter()))
529 }
530 }
531 }
532
533 #[must_use]
535 pub fn contains(&self, value: &ScopeValue) -> bool {
536 self.iter().any(|v| v == value)
537 }
538}
539
540impl<'a> IntoIterator for ScopeFilterValues<'a> {
541 type Item = &'a ScopeValue;
542 type IntoIter = ScopeFilterValuesIter<'a>;
543
544 fn into_iter(self) -> Self::IntoIter {
545 self.iter()
546 }
547}
548
549impl<'a> IntoIterator for &ScopeFilterValues<'a> {
550 type Item = &'a ScopeValue;
551 type IntoIter = ScopeFilterValuesIter<'a>;
552
553 fn into_iter(self) -> Self::IntoIter {
554 self.iter()
555 }
556}
557
558#[derive(Debug, Clone)]
560pub struct ScopeFilterValuesIter<'a>(ScopeFilterValuesIterInner<'a>);
561
562#[derive(Debug, Clone)]
568enum ScopeFilterValuesIterInner<'a> {
569 Single(Option<&'a ScopeValue>),
571 Multiple(std::slice::Iter<'a, ScopeValue>),
573}
574
575impl<'a> Iterator for ScopeFilterValuesIter<'a> {
576 type Item = &'a ScopeValue;
577
578 fn next(&mut self) -> Option<Self::Item> {
579 match &mut self.0 {
580 ScopeFilterValuesIterInner::Single(v) => v.take(),
581 ScopeFilterValuesIterInner::Multiple(iter) => iter.next(),
582 }
583 }
584
585 fn size_hint(&self) -> (usize, Option<usize>) {
586 match &self.0 {
587 ScopeFilterValuesIterInner::Single(v) => {
588 let n = usize::from(v.is_some());
589 (n, Some(n))
590 }
591 ScopeFilterValuesIterInner::Multiple(iter) => iter.size_hint(),
592 }
593 }
594}
595
596impl ExactSizeIterator for ScopeFilterValuesIter<'_> {}
597
598#[derive(Clone, Debug, PartialEq)]
603pub struct ScopeConstraint {
604 filters: Vec<ScopeFilter>,
605}
606
607#[derive(Debug, thiserror::Error, PartialEq, Eq)]
615#[error("a scope constraint must carry at least one filter; an empty one matches every row")]
616pub struct EmptyScopeConstraint;
617
618impl ScopeConstraint {
619 pub fn try_new(filters: Vec<ScopeFilter>) -> Result<Self, EmptyScopeConstraint> {
629 if filters.is_empty() {
630 return Err(EmptyScopeConstraint);
631 }
632 Ok(Self { filters })
633 }
634
635 #[must_use]
645 pub fn new(filters: Vec<ScopeFilter>) -> Self {
646 assert!(
647 !filters.is_empty(),
648 "a scope constraint must carry at least one filter; an empty one matches every row"
649 );
650 Self { filters }
651 }
652
653 #[inline]
655 #[must_use]
656 pub fn filters(&self) -> &[ScopeFilter] {
657 &self.filters
658 }
659
660 #[inline]
662 #[must_use]
663 pub fn is_empty(&self) -> bool {
664 self.filters.is_empty()
665 }
666}
667
668#[derive(Clone, Debug, PartialEq)]
690pub struct AccessScope {
691 constraints: Vec<ScopeConstraint>,
692 unconstrained: bool,
693}
694
695impl Default for AccessScope {
696 fn default() -> Self {
698 Self::deny_all()
699 }
700}
701
702impl AccessScope {
703 #[must_use]
707 pub fn from_constraints(constraints: Vec<ScopeConstraint>) -> Self {
708 Self {
709 constraints,
710 unconstrained: false,
711 }
712 }
713
714 #[must_use]
716 pub fn single(constraint: ScopeConstraint) -> Self {
717 Self::from_constraints(vec![constraint])
718 }
719
720 #[must_use]
725 pub fn allow_all() -> Self {
726 Self {
727 constraints: Vec::new(),
728 unconstrained: true,
729 }
730 }
731
732 #[must_use]
734 pub fn deny_all() -> Self {
735 Self {
736 constraints: Vec::new(),
737 unconstrained: false,
738 }
739 }
740
741 #[must_use]
745 pub fn for_tenants(ids: Vec<Uuid>) -> Self {
746 Self::single(ScopeConstraint::new(vec![ScopeFilter::in_uuids(
747 pep_properties::OWNER_TENANT_ID,
748 ids,
749 )]))
750 }
751
752 #[must_use]
754 pub fn for_tenant(id: Uuid) -> Self {
755 Self::for_tenants(vec![id])
756 }
757
758 #[must_use]
760 pub fn for_resources(ids: Vec<Uuid>) -> Self {
761 Self::single(ScopeConstraint::new(vec![ScopeFilter::in_uuids(
762 pep_properties::RESOURCE_ID,
763 ids,
764 )]))
765 }
766
767 #[must_use]
769 pub fn for_resource(id: Uuid) -> Self {
770 Self::for_resources(vec![id])
771 }
772
773 #[inline]
777 #[must_use]
778 pub fn constraints(&self) -> &[ScopeConstraint] {
779 &self.constraints
780 }
781
782 #[inline]
784 #[must_use]
785 pub fn is_unconstrained(&self) -> bool {
786 self.unconstrained
787 }
788
789 #[must_use]
793 pub fn is_deny_all(&self) -> bool {
794 !self.unconstrained && self.constraints.is_empty()
795 }
796
797 #[must_use]
806 pub fn all_values_for(&self, property: &str) -> Vec<&ScopeValue> {
807 let mut result = Vec::new();
808 for constraint in &self.constraints {
809 for filter in constraint.filters() {
810 if filter.property() == property {
811 result.extend(filter.values());
812 }
813 }
814 }
815 result
816 }
817
818 #[must_use]
826 pub fn all_uuid_values_for(&self, property: &str) -> Vec<Uuid> {
827 let mut result = Vec::new();
828 for constraint in &self.constraints {
829 for filter in constraint.filters() {
830 if filter.property() == property {
831 result.extend(filter.uuid_values());
832 }
833 }
834 }
835 result
836 }
837
838 #[must_use]
868 pub fn contains_uuid(&self, property: &str, id: Uuid) -> bool {
869 self.constraints.iter().any(|c| {
870 c.filters().iter().any(|f| {
871 f.property() == property && f.values().iter().any(|v| v.as_uuid() == Some(id))
872 })
873 })
874 }
875
876 #[must_use]
882 pub fn has_property(&self, property: &str) -> bool {
883 self.constraints
884 .iter()
885 .any(|c| c.filters().iter().any(|f| f.property() == property))
886 }
887
888 #[must_use]
913 pub fn tenant_only(&self) -> Self {
914 self.retain_properties(&[pep_properties::OWNER_TENANT_ID])
915 }
916
917 #[must_use]
929 pub fn tenant_and_owner(&self) -> Self {
930 self.retain_properties(&[pep_properties::OWNER_TENANT_ID, pep_properties::OWNER_ID])
931 }
932
933 #[must_use]
953 pub fn ensure_owner(&self, owner_id: Uuid) -> Self {
954 if self.is_deny_all() {
955 return Self::deny_all();
956 }
957
958 let owner_filter = ScopeFilter::eq(pep_properties::OWNER_ID, owner_id);
959
960 if self.unconstrained {
961 return Self::single(ScopeConstraint::new(vec![owner_filter]));
962 }
963
964 let constraints = self
965 .constraints
966 .iter()
967 .filter_map(|c| {
968 let owner_filters: Vec<&ScopeFilter> = c
969 .filters()
970 .iter()
971 .filter(|f| f.property() == pep_properties::OWNER_ID)
972 .collect();
973
974 if owner_filters.is_empty() {
975 let mut filters = c.filters().to_vec();
976 filters.push(owner_filter.clone());
977 return Some(ScopeConstraint::new(filters));
978 }
979
980 let all_match = owner_filters
983 .iter()
984 .all(|f| f.values().iter().any(|v| v.as_uuid() == Some(owner_id)));
985 if !all_match {
986 return None;
987 }
988
989 if owner_filters.len() == 1 && matches!(owner_filters[0], ScopeFilter::Eq(_)) {
991 return Some(c.clone());
992 }
993
994 let mut filters: Vec<ScopeFilter> = c
996 .filters()
997 .iter()
998 .filter(|f| f.property() != pep_properties::OWNER_ID)
999 .cloned()
1000 .collect();
1001 filters.push(owner_filter.clone());
1002 Some(ScopeConstraint::new(filters))
1003 })
1004 .collect();
1005
1006 Self::from_constraints(constraints)
1007 }
1008
1009 fn retain_properties(&self, properties: &[&str]) -> Self {
1012 if self.unconstrained {
1013 return Self::deny_all();
1014 }
1015
1016 let constraints = self
1017 .constraints
1018 .iter()
1019 .filter_map(|c| {
1020 let kept: Vec<ScopeFilter> = c
1021 .filters()
1022 .iter()
1023 .filter(|f| properties.contains(&f.property()))
1024 .cloned()
1025 .collect();
1026
1027 if kept.is_empty() {
1028 None
1029 } else {
1030 Some(ScopeConstraint::new(kept))
1031 }
1032 })
1033 .collect();
1034
1035 Self::from_constraints(constraints)
1036 }
1037}
1038
1039#[cfg(test)]
1040#[cfg_attr(coverage_nightly, coverage(off))]
1041mod tests {
1042 use super::*;
1043 use uuid::Uuid;
1044
1045 const T1: &str = "11111111-1111-1111-1111-111111111111";
1046 const T2: &str = "22222222-2222-2222-2222-222222222222";
1047
1048 fn uid(s: &str) -> Uuid {
1049 Uuid::parse_str(s).unwrap()
1050 }
1051
1052 #[test]
1055 fn scope_filter_eq_exposes_exactly_one_value() {
1056 let f = ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1));
1057 assert_eq!(f.property(), pep_properties::OWNER_TENANT_ID);
1058 assert!(matches!(f, ScopeFilter::Eq(_)));
1059
1060 assert_eq!(f.values().iter().count(), 1);
1064 assert_eq!(f.uuid_values(), vec![uid(T1)]);
1065 }
1066
1067 #[test]
1068 fn all_values_for_works_with_eq() {
1069 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
1070 pep_properties::OWNER_TENANT_ID,
1071 uid(T1),
1072 )]));
1073 assert_eq!(
1074 scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
1075 &[uid(T1)]
1076 );
1077 }
1078
1079 #[test]
1080 fn all_values_for_works_with_mixed_eq_and_in() {
1081 let scope = AccessScope::from_constraints(vec![
1082 ScopeConstraint::new(vec![ScopeFilter::eq(
1083 pep_properties::OWNER_TENANT_ID,
1084 uid(T1),
1085 )]),
1086 ScopeConstraint::new(vec![ScopeFilter::in_uuids(
1087 pep_properties::OWNER_TENANT_ID,
1088 vec![uid(T2)],
1089 )]),
1090 ]);
1091 let values = scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID);
1092 assert_eq!(values, &[uid(T1), uid(T2)]);
1093 }
1094
1095 #[test]
1096 fn contains_uuid_works_with_eq() {
1097 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
1098 pep_properties::OWNER_TENANT_ID,
1099 uid(T1),
1100 )]));
1101 assert!(scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1102 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T2)));
1103 }
1104
1105 #[test]
1106 fn a_subquery_filter_reports_that_it_cannot_be_decided_in_memory() {
1107 let empty_in = ScopeFilter::r#in(pep_properties::OWNER_TENANT_ID, vec![]);
1112 assert_eq!(empty_in.values().iter().count(), 0);
1113 assert!(
1114 empty_in.is_representable_in_memory(),
1115 "an In filter with no values is a real, decidable negative"
1116 );
1117
1118 for subquery in [
1119 ScopeFilter::in_group(pep_properties::RESOURCE_ID, vec![ScopeValue::Uuid(uid(T1))]),
1120 ScopeFilter::in_group_subtree(
1121 pep_properties::RESOURCE_ID,
1122 vec![ScopeValue::Uuid(uid(T1))],
1123 ),
1124 ] {
1125 assert_eq!(subquery.values().iter().count(), 0);
1126 assert!(
1127 !subquery.is_representable_in_memory(),
1128 "a subquery filter's empty value view is not a negative"
1129 );
1130 }
1131 }
1132
1133 #[test]
1134 fn contains_uuid_matches_a_uuid_held_as_a_string() {
1135 let as_text = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
1140 pep_properties::OWNER_TENANT_ID,
1141 ScopeValue::String(uid(T1).to_string()),
1142 )]));
1143 assert!(as_text.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1144 assert!(!as_text.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T2)));
1145
1146 let typed = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
1147 pep_properties::OWNER_TENANT_ID,
1148 uid(T1),
1149 )]));
1150 assert!(typed.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1151 }
1152
1153 #[test]
1154 fn contains_uuid_is_false_for_a_subquery_filter() {
1155 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::in_group(
1159 pep_properties::RESOURCE_ID,
1160 vec![ScopeValue::Uuid(uid(T1))],
1161 )]));
1162 assert!(!scope.contains_uuid(pep_properties::RESOURCE_ID, uid(T1)));
1163 }
1164
1165 #[test]
1166 fn contains_uuid_does_not_evaluate_the_conjunction() {
1167 let scope = AccessScope::single(ScopeConstraint::new(vec![
1172 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
1173 ScopeFilter::eq(pep_properties::OWNER_ID, uid(T2)),
1174 ]));
1175
1176 assert!(scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1177 assert!(
1178 !scope.contains_uuid(pep_properties::OWNER_ID, uid(T1)),
1179 "guard: T1 is the tenant, not the owner"
1180 );
1181 }
1182
1183 #[test]
1184 fn an_allow_all_scope_permits_everything_it_reports_no_constraint_for() {
1185 let scope = AccessScope::allow_all();
1189
1190 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1191 assert!(!scope.has_property(pep_properties::OWNER_TENANT_ID));
1192 assert!(
1193 scope
1194 .all_uuid_values_for(pep_properties::OWNER_TENANT_ID)
1195 .is_empty()
1196 );
1197
1198 assert!(scope.is_unconstrained());
1202 assert!(!AccessScope::deny_all().is_unconstrained());
1203 }
1204
1205 #[test]
1206 fn an_empty_constraint_is_refused() {
1207 assert_eq!(
1212 ScopeConstraint::try_new(vec![]).unwrap_err(),
1213 EmptyScopeConstraint
1214 );
1215
1216 assert!(
1217 ScopeConstraint::try_new(vec![ScopeFilter::eq(
1218 pep_properties::OWNER_TENANT_ID,
1219 uid(T1)
1220 )])
1221 .is_ok(),
1222 "one filter is enough to narrow"
1223 );
1224 }
1225
1226 #[test]
1227 #[should_panic(expected = "must carry at least one filter")]
1228 fn the_infallible_constructor_refuses_an_empty_list_too() {
1229 drop(ScopeConstraint::new(vec![]));
1233 }
1234
1235 #[test]
1236 fn a_deny_all_scope_permits_nothing() {
1237 let scope = AccessScope::deny_all();
1238 assert!(scope.is_deny_all());
1239 assert!(!scope.is_unconstrained());
1240 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1241 }
1242
1243 #[test]
1246 fn tenant_only_strips_owner_id() {
1247 let scope = AccessScope::single(ScopeConstraint::new(vec![
1248 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
1249 ScopeFilter::eq(pep_properties::OWNER_ID, uid(T2)),
1250 ]));
1251
1252 let tenant_scope = scope.tenant_only();
1253 assert!(tenant_scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1254 assert!(!tenant_scope.has_property(pep_properties::OWNER_ID));
1255 }
1256
1257 #[test]
1258 fn tenant_only_widens_a_resource_scoped_grant_to_the_whole_tenant() {
1259 let scope = AccessScope::single(ScopeConstraint::new(vec![
1265 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
1266 ScopeFilter::eq(pep_properties::RESOURCE_ID, uid(T2)),
1267 ]));
1268
1269 let tenant_scope = scope.tenant_only();
1270 assert!(
1271 !tenant_scope.has_property(pep_properties::RESOURCE_ID),
1272 "the resource narrowing is gone, so this grant now covers the tenant"
1273 );
1274 assert!(tenant_scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1275 }
1276
1277 #[test]
1278 fn tenant_only_unconstrained_becomes_deny_all() {
1279 let scope = AccessScope::allow_all();
1280 let tenant_scope = scope.tenant_only();
1281 assert!(tenant_scope.is_deny_all());
1282 }
1283
1284 #[test]
1285 fn tenant_only_deny_all_when_no_tenant_filters() {
1286 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
1287 pep_properties::OWNER_ID,
1288 uid(T1),
1289 )]));
1290
1291 let tenant_scope = scope.tenant_only();
1292 assert!(tenant_scope.is_deny_all());
1293 }
1294
1295 #[test]
1296 fn tenant_only_on_deny_all_stays_deny_all() {
1297 let scope = AccessScope::deny_all();
1298 let tenant_scope = scope.tenant_only();
1299 assert!(tenant_scope.is_deny_all());
1300 }
1301
1302 #[test]
1305 fn tenant_and_owner_keeps_both_properties() {
1306 let scope = AccessScope::single(ScopeConstraint::new(vec![
1307 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
1308 ScopeFilter::eq(pep_properties::OWNER_ID, uid(T2)),
1309 ScopeFilter::eq(pep_properties::RESOURCE_ID, uid(T1)),
1310 ]));
1311
1312 let narrowed = scope.tenant_and_owner();
1313 assert!(narrowed.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1314 assert!(narrowed.contains_uuid(pep_properties::OWNER_ID, uid(T2)));
1315 assert!(!narrowed.has_property(pep_properties::RESOURCE_ID));
1316 }
1317
1318 #[test]
1319 fn tenant_and_owner_unconstrained_becomes_deny_all() {
1320 let scope = AccessScope::allow_all();
1321 assert!(scope.tenant_and_owner().is_deny_all());
1322 }
1323
1324 #[test]
1325 fn tenant_and_owner_deny_all_when_no_matching_filters() {
1326 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
1327 pep_properties::RESOURCE_ID,
1328 uid(T1),
1329 )]));
1330 assert!(scope.tenant_and_owner().is_deny_all());
1331 }
1332
1333 #[test]
1336 fn ensure_owner_adds_owner_when_missing() {
1337 let scope = AccessScope::for_tenant(uid(T1));
1338 let owner_id = uid(T2);
1339
1340 let scoped = scope.ensure_owner(owner_id);
1341 assert!(scoped.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1342 assert!(scoped.contains_uuid(pep_properties::OWNER_ID, owner_id));
1343 }
1344
1345 #[test]
1346 fn ensure_owner_keeps_existing_owner() {
1347 let existing_owner = uid(T2);
1348 let scope = AccessScope::single(ScopeConstraint::new(vec![
1349 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
1350 ScopeFilter::eq(pep_properties::OWNER_ID, existing_owner),
1351 ]));
1352
1353 let scoped = scope.ensure_owner(existing_owner);
1354 assert_eq!(
1355 scoped.all_uuid_values_for(pep_properties::OWNER_ID),
1356 &[existing_owner]
1357 );
1358 }
1359
1360 #[test]
1361 fn ensure_owner_on_unconstrained_creates_owner_scope() {
1362 let scope = AccessScope::allow_all();
1363 let owner_id = uid(T1);
1364
1365 let scoped = scope.ensure_owner(owner_id);
1366 assert!(!scoped.is_unconstrained());
1367 assert!(scoped.contains_uuid(pep_properties::OWNER_ID, owner_id));
1368 }
1369
1370 #[test]
1371 fn ensure_owner_on_deny_all_stays_deny_all() {
1372 let scope = AccessScope::deny_all();
1373 let scoped = scope.ensure_owner(uid(T1));
1374 assert!(scoped.is_deny_all());
1375 }
1376
1377 #[test]
1378 fn ensure_owner_narrows_existing_owner_to_subject() {
1379 let user_a = uid(T1);
1380 let user_b = uid(T2);
1381 let scope = AccessScope::single(ScopeConstraint::new(vec![
1382 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
1383 ScopeFilter::in_uuids(pep_properties::OWNER_ID, vec![user_a, user_b]),
1384 ]));
1385
1386 let scoped = scope.ensure_owner(user_a);
1387 assert_eq!(
1388 scoped.all_uuid_values_for(pep_properties::OWNER_ID),
1389 &[user_a],
1390 "Must narrow to exactly the subject's owner_id"
1391 );
1392 assert!(scoped.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1393 }
1394
1395 #[test]
1396 fn ensure_owner_drops_constraint_when_subject_not_in_pdp() {
1397 let user_x = uid(T1);
1398 let user_y = uid(T2);
1399 let scope = AccessScope::single(ScopeConstraint::new(vec![
1400 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
1401 ScopeFilter::eq(pep_properties::OWNER_ID, user_x),
1402 ]));
1403
1404 let scoped = scope.ensure_owner(user_y);
1405 assert!(
1406 scoped.is_deny_all(),
1407 "Must be deny-all when subject not in PDP's owner set"
1408 );
1409 }
1410
1411 #[test]
1412 fn ensure_owner_checks_all_owner_filters_in_constraint() {
1413 let alice = uid(T1);
1414 let bob = uid(T2);
1415 let scope = AccessScope::single(ScopeConstraint::new(vec![
1418 ScopeFilter::in_uuids(pep_properties::OWNER_ID, vec![alice, bob]),
1419 ScopeFilter::in_uuids(pep_properties::OWNER_ID, vec![bob]),
1420 ]));
1421
1422 let scoped = scope.ensure_owner(alice);
1423 assert!(
1424 scoped.is_deny_all(),
1425 "Must deny when subject is missing from any owner_id filter"
1426 );
1427
1428 let scoped = scope.ensure_owner(bob);
1430 assert!(!scoped.is_deny_all());
1431 assert_eq!(
1432 scoped.all_uuid_values_for(pep_properties::OWNER_ID),
1433 &[bob],
1434 "Must narrow to single Eq for the matching owner"
1435 );
1436 }
1437
1438 #[test]
1439 fn ensure_owner_multi_constraint_keeps_only_matching() {
1440 let alice = uid(T1);
1441 let bob = uid(T2);
1442 let tenant = uid(T1);
1443
1444 let c1 = ScopeConstraint::new(vec![
1446 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, tenant),
1447 ScopeFilter::eq(pep_properties::OWNER_ID, alice),
1448 ]);
1449 let c2 = ScopeConstraint::new(vec![
1451 ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, tenant),
1452 ScopeFilter::eq(pep_properties::OWNER_ID, bob),
1453 ]);
1454
1455 let scope = AccessScope::from_constraints(vec![c1, c2]);
1456 let scoped = scope.ensure_owner(alice);
1457
1458 assert!(
1459 !scoped.is_deny_all(),
1460 "Must not be deny-all - one constraint matches"
1461 );
1462 assert_eq!(
1463 scoped.all_uuid_values_for(pep_properties::OWNER_ID),
1464 &[alice],
1465 "Must keep only the constraint matching alice"
1466 );
1467 assert!(
1468 scoped.contains_uuid(pep_properties::OWNER_TENANT_ID, tenant),
1469 "Tenant filter must be preserved"
1470 );
1471 }
1472
1473 #[test]
1476 fn scope_filter_in_group_constructor() {
1477 let f = ScopeFilter::in_group(
1478 pep_properties::OWNER_TENANT_ID,
1479 vec![ScopeValue::Uuid(uid(T1))],
1480 );
1481 assert_eq!(f.property(), pep_properties::OWNER_TENANT_ID);
1482 assert!(matches!(f, ScopeFilter::InGroup(_)));
1483 assert_eq!(f.values().iter().count(), 0);
1484 }
1485
1486 #[test]
1489 fn scope_filter_in_group_subtree_constructor() {
1490 let f = ScopeFilter::in_group_subtree(
1491 pep_properties::OWNER_TENANT_ID,
1492 vec![ScopeValue::Uuid(uid(T1))],
1493 );
1494 assert_eq!(f.property(), pep_properties::OWNER_TENANT_ID);
1495 assert!(matches!(f, ScopeFilter::InGroupSubtree(_)));
1496 assert_eq!(f.values().iter().count(), 0);
1497 }
1498
1499 #[test]
1500 fn in_group_scope_contains_uuid_returns_false() {
1501 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::in_group(
1502 pep_properties::OWNER_TENANT_ID,
1503 vec![ScopeValue::Uuid(uid(T1))],
1504 )]));
1505 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1506 }
1507
1508 #[test]
1509 fn in_group_subtree_scope_contains_uuid_returns_false() {
1510 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::in_group_subtree(
1511 pep_properties::OWNER_TENANT_ID,
1512 vec![ScopeValue::Uuid(uid(T1))],
1513 )]));
1514 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1515 }
1516
1517 #[test]
1520 fn scope_filter_in_tenant_subtree_constructor_defaults_to_respect() {
1521 let f = ScopeFilter::in_tenant_subtree(
1522 pep_properties::RESOURCE_ID,
1523 ScopeValue::Uuid(uid(T1)),
1524 true,
1525 Vec::new(),
1526 );
1527 assert_eq!(f.property(), pep_properties::RESOURCE_ID);
1528 assert!(matches!(f, ScopeFilter::InTenantSubtree(_)));
1529 assert_eq!(f.values().iter().count(), 0);
1530 match &f {
1531 ScopeFilter::InTenantSubtree(sf) => {
1532 assert!(sf.respect_barriers());
1533 assert!(sf.descendant_status().is_empty());
1534 }
1535 other => panic!("unexpected variant: {other:?}"),
1536 }
1537 }
1538
1539 #[test]
1540 fn in_tenant_subtree_scope_contains_uuid_returns_false() {
1541 let scope =
1542 AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::in_tenant_subtree(
1543 pep_properties::RESOURCE_ID,
1544 ScopeValue::Uuid(uid(T1)),
1545 true,
1546 Vec::new(),
1547 )]));
1548 assert!(!scope.contains_uuid(pep_properties::RESOURCE_ID, uid(T1)));
1549 }
1550
1551 #[test]
1552 fn in_tenant_subtree_scope_filter_carries_descendant_status() {
1553 let filter =
1554 ScopeFilter::InTenantSubtree(InTenantSubtreeScopeFilter::with_descendant_status(
1555 pep_properties::OWNER_TENANT_ID,
1556 ScopeValue::Uuid(uid(T1)),
1557 true,
1558 vec![ScopeValue::Int(1), ScopeValue::Int(2)],
1559 ));
1560
1561 let ScopeFilter::InTenantSubtree(inner) = &filter else {
1563 panic!("constructed as InTenantSubtree");
1564 };
1565 assert_eq!(
1566 inner.descendant_status(),
1567 &[ScopeValue::Int(1), ScopeValue::Int(2)]
1568 );
1569
1570 assert_eq!(filter.values().iter().count(), 0);
1575 assert!(!filter.is_representable_in_memory());
1576 }
1577
1578 #[test]
1579 fn in_tenant_subtree_scope_filter_exposes_property_and_root() {
1580 let f =
1581 InTenantSubtreeScopeFilter::new(pep_properties::RESOURCE_ID, ScopeValue::Uuid(uid(T1)));
1582 assert_eq!(f.property(), pep_properties::RESOURCE_ID);
1583 assert_eq!(f.root_tenant_id(), &ScopeValue::Uuid(uid(T1)));
1584 assert!(f.respect_barriers());
1585 }
1586
1587 #[test]
1588 fn in_tenant_subtree_scope_filter_ignore_barriers_constructor() {
1589 let f = InTenantSubtreeScopeFilter::with_respect_barriers(
1590 pep_properties::OWNER_TENANT_ID,
1591 ScopeValue::Uuid(uid(T1)),
1592 false,
1593 );
1594 assert!(!f.respect_barriers());
1595 }
1596
1597 #[test]
1606 fn contains_uuid_matches_string_variant() {
1607 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
1608 pep_properties::OWNER_TENANT_ID,
1609 ScopeValue::String(T1.to_owned()),
1610 )]));
1611 assert!(scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1612 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T2)));
1613 }
1614
1615 #[test]
1616 fn contains_uuid_does_not_match_invalid_string() {
1617 let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
1618 pep_properties::OWNER_TENANT_ID,
1619 ScopeValue::String("not-a-uuid".to_owned()),
1620 )]));
1621 assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
1622 }
1623}