use std::collections::{BTreeSet, VecDeque};
use std::sync::Arc;
use corium_protocol::authz::ViewFilter;
use crate::model::{ObjectRef, SubjectRef};
use crate::policy::Policy;
use crate::view;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Limits {
pub max_depth: usize,
pub max_visited: usize,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_depth: 8,
max_visited: 10_000,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PathStep {
pub relation: String,
pub object: ObjectRef,
}
impl std::fmt::Display for PathStep {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}#{}", self.object, self.relation)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Match {
pub relation: String,
pub object: ObjectRef,
pub subject: SubjectRef,
pub path: Vec<PathStep>,
}
impl Match {
#[must_use]
pub fn render_path(&self) -> String {
let mut parts: Vec<String> = self.path.iter().map(ToString::to_string).collect();
parts.push(self.subject.to_string());
parts.join(" -> ")
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Denial {
NoPermission {
object_type: String,
action: String,
},
NoPath {
relations: Vec<String>,
objects: Vec<String>,
},
Exhausted {
visited: usize,
},
}
impl std::fmt::Display for Denial {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoPermission {
object_type,
action,
} => write!(
formatter,
"no permission maps action {action:?} on object type {object_type:?}"
),
Self::NoPath { relations, objects } => write!(
formatter,
"no relationship path grants {relations:?} on {objects:?}"
),
Self::Exhausted { visited } => write!(
formatter,
"relationship search exhausted its budget after {visited} goals"
),
}
}
}
#[derive(Clone, Debug)]
pub enum Outcome {
Allow {
matches: Vec<Match>,
},
AllowFiltered {
matches: Vec<Match>,
filter: Arc<dyn ViewFilter>,
views: Vec<String>,
},
Deny(Denial),
}
impl Outcome {
#[must_use]
pub fn is_allowed(&self) -> bool {
!matches!(self, Self::Deny(_))
}
#[must_use]
pub fn matches(&self) -> &[Match] {
match self {
Self::Allow { matches } | Self::AllowFiltered { matches, .. } => matches,
Self::Deny(_) => &[],
}
}
}
#[derive(Clone, Debug)]
pub struct Query {
pub subjects: BTreeSet<ObjectRef>,
pub relations: BTreeSet<String>,
pub action: String,
pub objects: Vec<ObjectRef>,
pub expand_relations: Vec<String>,
pub limits: Limits,
}
#[must_use]
pub fn check(policy: &Policy, query: &Query) -> Outcome {
if query.relations.is_empty() {
return Outcome::Deny(Denial::NoPermission {
object_type: query
.objects
.first()
.map_or_else(|| "*".to_owned(), |object| object.kind.clone()),
action: query.action.clone(),
});
}
let mut budget = Budget {
visited: 0,
max_visited: query.limits.max_visited,
};
let mut matches = Vec::new();
let mut exhausted = false;
'roots: for object in &query.objects {
for relation in &query.relations {
match Walk::new(policy, query).run(relation, object, &mut budget) {
Search::Found(found) => matches.push(found),
Search::NotFound => {}
Search::Exhausted => {
exhausted = true;
break 'roots;
}
}
}
}
if matches.is_empty() {
return Outcome::Deny(if exhausted {
Denial::Exhausted {
visited: budget.visited,
}
} else {
Denial::NoPath {
relations: query.relations.iter().cloned().collect(),
objects: query.objects.iter().map(ToString::to_string).collect(),
}
});
}
combine_views(policy, matches)
}
fn combine_views(policy: &Policy, matches: Vec<Match>) -> Outcome {
let mut filters = Vec::new();
let mut names = Vec::new();
for found in &matches {
let Some(binding) = policy.binding_for(&found.relation, &found.object) else {
continue;
};
if binding.unfiltered {
return Outcome::Allow { matches };
}
if let Some(name) = &binding.view
&& let Some(filter) = policy.view(name)
{
filters.push(Arc::clone(filter));
names.push(name.clone());
}
}
match view::combine(filters) {
None => Outcome::Allow { matches },
Some(filter) => Outcome::AllowFiltered {
matches,
filter,
views: names,
},
}
}
struct Budget {
visited: usize,
max_visited: usize,
}
impl Budget {
fn spend(&mut self) -> bool {
if self.visited >= self.max_visited {
return false;
}
self.visited += 1;
true
}
}
enum Search {
Found(Match),
NotFound,
Exhausted,
}
struct Node {
step: PathStep,
parent: Option<usize>,
depth: usize,
}
struct Walk<'a> {
policy: &'a Policy,
query: &'a Query,
nodes: Vec<Node>,
queue: VecDeque<usize>,
visited: BTreeSet<(String, ObjectRef)>,
}
impl<'a> Walk<'a> {
fn new(policy: &'a Policy, query: &'a Query) -> Self {
Self {
policy,
query,
nodes: Vec::new(),
queue: VecDeque::new(),
visited: BTreeSet::new(),
}
}
fn run(mut self, relation: &str, object: &ObjectRef, budget: &mut Budget) -> Search {
self.push(relation.to_owned(), object.clone(), None, 0);
while let Some(index) = self.queue.pop_front() {
if !budget.spend() {
return Search::Exhausted;
}
let goal = PathStep {
relation: self.nodes[index].step.relation.clone(),
object: self.nodes[index].step.object.clone(),
};
let depth = self.nodes[index].depth;
for subject in self.policy.subjects_for(&goal.object, &goal.relation) {
let subject = subject.clone();
match &subject.relation {
Some(userset) => {
self.push(
userset.clone(),
subject.object.clone(),
Some(index),
depth + 1,
);
}
None if self.holds(&subject) => {
return Search::Found(Match {
relation: relation.to_owned(),
object: object.clone(),
subject,
path: self.path_of(index),
});
}
None => {
for expansion in &self.query.expand_relations {
self.push(
expansion.clone(),
subject.object.clone(),
Some(index),
depth + 1,
);
}
}
}
}
for rewrite in self.policy.rewrites_for(&goal.relation, &goal.object.kind) {
let on_relation = rewrite.on_relation.clone();
let parents: Vec<ObjectRef> = self
.policy
.subjects_for(&goal.object, &rewrite.via_relation)
.into_iter()
.map(|parent| parent.object.clone())
.collect();
for parent in parents {
self.push(on_relation.clone(), parent, Some(index), depth + 1);
}
}
}
Search::NotFound
}
fn holds(&self, subject: &SubjectRef) -> bool {
if self.query.subjects.contains(&subject.object) {
return true;
}
subject.object.is_wildcard()
&& self
.query
.subjects
.iter()
.any(|held| held.kind == subject.object.kind)
}
fn push(&mut self, relation: String, object: ObjectRef, parent: Option<usize>, depth: usize) {
if depth > self.query.limits.max_depth {
return;
}
if !self.visited.insert((relation.clone(), object.clone())) {
return;
}
self.nodes.push(Node {
step: PathStep { relation, object },
parent,
depth,
});
self.queue.push_back(self.nodes.len() - 1);
}
fn path_of(&self, mut index: usize) -> Vec<PathStep> {
let mut path = Vec::new();
loop {
path.push(self.nodes[index].step.clone());
match self.nodes[index].parent {
Some(parent) => index = parent,
None => break,
}
}
path.reverse();
path
}
}