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";
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScopeFilter {
Eq(EqScopeFilter),
In(InScopeFilter),
InGroup(InGroupScopeFilter),
InGroupSubtree(InGroupSubtreeScopeFilter),
InTenantSubtree(InTenantSubtreeScopeFilter),
}
#[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
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InTenantSubtreeScopeFilter {
property: String,
root_tenant_id: ScopeValue,
respect_barriers: bool,
descendant_status: Vec<ScopeValue>,
}
impl InTenantSubtreeScopeFilter {
#[must_use]
pub fn new(property: impl Into<String>, root_tenant_id: impl Into<ScopeValue>) -> Self {
Self::with_respect_barriers(property, root_tenant_id, true)
}
#[must_use]
pub fn with_respect_barriers(
property: impl Into<String>,
root_tenant_id: impl Into<ScopeValue>,
respect_barriers: bool,
) -> Self {
Self::with_descendant_status(property, root_tenant_id, respect_barriers, Vec::new())
}
#[must_use]
pub fn with_descendant_status(
property: impl Into<String>,
root_tenant_id: impl Into<ScopeValue>,
respect_barriers: bool,
descendant_status: Vec<ScopeValue>,
) -> Self {
Self {
property: property.into(),
root_tenant_id: root_tenant_id.into(),
respect_barriers,
descendant_status,
}
}
#[inline]
#[must_use]
pub fn property(&self) -> &str {
&self.property
}
#[inline]
#[must_use]
pub fn root_tenant_id(&self) -> &ScopeValue {
&self.root_tenant_id
}
#[inline]
#[must_use]
pub fn respect_barriers(&self) -> bool {
self.respect_barriers
}
#[inline]
#[must_use]
pub fn descendant_status(&self) -> &[ScopeValue] {
&self.descendant_status
}
}
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 in_tenant_subtree(
property: impl Into<String>,
root_tenant_id: impl Into<ScopeValue>,
respect_barriers: bool,
descendant_status: Vec<ScopeValue>,
) -> Self {
Self::InTenantSubtree(InTenantSubtreeScopeFilter::with_descendant_status(
property,
root_tenant_id,
respect_barriers,
descendant_status,
))
}
#[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(),
Self::InTenantSubtree(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(_) | Self::InTenantSubtree(_) => {
ScopeFilterValues::Multiple(&[])
}
}
}
#[must_use]
pub fn is_representable_in_memory(&self) -> bool {
match self {
Self::Eq(_) | Self::In(_) => true,
Self::InGroup(_) | Self::InGroupSubtree(_) | Self::InTenantSubtree(_) => false,
}
}
#[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(ScopeFilterValuesIterInner::Single(Some(v))),
Self::Multiple(vs) => {
ScopeFilterValuesIter(ScopeFilterValuesIterInner::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()
}
}
#[derive(Debug, Clone)]
pub struct ScopeFilterValuesIter<'a>(ScopeFilterValuesIterInner<'a>);
#[derive(Debug, Clone)]
enum ScopeFilterValuesIterInner<'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 &mut self.0 {
ScopeFilterValuesIterInner::Single(v) => v.take(),
ScopeFilterValuesIterInner::Multiple(iter) => iter.next(),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match &self.0 {
ScopeFilterValuesIterInner::Single(v) => {
let n = usize::from(v.is_some());
(n, Some(n))
}
ScopeFilterValuesIterInner::Multiple(iter) => iter.size_hint(),
}
}
}
impl ExactSizeIterator for ScopeFilterValuesIter<'_> {}
#[derive(Clone, Debug, PartialEq)]
pub struct ScopeConstraint {
filters: Vec<ScopeFilter>,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
#[error("a scope constraint must carry at least one filter; an empty one matches every row")]
pub struct EmptyScopeConstraint;
impl ScopeConstraint {
pub fn try_new(filters: Vec<ScopeFilter>) -> Result<Self, EmptyScopeConstraint> {
if filters.is_empty() {
return Err(EmptyScopeConstraint);
}
Ok(Self { filters })
}
#[must_use]
pub fn new(filters: Vec<ScopeFilter>) -> Self {
assert!(
!filters.is_empty(),
"a scope constraint must carry at least one filter; an empty one matches every row"
);
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_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_exposes_exactly_one_value() {
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_eq!(f.values().iter().count(), 1);
assert_eq!(f.uuid_values(), vec![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_uuid_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 a_subquery_filter_reports_that_it_cannot_be_decided_in_memory() {
let empty_in = ScopeFilter::r#in(pep_properties::OWNER_TENANT_ID, vec![]);
assert_eq!(empty_in.values().iter().count(), 0);
assert!(
empty_in.is_representable_in_memory(),
"an In filter with no values is a real, decidable negative"
);
for subquery in [
ScopeFilter::in_group(pep_properties::RESOURCE_ID, vec![ScopeValue::Uuid(uid(T1))]),
ScopeFilter::in_group_subtree(
pep_properties::RESOURCE_ID,
vec![ScopeValue::Uuid(uid(T1))],
),
] {
assert_eq!(subquery.values().iter().count(), 0);
assert!(
!subquery.is_representable_in_memory(),
"a subquery filter's empty value view is not a negative"
);
}
}
#[test]
fn contains_uuid_matches_a_uuid_held_as_a_string() {
let as_text = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
pep_properties::OWNER_TENANT_ID,
ScopeValue::String(uid(T1).to_string()),
)]));
assert!(as_text.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
assert!(!as_text.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T2)));
let typed = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::eq(
pep_properties::OWNER_TENANT_ID,
uid(T1),
)]));
assert!(typed.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
}
#[test]
fn contains_uuid_is_false_for_a_subquery_filter() {
let scope = AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::in_group(
pep_properties::RESOURCE_ID,
vec![ScopeValue::Uuid(uid(T1))],
)]));
assert!(!scope.contains_uuid(pep_properties::RESOURCE_ID, uid(T1)));
}
#[test]
fn contains_uuid_does_not_evaluate_the_conjunction() {
let scope = AccessScope::single(ScopeConstraint::new(vec![
ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
ScopeFilter::eq(pep_properties::OWNER_ID, uid(T2)),
]));
assert!(scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
assert!(
!scope.contains_uuid(pep_properties::OWNER_ID, uid(T1)),
"guard: T1 is the tenant, not the owner"
);
}
#[test]
fn an_allow_all_scope_permits_everything_it_reports_no_constraint_for() {
let scope = AccessScope::allow_all();
assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
assert!(!scope.has_property(pep_properties::OWNER_TENANT_ID));
assert!(
scope
.all_uuid_values_for(pep_properties::OWNER_TENANT_ID)
.is_empty()
);
assert!(scope.is_unconstrained());
assert!(!AccessScope::deny_all().is_unconstrained());
}
#[test]
fn an_empty_constraint_is_refused() {
assert_eq!(
ScopeConstraint::try_new(vec![]).unwrap_err(),
EmptyScopeConstraint
);
assert!(
ScopeConstraint::try_new(vec![ScopeFilter::eq(
pep_properties::OWNER_TENANT_ID,
uid(T1)
)])
.is_ok(),
"one filter is enough to narrow"
);
}
#[test]
#[should_panic(expected = "must carry at least one filter")]
fn the_infallible_constructor_refuses_an_empty_list_too() {
drop(ScopeConstraint::new(vec![]));
}
#[test]
fn a_deny_all_scope_permits_nothing() {
let scope = AccessScope::deny_all();
assert!(scope.is_deny_all());
assert!(!scope.is_unconstrained());
assert!(!scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
}
#[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_widens_a_resource_scoped_grant_to_the_whole_tenant() {
let scope = AccessScope::single(ScopeConstraint::new(vec![
ScopeFilter::eq(pep_properties::OWNER_TENANT_ID, uid(T1)),
ScopeFilter::eq(pep_properties::RESOURCE_ID, uid(T2)),
]));
let tenant_scope = scope.tenant_only();
assert!(
!tenant_scope.has_property(pep_properties::RESOURCE_ID),
"the resource narrowing is gone, so this grant now covers the tenant"
);
assert!(tenant_scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uid(T1)));
}
#[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 scope_filter_in_tenant_subtree_constructor_defaults_to_respect() {
let f = ScopeFilter::in_tenant_subtree(
pep_properties::RESOURCE_ID,
ScopeValue::Uuid(uid(T1)),
true,
Vec::new(),
);
assert_eq!(f.property(), pep_properties::RESOURCE_ID);
assert!(matches!(f, ScopeFilter::InTenantSubtree(_)));
assert_eq!(f.values().iter().count(), 0);
match &f {
ScopeFilter::InTenantSubtree(sf) => {
assert!(sf.respect_barriers());
assert!(sf.descendant_status().is_empty());
}
other => panic!("unexpected variant: {other:?}"),
}
}
#[test]
fn in_tenant_subtree_scope_contains_uuid_returns_false() {
let scope =
AccessScope::single(ScopeConstraint::new(vec![ScopeFilter::in_tenant_subtree(
pep_properties::RESOURCE_ID,
ScopeValue::Uuid(uid(T1)),
true,
Vec::new(),
)]));
assert!(!scope.contains_uuid(pep_properties::RESOURCE_ID, uid(T1)));
}
#[test]
fn in_tenant_subtree_scope_filter_carries_descendant_status() {
let filter =
ScopeFilter::InTenantSubtree(InTenantSubtreeScopeFilter::with_descendant_status(
pep_properties::OWNER_TENANT_ID,
ScopeValue::Uuid(uid(T1)),
true,
vec![ScopeValue::Int(1), ScopeValue::Int(2)],
));
let ScopeFilter::InTenantSubtree(inner) = &filter else {
panic!("constructed as InTenantSubtree");
};
assert_eq!(
inner.descendant_status(),
&[ScopeValue::Int(1), ScopeValue::Int(2)]
);
assert_eq!(filter.values().iter().count(), 0);
assert!(!filter.is_representable_in_memory());
}
#[test]
fn in_tenant_subtree_scope_filter_exposes_property_and_root() {
let f =
InTenantSubtreeScopeFilter::new(pep_properties::RESOURCE_ID, ScopeValue::Uuid(uid(T1)));
assert_eq!(f.property(), pep_properties::RESOURCE_ID);
assert_eq!(f.root_tenant_id(), &ScopeValue::Uuid(uid(T1)));
assert!(f.respect_barriers());
}
#[test]
fn in_tenant_subtree_scope_filter_ignore_barriers_constructor() {
let f = InTenantSubtreeScopeFilter::with_respect_barriers(
pep_properties::OWNER_TENANT_ID,
ScopeValue::Uuid(uid(T1)),
false,
);
assert!(!f.respect_barriers());
}
#[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)));
}
}