use std::fmt;
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum ScopeValue {
Uuid(Uuid),
String(String),
Int(i64),
Bool(bool),
}
impl ScopeValue {
#[must_use]
pub fn as_uuid(&self) -> Option<Uuid> {
match self {
Self::Uuid(u) => Some(*u),
Self::String(s) => Uuid::parse_str(s).ok(),
Self::Int(_) | Self::Bool(_) => None,
}
}
}
impl fmt::Display for ScopeValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Uuid(u) => write!(f, "{u}"),
Self::String(s) => write!(f, "{s}"),
Self::Int(n) => write!(f, "{n}"),
Self::Bool(b) => write!(f, "{b}"),
}
}
}
impl From<Uuid> for ScopeValue {
#[inline]
fn from(u: Uuid) -> Self {
Self::Uuid(u)
}
}
impl From<&Uuid> for ScopeValue {
#[inline]
fn from(u: &Uuid) -> Self {
Self::Uuid(*u)
}
}
impl From<String> for ScopeValue {
#[inline]
fn from(s: String) -> Self {
Self::String(s)
}
}
impl From<&str> for ScopeValue {
#[inline]
fn from(s: &str) -> Self {
Self::String(s.to_owned())
}
}
impl From<i64> for ScopeValue {
#[inline]
fn from(n: i64) -> Self {
Self::Int(n)
}
}
impl From<bool> for ScopeValue {
#[inline]
fn from(b: bool) -> Self {
Self::Bool(b)
}
}
pub mod pep_properties {
pub const OWNER_TENANT_ID: &str = "owner_tenant_id";
pub const RESOURCE_ID: &str = "id";
pub const OWNER_ID: &str = "owner_id";
}
pub mod rg_tables {
pub const MEMBERSHIP_TABLE: &str = "resource_group_membership";
pub const MEMBERSHIP_RESOURCE_ID: &str = "resource_id";
pub const MEMBERSHIP_GROUP_ID: &str = "group_id";
pub const CLOSURE_TABLE: &str = "resource_group_closure";
pub const CLOSURE_ANCESTOR_ID: &str = "ancestor_id";
pub const CLOSURE_DESCENDANT_ID: &str = "descendant_id";
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ScopeFilter {
Eq(EqScopeFilter),
In(InScopeFilter),
InGroup(InGroupScopeFilter),
InGroupSubtree(InGroupSubtreeScopeFilter),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct EqScopeFilter {
property: String,
value: ScopeValue,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InScopeFilter {
property: String,
values: Vec<ScopeValue>,
}
impl EqScopeFilter {
#[must_use]
pub fn new(property: impl Into<String>, value: impl Into<ScopeValue>) -> Self {
Self {
property: property.into(),
value: value.into(),
}
}
#[inline]
#[must_use]
pub fn property(&self) -> &str {
&self.property
}
#[inline]
#[must_use]
pub fn value(&self) -> &ScopeValue {
&self.value
}
}
impl InScopeFilter {
#[must_use]
pub fn new(property: impl Into<String>, values: Vec<ScopeValue>) -> Self {
Self {
property: property.into(),
values,
}
}
#[must_use]
pub fn from_values<V: Into<ScopeValue>>(
property: impl Into<String>,
values: impl IntoIterator<Item = V>,
) -> Self {
Self {
property: property.into(),
values: values.into_iter().map(Into::into).collect(),
}
}
#[inline]
#[must_use]
pub fn property(&self) -> &str {
&self.property
}
#[inline]
#[must_use]
pub fn values(&self) -> &[ScopeValue] {
&self.values
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InGroupScopeFilter {
property: String,
group_ids: Vec<ScopeValue>,
}
impl InGroupScopeFilter {
#[must_use]
pub fn new(property: impl Into<String>, group_ids: Vec<ScopeValue>) -> Self {
Self {
property: property.into(),
group_ids,
}
}
#[inline]
#[must_use]
pub fn property(&self) -> &str {
&self.property
}
#[inline]
#[must_use]
pub fn group_ids(&self) -> &[ScopeValue] {
&self.group_ids
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InGroupSubtreeScopeFilter {
property: String,
ancestor_ids: Vec<ScopeValue>,
}
impl InGroupSubtreeScopeFilter {
#[must_use]
pub fn new(property: impl Into<String>, ancestor_ids: Vec<ScopeValue>) -> Self {
Self {
property: property.into(),
ancestor_ids,
}
}
#[inline]
#[must_use]
pub fn property(&self) -> &str {
&self.property
}
#[inline]
#[must_use]
pub fn ancestor_ids(&self) -> &[ScopeValue] {
&self.ancestor_ids
}
}
impl ScopeFilter {
#[must_use]
pub fn eq(property: impl Into<String>, value: impl Into<ScopeValue>) -> Self {
Self::Eq(EqScopeFilter::new(property, value))
}
#[must_use]
pub fn r#in(property: impl Into<String>, values: Vec<ScopeValue>) -> Self {
Self::In(InScopeFilter::new(property, values))
}
#[must_use]
pub fn in_uuids(property: impl Into<String>, uuids: Vec<Uuid>) -> Self {
Self::In(InScopeFilter::new(
property,
uuids.into_iter().map(ScopeValue::Uuid).collect(),
))
}
#[must_use]
pub fn in_group(property: impl Into<String>, group_ids: Vec<ScopeValue>) -> Self {
Self::InGroup(InGroupScopeFilter::new(property, group_ids))
}
#[must_use]
pub fn in_group_subtree(property: impl Into<String>, ancestor_ids: Vec<ScopeValue>) -> Self {
Self::InGroupSubtree(InGroupSubtreeScopeFilter::new(property, ancestor_ids))
}
#[must_use]
pub fn property(&self) -> &str {
match self {
Self::Eq(f) => f.property(),
Self::In(f) => f.property(),
Self::InGroup(f) => f.property(),
Self::InGroupSubtree(f) => f.property(),
}
}
#[must_use]
pub fn values(&self) -> ScopeFilterValues<'_> {
match self {
Self::Eq(f) => ScopeFilterValues::Single(&f.value),
Self::In(f) => ScopeFilterValues::Multiple(&f.values),
Self::InGroup(_) | Self::InGroupSubtree(_) => ScopeFilterValues::Multiple(&[]),
}
}
#[must_use]
pub fn uuid_values(&self) -> Vec<Uuid> {
self.values()
.iter()
.filter_map(ScopeValue::as_uuid)
.collect()
}
}
#[derive(Clone, Debug)]
pub enum ScopeFilterValues<'a> {
Single(&'a ScopeValue),
Multiple(&'a [ScopeValue]),
}
impl<'a> ScopeFilterValues<'a> {
#[must_use]
pub fn iter(&self) -> ScopeFilterValuesIter<'a> {
match self {
Self::Single(v) => ScopeFilterValuesIter::Single(Some(v)),
Self::Multiple(vs) => ScopeFilterValuesIter::Multiple(vs.iter()),
}
}
#[must_use]
pub fn contains(&self, value: &ScopeValue) -> bool {
self.iter().any(|v| v == value)
}
}
impl<'a> IntoIterator for ScopeFilterValues<'a> {
type Item = &'a ScopeValue;
type IntoIter = ScopeFilterValuesIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl<'a> IntoIterator for &ScopeFilterValues<'a> {
type Item = &'a ScopeValue;
type IntoIter = ScopeFilterValuesIter<'a>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
pub enum ScopeFilterValuesIter<'a> {
Single(Option<&'a ScopeValue>),
Multiple(std::slice::Iter<'a, ScopeValue>),
}
impl<'a> Iterator for ScopeFilterValuesIter<'a> {
type Item = &'a ScopeValue;
fn next(&mut self) -> Option<Self::Item> {
match self {
Self::Single(v) => v.take(),
Self::Multiple(iter) => iter.next(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ScopeConstraint {
filters: Vec<ScopeFilter>,
}
impl ScopeConstraint {
#[must_use]
pub fn new(filters: Vec<ScopeFilter>) -> Self {
Self { filters }
}
#[inline]
#[must_use]
pub fn filters(&self) -> &[ScopeFilter] {
&self.filters
}
#[inline]
#[must_use]
pub fn is_empty(&self) -> bool {
self.filters.is_empty()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct AccessScope {
constraints: Vec<ScopeConstraint>,
unconstrained: bool,
}
impl Default for AccessScope {
fn default() -> Self {
Self::deny_all()
}
}
impl AccessScope {
#[must_use]
pub fn from_constraints(constraints: Vec<ScopeConstraint>) -> Self {
Self {
constraints,
unconstrained: false,
}
}
#[must_use]
pub fn single(constraint: ScopeConstraint) -> Self {
Self::from_constraints(vec![constraint])
}
#[must_use]
pub fn allow_all() -> Self {
Self {
constraints: Vec::new(),
unconstrained: true,
}
}
#[must_use]
pub fn deny_all() -> Self {
Self {
constraints: Vec::new(),
unconstrained: false,
}
}
#[must_use]
pub fn for_tenants(ids: Vec<Uuid>) -> Self {
Self::single(ScopeConstraint::new(vec![ScopeFilter::in_uuids(
pep_properties::OWNER_TENANT_ID,
ids,
)]))
}
#[must_use]
pub fn for_tenant(id: Uuid) -> Self {
Self::for_tenants(vec![id])
}
#[must_use]
pub fn for_resources(ids: Vec<Uuid>) -> Self {
Self::single(ScopeConstraint::new(vec![ScopeFilter::in_uuids(
pep_properties::RESOURCE_ID,
ids,
)]))
}
#[must_use]
pub fn for_resource(id: Uuid) -> Self {
Self::for_resources(vec![id])
}
#[inline]
#[must_use]
pub fn constraints(&self) -> &[ScopeConstraint] {
&self.constraints
}
#[inline]
#[must_use]
pub fn is_unconstrained(&self) -> bool {
self.unconstrained
}
#[must_use]
pub fn is_deny_all(&self) -> bool {
!self.unconstrained && self.constraints.is_empty()
}
#[must_use]
pub fn all_values_for(&self, property: &str) -> Vec<&ScopeValue> {
let mut result = Vec::new();
for constraint in &self.constraints {
for filter in constraint.filters() {
if filter.property() == property {
result.extend(filter.values());
}
}
}
result
}
#[must_use]
pub fn all_uuid_values_for(&self, property: &str) -> Vec<Uuid> {
let mut result = Vec::new();
for constraint in &self.constraints {
for filter in constraint.filters() {
if filter.property() == property {
result.extend(filter.uuid_values());
}
}
}
result
}
#[must_use]
pub fn contains_value(&self, property: &str, value: &ScopeValue) -> bool {
self.constraints.iter().any(|c| {
c.filters()
.iter()
.any(|f| f.property() == property && f.values().contains(value))
})
}
#[must_use]
pub fn contains_uuid(&self, property: &str, id: Uuid) -> bool {
self.constraints.iter().any(|c| {
c.filters().iter().any(|f| {
f.property() == property && f.values().iter().any(|v| v.as_uuid() == Some(id))
})
})
}
#[must_use]
pub fn has_property(&self, property: &str) -> bool {
self.constraints
.iter()
.any(|c| c.filters().iter().any(|f| f.property() == property))
}
#[must_use]
pub fn tenant_only(&self) -> Self {
self.retain_properties(&[pep_properties::OWNER_TENANT_ID])
}
#[must_use]
pub fn tenant_and_owner(&self) -> Self {
self.retain_properties(&[pep_properties::OWNER_TENANT_ID, pep_properties::OWNER_ID])
}
#[must_use]
pub fn ensure_owner(&self, owner_id: Uuid) -> Self {
if self.is_deny_all() {
return Self::deny_all();
}
let owner_filter = ScopeFilter::eq(pep_properties::OWNER_ID, owner_id);
if self.unconstrained {
return Self::single(ScopeConstraint::new(vec![owner_filter]));
}
let constraints = self
.constraints
.iter()
.filter_map(|c| {
let owner_filters: Vec<&ScopeFilter> = c
.filters()
.iter()
.filter(|f| f.property() == pep_properties::OWNER_ID)
.collect();
if owner_filters.is_empty() {
let mut filters = c.filters().to_vec();
filters.push(owner_filter.clone());
return Some(ScopeConstraint::new(filters));
}
let all_match = owner_filters
.iter()
.all(|f| f.values().iter().any(|v| v.as_uuid() == Some(owner_id)));
if !all_match {
return None;
}
if owner_filters.len() == 1 && matches!(owner_filters[0], ScopeFilter::Eq(_)) {
return Some(c.clone());
}
let mut filters: Vec<ScopeFilter> = c
.filters()
.iter()
.filter(|f| f.property() != pep_properties::OWNER_ID)
.cloned()
.collect();
filters.push(owner_filter.clone());
Some(ScopeConstraint::new(filters))
})
.collect();
Self::from_constraints(constraints)
}
fn retain_properties(&self, properties: &[&str]) -> Self {
if self.unconstrained {
return Self::deny_all();
}
let constraints = self
.constraints
.iter()
.filter_map(|c| {
let kept: Vec<ScopeFilter> = c
.filters()
.iter()
.filter(|f| properties.contains(&f.property()))
.cloned()
.collect();
if kept.is_empty() {
None
} else {
Some(ScopeConstraint::new(kept))
}
})
.collect();
Self::from_constraints(constraints)
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use uuid::Uuid;
const T1: &str = "11111111-1111-1111-1111-111111111111";
const T2: &str = "22222222-2222-2222-2222-222222222222";
fn uid(s: &str) -> Uuid {
Uuid::parse_str(s).unwrap()
}
#[test]
fn scope_filter_eq_constructor() {
let f = ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1));
assert_eq!(f.property(), pep_properties::OWNER_TENANT_ID);
assert!(matches!(f, ScopeFilter::Eq(_)));
assert!(f.values().contains(&ScopeValue::Uuid(uid(T1))));
}
#[test]
fn all_values_for_works_with_eq() {
let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
pep_properties::OWNER_TENANT_ID,
uid(T1),
)]));
assert_eq!(
scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
&[uid(T1)]
);
}
#[test]
fn all_values_for_works_with_mixed_eq_and_in() {
let scope = AccessScope::from_constraints(vec![
ScopeConstraint::new(vec![ScopeFilter::eq(
pep_properties::OWNER_TENANT_ID,
uid(T1),
)]),
ScopeConstraint::new(vec![ScopeFilter::in_uuids(
pep_properties::OWNER_TENANT_ID,
vec![uid(T2)],
)]),
]);
let values = scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID);
assert_eq!(values, &[uid(T1), uid(T2)]);
}
#[test]
fn contains_value_works_with_eq() {
let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
pep_properties::OWNER_TENANT_ID,
uid(T1),
)]));
assert!(scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T2)));
}
#[test]
fn tenant_only_strips_owner_id() {
let scope = AccessScope::single(ScopeConstraint::new(vec![
ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
ScopeFilter::eq(pep_properties::OWNER_ID, uid(T2)),
]));
let tenant_scope = scope.tenant_only();
assert!(tenant_scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
assert!(!tenant_scope.has_property(pep_properties::OWNER_ID));
}
#[test]
fn tenant_only_unconstrained_becomes_deny_all() {
let scope = AccessScope::allow_all();
let tenant_scope = scope.tenant_only();
assert!(tenant_scope.is_deny_all());
}
#[test]
fn tenant_only_deny_all_when_no_tenant_filters() {
let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
pep_properties::OWNER_ID,
uid(T1),
)]));
let tenant_scope = scope.tenant_only();
assert!(tenant_scope.is_deny_all());
}
#[test]
fn tenant_only_on_deny_all_stays_deny_all() {
let scope = AccessScope::deny_all();
let tenant_scope = scope.tenant_only();
assert!(tenant_scope.is_deny_all());
}
#[test]
fn tenant_and_owner_keeps_both_properties() {
let scope = AccessScope::single(ScopeConstraint::new(vec![
ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
ScopeFilter::eq(pep_properties::OWNER_ID, uid(T2)),
ScopeFilter::eq(pep_properties::RESOURCE_ID, uid(T1)),
]));
let narrowed = scope.tenant_and_owner();
assert!(narrowed.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
assert!(narrowed.contains_uuid(pep_properties::OWNER_ID, uid(T2)));
assert!(!narrowed.has_property(pep_properties::RESOURCE_ID));
}
#[test]
fn tenant_and_owner_unconstrained_becomes_deny_all() {
let scope = AccessScope::allow_all();
assert!(scope.tenant_and_owner().is_deny_all());
}
#[test]
fn tenant_and_owner_deny_all_when_no_matching_filters() {
let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
pep_properties::RESOURCE_ID,
uid(T1),
)]));
assert!(scope.tenant_and_owner().is_deny_all());
}
#[test]
fn ensure_owner_adds_owner_when_missing() {
let scope = AccessScope::for_tenant(uid(T1));
let owner_id = uid(T2);
let scoped = scope.ensure_owner(owner_id);
assert!(scoped.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
assert!(scoped.contains_uuid(pep_properties::OWNER_ID, owner_id));
}
#[test]
fn ensure_owner_keeps_existing_owner() {
let existing_owner = uid(T2);
let scope = AccessScope::single(ScopeConstraint::new(vec![
ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
ScopeFilter::eq(pep_properties::OWNER_ID, existing_owner),
]));
let scoped = scope.ensure_owner(existing_owner);
assert_eq!(
scoped.all_uuid_values_for(pep_properties::OWNER_ID),
&[existing_owner]
);
}
#[test]
fn ensure_owner_on_unconstrained_creates_owner_scope() {
let scope = AccessScope::allow_all();
let owner_id = uid(T1);
let scoped = scope.ensure_owner(owner_id);
assert!(!scoped.is_unconstrained());
assert!(scoped.contains_uuid(pep_properties::OWNER_ID, owner_id));
}
#[test]
fn ensure_owner_on_deny_all_stays_deny_all() {
let scope = AccessScope::deny_all();
let scoped = scope.ensure_owner(uid(T1));
assert!(scoped.is_deny_all());
}
#[test]
fn ensure_owner_narrows_existing_owner_to_subject() {
let user_a = uid(T1);
let user_b = uid(T2);
let scope = AccessScope::single(ScopeConstraint::new(vec![
ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
ScopeFilter::in_uuids(pep_properties::OWNER_ID, vec![user_a, user_b]),
]));
let scoped = scope.ensure_owner(user_a);
assert_eq!(
scoped.all_uuid_values_for(pep_properties::OWNER_ID),
&[user_a],
"Must narrow to exactly the subject's owner_id"
);
assert!(scoped.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
}
#[test]
fn ensure_owner_drops_constraint_when_subject_not_in_pdp() {
let user_x = uid(T1);
let user_y = uid(T2);
let scope = AccessScope::single(ScopeConstraint::new(vec![
ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
ScopeFilter::eq(pep_properties::OWNER_ID, user_x),
]));
let scoped = scope.ensure_owner(user_y);
assert!(
scoped.is_deny_all(),
"Must be deny-all when subject not in PDP's owner set"
);
}
#[test]
fn ensure_owner_checks_all_owner_filters_in_constraint() {
let alice = uid(T1);
let bob = uid(T2);
let scope = AccessScope::single(ScopeConstraint::new(vec![
ScopeFilter::in_uuids(pep_properties::OWNER_ID, vec![alice, bob]),
ScopeFilter::in_uuids(pep_properties::OWNER_ID, vec![bob]),
]));
let scoped = scope.ensure_owner(alice);
assert!(
scoped.is_deny_all(),
"Must deny when subject is missing from any owner_id filter"
);
let scoped = scope.ensure_owner(bob);
assert!(!scoped.is_deny_all());
assert_eq!(
scoped.all_uuid_values_for(pep_properties::OWNER_ID),
&[bob],
"Must narrow to single Eq for the matching owner"
);
}
#[test]
fn ensure_owner_multi_constraint_keeps_only_matching() {
let alice = uid(T1);
let bob = uid(T2);
let tenant = uid(T1);
let c1 = ScopeConstraint::new(vec![
ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, tenant),
ScopeFilter::eq(pep_properties::OWNER_ID, alice),
]);
let c2 = ScopeConstraint::new(vec![
ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, tenant),
ScopeFilter::eq(pep_properties::OWNER_ID, bob),
]);
let scope = AccessScope::from_constraints(vec![c1, c2]);
let scoped = scope.ensure_owner(alice);
assert!(
!scoped.is_deny_all(),
"Must not be deny-all - one constraint matches"
);
assert_eq!(
scoped.all_uuid_values_for(pep_properties::OWNER_ID),
&[alice],
"Must keep only the constraint matching alice"
);
assert!(
scoped.contains_uuid(pep_properties::OWNER_TENANT_ID, tenant),
"Tenant filter must be preserved"
);
}
#[test]
fn scope_filter_in_group_constructor() {
let f = ScopeFilter::in_group(
pep_properties::OWNER_TENANT_ID,
vec![ScopeValue::Uuid(uid(T1))],
);
assert_eq!(f.property(), pep_properties::OWNER_TENANT_ID);
assert!(matches!(f, ScopeFilter::InGroup(_)));
assert_eq!(f.values().iter().count(), 0);
}
#[test]
fn scope_filter_in_group_subtree_constructor() {
let f = ScopeFilter::in_group_subtree(
pep_properties::OWNER_TENANT_ID,
vec![ScopeValue::Uuid(uid(T1))],
);
assert_eq!(f.property(), pep_properties::OWNER_TENANT_ID);
assert!(matches!(f, ScopeFilter::InGroupSubtree(_)));
assert_eq!(f.values().iter().count(), 0);
}
#[test]
fn in_group_scope_contains_uuid_returns_false() {
let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::in_group(
pep_properties::OWNER_TENANT_ID,
vec![ScopeValue::Uuid(uid(T1))],
)]));
assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
}
#[test]
fn in_group_subtree_scope_contains_uuid_returns_false() {
let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::in_group_subtree(
pep_properties::OWNER_TENANT_ID,
vec![ScopeValue::Uuid(uid(T1))],
)]));
assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
}
#[test]
fn contains_uuid_matches_string_variant() {
let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
pep_properties::OWNER_TENANT_ID,
ScopeValue::String(T1.to_owned()),
)]));
assert!(scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T2)));
}
#[test]
fn contains_uuid_does_not_match_invalid_string() {
let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
pep_properties::OWNER_TENANT_ID,
ScopeValue::String("not-a-uuid".to_owned()),
)]));
assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
}
}