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)]
pub enum ScopeFilter {
Eq(EqScopeFilter),
In(InScopeFilter),
}
#[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
}
}
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 property(&self) -> &str {
match self {
Self::Eq(f) => f.property(),
Self::In(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),
}
}
#[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.contains_value(property, &ScopeValue::Uuid(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])
}
fn retain_properties(&self, properties: &[&str]) -> Self {
if self.unconstrained {
return self.clone();
}
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_preserves_unconstrained() {
let scope = AccessScope::allow_all();
let tenant_scope = scope.tenant_only();
assert!(tenant_scope.is_unconstrained());
}
#[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_preserves_unconstrained() {
let scope = AccessScope::allow_all();
assert!(scope.tenant_and_owner().is_unconstrained());
}
#[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());
}
}