1use std::collections::{BTreeSet, VecDeque};
12use std::sync::Arc;
13
14use corium_protocol::authz::ViewFilter;
15
16use crate::model::{ObjectRef, SubjectRef};
17use crate::policy::Policy;
18use crate::view;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct Limits {
23 pub max_depth: usize,
25 pub max_visited: usize,
27}
28
29impl Default for Limits {
30 fn default() -> Self {
31 Self {
32 max_depth: 8,
33 max_visited: 10_000,
34 }
35 }
36}
37
38#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct PathStep {
41 pub relation: String,
43 pub object: ObjectRef,
45}
46
47impl std::fmt::Display for PathStep {
48 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 write!(formatter, "{}#{}", self.object, self.relation)
50 }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
56pub struct Match {
57 pub relation: String,
59 pub object: ObjectRef,
61 pub subject: SubjectRef,
63 pub path: Vec<PathStep>,
65}
66
67impl Match {
68 #[must_use]
71 pub fn render_path(&self) -> String {
72 let mut parts: Vec<String> = self.path.iter().map(ToString::to_string).collect();
73 parts.push(self.subject.to_string());
74 parts.join(" -> ")
75 }
76}
77
78#[derive(Clone, Debug, PartialEq, Eq)]
80pub enum Denial {
81 NoPermission {
83 object_type: String,
85 action: String,
87 },
88 NoPath {
90 relations: Vec<String>,
92 objects: Vec<String>,
94 },
95 Exhausted {
98 visited: usize,
100 },
101}
102
103impl std::fmt::Display for Denial {
104 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match self {
106 Self::NoPermission {
107 object_type,
108 action,
109 } => write!(
110 formatter,
111 "no permission maps action {action:?} on object type {object_type:?}"
112 ),
113 Self::NoPath { relations, objects } => write!(
114 formatter,
115 "no relationship path grants {relations:?} on {objects:?}"
116 ),
117 Self::Exhausted { visited } => write!(
118 formatter,
119 "relationship search exhausted its budget after {visited} goals"
120 ),
121 }
122 }
123}
124
125#[derive(Clone, Debug)]
127pub enum Outcome {
128 Allow {
130 matches: Vec<Match>,
132 },
133 AllowFiltered {
135 matches: Vec<Match>,
137 filter: Arc<dyn ViewFilter>,
139 views: Vec<String>,
141 },
142 Deny(Denial),
144}
145
146impl Outcome {
147 #[must_use]
149 pub fn is_allowed(&self) -> bool {
150 !matches!(self, Self::Deny(_))
151 }
152
153 #[must_use]
155 pub fn matches(&self) -> &[Match] {
156 match self {
157 Self::Allow { matches } | Self::AllowFiltered { matches, .. } => matches,
158 Self::Deny(_) => &[],
159 }
160 }
161}
162
163#[derive(Clone, Debug)]
166pub struct Query {
167 pub subjects: BTreeSet<ObjectRef>,
169 pub relations: BTreeSet<String>,
171 pub action: String,
173 pub objects: Vec<ObjectRef>,
175 pub expand_relations: Vec<String>,
177 pub limits: Limits,
179}
180
181#[must_use]
183pub fn check(policy: &Policy, query: &Query) -> Outcome {
184 if query.relations.is_empty() {
185 return Outcome::Deny(Denial::NoPermission {
186 object_type: query
187 .objects
188 .first()
189 .map_or_else(|| "*".to_owned(), |object| object.kind.clone()),
190 action: query.action.clone(),
191 });
192 }
193
194 let mut budget = Budget {
195 visited: 0,
196 max_visited: query.limits.max_visited,
197 };
198 let mut matches = Vec::new();
199 let mut exhausted = false;
200 'roots: for object in &query.objects {
204 for relation in &query.relations {
205 match Walk::new(policy, query).run(relation, object, &mut budget) {
206 Search::Found(found) => matches.push(found),
207 Search::NotFound => {}
208 Search::Exhausted => {
209 exhausted = true;
210 break 'roots;
211 }
212 }
213 }
214 }
215
216 if matches.is_empty() {
217 return Outcome::Deny(if exhausted {
218 Denial::Exhausted {
219 visited: budget.visited,
220 }
221 } else {
222 Denial::NoPath {
223 relations: query.relations.iter().cloned().collect(),
224 objects: query.objects.iter().map(ToString::to_string).collect(),
225 }
226 });
227 }
228
229 combine_views(policy, matches)
230}
231
232fn combine_views(policy: &Policy, matches: Vec<Match>) -> Outcome {
239 let mut filters = Vec::new();
240 let mut names = Vec::new();
241 for found in &matches {
242 let Some(binding) = policy.binding_for(&found.relation, &found.object) else {
243 continue;
244 };
245 if binding.unfiltered {
246 return Outcome::Allow { matches };
247 }
248 if let Some(name) = &binding.view
249 && let Some(filter) = policy.view(name)
250 {
251 filters.push(Arc::clone(filter));
252 names.push(name.clone());
253 }
254 }
255 match view::combine(filters) {
256 None => Outcome::Allow { matches },
257 Some(filter) => Outcome::AllowFiltered {
258 matches,
259 filter,
260 views: names,
261 },
262 }
263}
264
265struct Budget {
267 visited: usize,
268 max_visited: usize,
269}
270
271impl Budget {
272 fn spend(&mut self) -> bool {
273 if self.visited >= self.max_visited {
274 return false;
275 }
276 self.visited += 1;
277 true
278 }
279}
280
281enum Search {
282 Found(Match),
283 NotFound,
284 Exhausted,
285}
286
287struct Node {
290 step: PathStep,
291 parent: Option<usize>,
292 depth: usize,
293}
294
295struct Walk<'a> {
297 policy: &'a Policy,
298 query: &'a Query,
299 nodes: Vec<Node>,
300 queue: VecDeque<usize>,
301 visited: BTreeSet<(String, ObjectRef)>,
302}
303
304impl<'a> Walk<'a> {
305 fn new(policy: &'a Policy, query: &'a Query) -> Self {
306 Self {
307 policy,
308 query,
309 nodes: Vec::new(),
310 queue: VecDeque::new(),
311 visited: BTreeSet::new(),
312 }
313 }
314
315 fn run(mut self, relation: &str, object: &ObjectRef, budget: &mut Budget) -> Search {
316 self.push(relation.to_owned(), object.clone(), None, 0);
317 while let Some(index) = self.queue.pop_front() {
318 if !budget.spend() {
319 return Search::Exhausted;
320 }
321 let goal = PathStep {
322 relation: self.nodes[index].step.relation.clone(),
323 object: self.nodes[index].step.object.clone(),
324 };
325 let depth = self.nodes[index].depth;
326
327 for subject in self.policy.subjects_for(&goal.object, &goal.relation) {
328 let subject = subject.clone();
329 match &subject.relation {
330 Some(userset) => {
333 self.push(
334 userset.clone(),
335 subject.object.clone(),
336 Some(index),
337 depth + 1,
338 );
339 }
340 None if self.holds(&subject) => {
341 return Search::Found(Match {
342 relation: relation.to_owned(),
343 object: object.clone(),
344 subject,
345 path: self.path_of(index),
346 });
347 }
348 None => {
352 for expansion in &self.query.expand_relations {
353 self.push(
354 expansion.clone(),
355 subject.object.clone(),
356 Some(index),
357 depth + 1,
358 );
359 }
360 }
361 }
362 }
363
364 for rewrite in self.policy.rewrites_for(&goal.relation, &goal.object.kind) {
365 let on_relation = rewrite.on_relation.clone();
366 let parents: Vec<ObjectRef> = self
367 .policy
368 .subjects_for(&goal.object, &rewrite.via_relation)
369 .into_iter()
370 .map(|parent| parent.object.clone())
371 .collect();
372 for parent in parents {
373 self.push(on_relation.clone(), parent, Some(index), depth + 1);
374 }
375 }
376 }
377 Search::NotFound
378 }
379
380 fn holds(&self, subject: &SubjectRef) -> bool {
384 if self.query.subjects.contains(&subject.object) {
385 return true;
386 }
387 subject.object.is_wildcard()
388 && self
389 .query
390 .subjects
391 .iter()
392 .any(|held| held.kind == subject.object.kind)
393 }
394
395 fn push(&mut self, relation: String, object: ObjectRef, parent: Option<usize>, depth: usize) {
396 if depth > self.query.limits.max_depth {
397 return;
398 }
399 if !self.visited.insert((relation.clone(), object.clone())) {
402 return;
403 }
404 self.nodes.push(Node {
405 step: PathStep { relation, object },
406 parent,
407 depth,
408 });
409 self.queue.push_back(self.nodes.len() - 1);
410 }
411
412 fn path_of(&self, mut index: usize) -> Vec<PathStep> {
413 let mut path = Vec::new();
414 loop {
415 path.push(self.nodes[index].step.clone());
416 match self.nodes[index].parent {
417 Some(parent) => index = parent,
418 None => break,
419 }
420 }
421 path.reverse();
422 path
423 }
424}