use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ObjectRef {
pub object_type: String,
pub object_id: String,
}
impl ObjectRef {
pub fn new(ty: impl Into<String>, id: impl Into<String>) -> Self {
Self {
object_type: ty.into(),
object_id: id.into(),
}
}
pub fn parse(s: &str) -> Option<Self> {
let (ty, id) = s.split_once(':')?;
if ty.is_empty() || id.is_empty() {
return None;
}
Some(Self::new(ty, id))
}
pub fn render(&self) -> String {
format!("{}:{}", self.object_type, self.object_id)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SubjectRef {
pub subject_type: String,
pub subject_id: String,
#[serde(default)]
pub subject_rel: String,
}
impl SubjectRef {
pub fn direct(ty: impl Into<String>, id: impl Into<String>) -> Self {
Self {
subject_type: ty.into(),
subject_id: id.into(),
subject_rel: String::new(),
}
}
pub fn userset(
ty: impl Into<String>,
id: impl Into<String>,
relation: impl Into<String>,
) -> Self {
Self {
subject_type: ty.into(),
subject_id: id.into(),
subject_rel: relation.into(),
}
}
pub fn is_direct(&self) -> bool {
self.subject_rel.is_empty()
}
pub fn parse(s: &str) -> Option<Self> {
let (head, rel) = match s.split_once('#') {
Some((h, r)) if !r.is_empty() => (h, r.to_string()),
Some(_) => return None,
None => (s, String::new()),
};
let (ty, id) = head.split_once(':')?;
if ty.is_empty() || id.is_empty() {
return None;
}
Some(Self {
subject_type: ty.to_string(),
subject_id: id.to_string(),
subject_rel: rel,
})
}
pub fn render(&self) -> String {
if self.subject_rel.is_empty() {
format!("{}:{}", self.subject_type, self.subject_id)
} else {
format!(
"{}:{}#{}",
self.subject_type, self.subject_id, self.subject_rel
)
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Tuple {
pub object_type: String,
pub object_id: String,
pub relation: String,
pub subject_type: String,
pub subject_id: String,
#[serde(default)]
pub subject_rel: String,
}
impl Tuple {
pub fn direct(
object: impl Into<ObjectRef>,
relation: impl Into<String>,
subject: impl Into<SubjectRef>,
) -> Self {
let o: ObjectRef = object.into();
let s: SubjectRef = subject.into();
Self {
object_type: o.object_type,
object_id: o.object_id,
relation: relation.into(),
subject_type: s.subject_type,
subject_id: s.subject_id,
subject_rel: s.subject_rel,
}
}
pub fn object(&self) -> ObjectRef {
ObjectRef::new(self.object_type.clone(), self.object_id.clone())
}
pub fn subject(&self) -> SubjectRef {
SubjectRef {
subject_type: self.subject_type.clone(),
subject_id: self.subject_id.clone(),
subject_rel: self.subject_rel.clone(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub enum Consistency {
#[default]
Minimum,
AtLeastAsFresh(String),
Exact(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CheckResult {
Allowed { resolved_via: Vec<Tuple> },
Denied,
DepthExceeded,
CycleDetected,
}
impl CheckResult {
pub fn is_allowed(&self) -> bool {
matches!(self, CheckResult::Allowed { .. })
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum UsersetTree {
Leaf {
subject: SubjectRef,
},
Node {
op: TreeOp,
children: Vec<UsersetTree>,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TreeOp {
Union,
Intersect,
Exclude,
Direct,
TuplesetArrow,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NamespaceSchema {
pub name: String,
pub definitions: BTreeMap<String, RelationDef>,
}
impl NamespaceSchema {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
definitions: BTreeMap::new(),
}
}
pub fn with_relation(mut self, name: impl Into<String>, def: RelationDef) -> Self {
self.definitions.insert(name.into(), def);
self
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct RelationDef {
pub name: String,
pub kind: RelationKind,
}
impl RelationDef {
pub fn relation(name: impl Into<String>, types: Vec<TypeRef>) -> Self {
Self {
name: name.into(),
kind: RelationKind::Direct(types),
}
}
pub fn permission(name: impl Into<String>, expr: PermissionExpr) -> Self {
Self {
name: name.into(),
kind: RelationKind::Permission(Box::new(expr)),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum RelationKind {
Direct(Vec<TypeRef>),
Permission(Box<PermissionExpr>),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TypeRef {
pub object_type: String,
#[serde(default)]
pub relation: Option<String>,
#[serde(default)]
pub wildcard: bool,
}
impl TypeRef {
pub fn direct(ty: impl Into<String>) -> Self {
Self {
object_type: ty.into(),
relation: None,
wildcard: false,
}
}
pub fn userset(ty: impl Into<String>, relation: impl Into<String>) -> Self {
Self {
object_type: ty.into(),
relation: Some(relation.into()),
wildcard: false,
}
}
pub fn wildcard(ty: impl Into<String>) -> Self {
Self {
object_type: ty.into(),
relation: None,
wildcard: true,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum PermissionExpr {
Direct {
relation: String,
},
Union {
left: Box<PermissionExpr>,
right: Box<PermissionExpr>,
},
Intersect {
left: Box<PermissionExpr>,
right: Box<PermissionExpr>,
},
Exclude {
left: Box<PermissionExpr>,
right: Box<PermissionExpr>,
},
TuplesetArrow {
tupleset: String,
permission: String,
},
}
impl PermissionExpr {
pub fn direct(relation: impl Into<String>) -> Self {
Self::Direct {
relation: relation.into(),
}
}
pub fn union(l: PermissionExpr, r: PermissionExpr) -> Self {
Self::Union {
left: Box::new(l),
right: Box::new(r),
}
}
pub fn intersect(l: PermissionExpr, r: PermissionExpr) -> Self {
Self::Intersect {
left: Box::new(l),
right: Box::new(r),
}
}
pub fn exclude(l: PermissionExpr, r: PermissionExpr) -> Self {
Self::Exclude {
left: Box::new(l),
right: Box::new(r),
}
}
pub fn arrow(tupleset: impl Into<String>, permission: impl Into<String>) -> Self {
Self::TuplesetArrow {
tupleset: tupleset.into(),
permission: permission.into(),
}
}
}
pub const MAX_DEPTH: u32 = 50;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn object_round_trips() {
let o = ObjectRef::new("document", "foo");
assert_eq!(o.render(), "document:foo");
assert_eq!(ObjectRef::parse("document:foo"), Some(o));
assert_eq!(ObjectRef::parse(""), None);
assert_eq!(ObjectRef::parse("nope"), None);
assert_eq!(ObjectRef::parse("a:"), None);
}
#[test]
fn subject_round_trips() {
let direct = SubjectRef::direct("user", "alice");
assert_eq!(direct.render(), "user:alice");
assert_eq!(SubjectRef::parse("user:alice"), Some(direct));
let userset = SubjectRef::userset("family", "ahmed", "member");
assert_eq!(userset.render(), "family:ahmed#member");
assert_eq!(SubjectRef::parse("family:ahmed#member"), Some(userset));
assert_eq!(SubjectRef::parse(""), None);
assert_eq!(SubjectRef::parse("user:#member"), None);
assert_eq!(SubjectRef::parse("family:ahmed#"), None);
}
#[test]
fn check_result_is_allowed() {
assert!(
CheckResult::Allowed {
resolved_via: vec![]
}
.is_allowed()
);
assert!(!CheckResult::Denied.is_allowed());
assert!(!CheckResult::DepthExceeded.is_allowed());
assert!(!CheckResult::CycleDetected.is_allowed());
}
}