1use std::collections::{BTreeSet, HashMap, HashSet};
37use std::fmt;
38use std::sync::{Arc, RwLock};
39
40pub const MAX_DEPTH: usize = 25;
42pub const MAX_READS: usize = 1000;
44pub const MAX_NAME: usize = 256;
46
47#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum AuthzError {
50 Parse(String),
52 Schema(String),
55 Limit(String),
57 Store(String),
59}
60
61impl fmt::Display for AuthzError {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 match self {
64 AuthzError::Parse(e) => write!(f, "parse error: {e}"),
65 AuthzError::Schema(e) => write!(f, "schema: {e}"),
66 AuthzError::Limit(e) => write!(f, "limit: {e}"),
67 AuthzError::Store(e) => write!(f, "store: {e}"),
68 }
69 }
70}
71
72impl std::error::Error for AuthzError {}
73
74fn name(text: &str, what: &str) -> Result<String, AuthzError> {
75 let ok = !text.is_empty()
76 && text.len() <= MAX_NAME
77 && text.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'));
78 if ok {
79 Ok(text.to_string())
80 } else {
81 Err(AuthzError::Parse(format!("{what} {text:?}: letters, digits, '_', '-' and '.' only, 1–{MAX_NAME} characters")))
82 }
83}
84
85fn id(text: &str) -> Result<String, AuthzError> {
89 let ok = !text.is_empty() && text.len() <= MAX_NAME && !text.bytes().any(|b| b == b'#' || b == b' ' || b.is_ascii_control());
90 if ok {
91 Ok(text.to_string())
92 } else {
93 Err(AuthzError::Parse(format!("id {text:?}: 1–{MAX_NAME} characters, no '#', spaces or control characters")))
94 }
95}
96
97#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
99pub struct ObjectRef {
100 pub kind: String,
101 pub id: String,
102}
103
104impl ObjectRef {
105 pub fn parse(text: &str) -> Result<ObjectRef, AuthzError> {
106 let (kind, rest) = text.split_once(':').ok_or_else(|| AuthzError::Parse(format!("object {text:?}: expected type:id")))?;
107 Ok(ObjectRef { kind: name(kind, "type")?, id: id(rest)? })
108 }
109}
110
111impl fmt::Display for ObjectRef {
112 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113 write!(f, "{}:{}", self.kind, self.id)
114 }
115}
116
117#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
119pub struct Subject {
120 pub object: ObjectRef,
121 pub relation: Option<String>,
122}
123
124impl Subject {
125 pub fn parse(text: &str) -> Result<Subject, AuthzError> {
126 match text.split_once('#') {
127 Some((object, relation)) => Ok(Subject { object: ObjectRef::parse(object)?, relation: Some(name(relation, "relation")?) }),
128 None => Ok(Subject { object: ObjectRef::parse(text)?, relation: None }),
129 }
130 }
131}
132
133impl fmt::Display for Subject {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 match &self.relation {
136 Some(relation) => write!(f, "{}#{relation}", self.object),
137 None => write!(f, "{}", self.object),
138 }
139 }
140}
141
142#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
144pub struct Tuple {
145 pub object: ObjectRef,
146 pub relation: String,
147 pub subject: Subject,
148}
149
150impl Tuple {
151 pub fn parse(text: &str) -> Result<Tuple, AuthzError> {
152 let text = text.trim();
153 let malformed = || AuthzError::Parse(format!("tuple {text:?}: expected object#relation@subject"));
154 let (object, rest) = text.split_once('#').ok_or_else(malformed)?;
155 let (relation, subject) = rest.split_once('@').ok_or_else(malformed)?;
156 Ok(Tuple { object: ObjectRef::parse(object)?, relation: name(relation, "relation")?, subject: Subject::parse(subject)? })
157 }
158}
159
160impl fmt::Display for Tuple {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 write!(f, "{}#{}@{}", self.object, self.relation, self.subject)
163 }
164}
165
166#[derive(Clone, Debug, PartialEq, Eq)]
168pub enum Term {
169 Direct(String),
171 DirectSet(String, String),
173 Computed(String),
175 TupleToUserset { tupleset: String, computed: String },
177}
178
179pub fn parse_expression(text: &str) -> Result<Vec<Term>, AuthzError> {
183 if text.len() > 4096 {
184 return Err(AuthzError::Parse("relation definition over 4096 characters".to_string()));
185 }
186 let mut terms = Vec::new();
187 for part in text.split('|') {
188 let part = part.trim();
189 let term = if let Some((tupleset, computed)) = part.split_once("->") {
190 Term::TupleToUserset { tupleset: name(tupleset.trim(), "relation")?, computed: name(computed.trim(), "relation")? }
191 } else if let Some((kind, relation)) = part.split_once('#') {
192 Term::DirectSet(name(kind.trim(), "type")?, name(relation.trim(), "relation")?)
193 } else {
194 Term::Direct(name(part, "type or relation")?)
195 };
196 terms.push(term);
197 }
198 if terms.is_empty() {
199 return Err(AuthzError::Parse("empty relation definition".to_string()));
200 }
201 Ok(terms)
202}
203
204#[derive(Clone, Debug, Default)]
206pub struct Schema {
207 types: HashMap<String, HashMap<String, Vec<Term>>>,
208 written: Vec<(String, String, String)>,
210}
211
212impl Schema {
213 pub fn new() -> Schema {
214 Schema::default()
215 }
216
217 pub fn relation(mut self, kind: &str, relation: &str, definition: &str) -> Result<Schema, AuthzError> {
221 let (kind, relation) = (name(kind, "type")?, name(relation, "relation")?);
222 let terms = parse_expression(definition)?;
223 self.types.entry(kind.clone()).or_default().insert(relation.clone(), terms);
224 self.written.push((kind, relation, definition.to_string()));
225 Ok(self)
226 }
227
228 pub fn object_type(mut self, kind: &str) -> Result<Schema, AuthzError> {
230 self.types.entry(name(kind, "type")?).or_default();
231 Ok(self)
232 }
233
234 pub fn types(&self) -> Vec<&str> {
236 let mut types: Vec<&str> = self.types.keys().map(String::as_str).collect();
237 types.sort_unstable();
238 types
239 }
240
241 pub fn relations(&self, kind: &str) -> Vec<&str> {
243 let mut relations: Vec<&str> = self.types.get(kind).map(|r| r.keys().map(String::as_str).collect()).unwrap_or_default();
244 relations.sort_unstable();
245 relations
246 }
247
248 pub fn has_relation(&self, kind: &str, relation: &str) -> bool {
249 self.types.get(kind).is_some_and(|r| r.contains_key(relation))
250 }
251
252 pub fn validate(&self) -> Result<Schema, AuthzError> {
257 let mut known: HashSet<String> = self.types.keys().cloned().collect();
258 let mut resolved = self.clone();
259 for (kind, relations) in &self.types {
260 for (relation, terms) in relations {
261 let mut out = Vec::with_capacity(terms.len());
262 for term in terms {
263 out.push(match term {
264 Term::Direct(word) if relations.contains_key(word) && !self.types.contains_key(word) => Term::Computed(word.clone()),
265 Term::Direct(word) => {
266 known.insert(word.clone());
267 Term::Direct(word.clone())
268 }
269 Term::DirectSet(set_kind, set_relation) => {
270 if !self.has_relation(set_kind, set_relation) {
271 return Err(AuthzError::Schema(format!("{kind}#{relation}: {set_kind}#{set_relation} isn't defined")));
272 }
273 term.clone()
274 }
275 Term::TupleToUserset { tupleset, .. } => {
276 let Some(set_terms) = relations.get(tupleset) else {
277 return Err(AuthzError::Schema(format!("{kind}#{relation}: {tupleset} isn't a relation of {kind}")));
278 };
279 if !set_terms.iter().all(|t| matches!(t, Term::Direct(_))) {
280 return Err(AuthzError::Schema(format!("{kind}#{relation}: {tupleset} must name objects directly (only types, no sets or relations)")));
281 }
282 term.clone()
283 }
284 Term::Computed(other) => {
285 if !relations.contains_key(other) {
286 return Err(AuthzError::Schema(format!("{kind}#{relation}: {other} isn't a relation of {kind}")));
287 }
288 term.clone()
289 }
290 });
291 }
292 resolved.types.get_mut(kind).expect("kind").insert(relation.clone(), out);
293 }
294 }
295 for (kind, relations) in &resolved.types {
297 for (relation, terms) in relations {
298 for term in terms {
299 if let Term::TupleToUserset { tupleset, computed } = term {
300 for target in relations[tupleset].iter().filter_map(|t| if let Term::Direct(k) = t { Some(k) } else { None }) {
301 if !resolved.has_relation(target, computed) {
302 return Err(AuthzError::Schema(format!("{kind}#{relation}: {tupleset}->{computed} needs {computed} on {target}")));
303 }
304 }
305 }
306 }
307 }
308 }
309 for kind in known {
310 resolved.types.entry(kind).or_default();
311 }
312 Ok(resolved)
313 }
314
315 fn terms(&self, kind: &str, relation: &str) -> Option<&[Term]> {
316 self.types.get(kind)?.get(relation).map(Vec::as_slice)
317 }
318
319 pub fn allows(&self, tuple: &Tuple) -> Result<(), AuthzError> {
322 let terms = self
323 .terms(&tuple.object.kind, &tuple.relation)
324 .ok_or_else(|| AuthzError::Schema(format!("{}#{} isn't defined", tuple.object.kind, tuple.relation)))?;
325 let fits = terms.iter().any(|term| match (term, &tuple.subject.relation) {
326 (Term::Direct(kind), None) => *kind == tuple.subject.object.kind,
327 (Term::DirectSet(kind, relation), Some(r)) => *kind == tuple.subject.object.kind && relation == r,
328 _ => false,
329 });
330 if fits {
331 Ok(())
332 } else {
333 Err(AuthzError::Schema(format!("{}#{} doesn't take {} as a subject", tuple.object.kind, tuple.relation, tuple.subject)))
334 }
335 }
336}
337
338#[derive(Clone, Debug, Default)]
340pub struct TupleFilter {
341 pub object: Option<ObjectRef>,
342 pub object_kind: Option<String>,
343 pub relation: Option<String>,
344 pub subject: Option<Subject>,
345}
346
347impl TupleFilter {
348 pub fn matches(&self, tuple: &Tuple) -> bool {
349 self.object.as_ref().is_none_or(|o| *o == tuple.object)
350 && self.object_kind.as_ref().is_none_or(|k| *k == tuple.object.kind)
351 && self.relation.as_ref().is_none_or(|r| *r == tuple.relation)
352 && self.subject.as_ref().is_none_or(|s| *s == tuple.subject)
353 }
354}
355
356pub trait TupleStore: Send + Sync {
358 fn write(&self, tuple: &Tuple) -> Result<bool, AuthzError>;
360 fn delete(&self, tuple: &Tuple) -> Result<bool, AuthzError>;
362 fn read(&self, object: &ObjectRef, relation: &str) -> Result<Vec<Subject>, AuthzError>;
364 fn list(&self, filter: &TupleFilter, limit: usize) -> Result<Vec<Tuple>, AuthzError>;
366}
367
368#[derive(Default)]
371pub struct MemoryStore {
372 tuples: RwLock<HashMap<(ObjectRef, String), BTreeSet<Subject>>>,
373}
374
375impl MemoryStore {
376 pub fn new() -> MemoryStore {
377 MemoryStore::default()
378 }
379}
380
381impl TupleStore for MemoryStore {
382 fn write(&self, tuple: &Tuple) -> Result<bool, AuthzError> {
383 let mut tuples = self.tuples.write().unwrap_or_else(|e| e.into_inner());
384 Ok(tuples.entry((tuple.object.clone(), tuple.relation.clone())).or_default().insert(tuple.subject.clone()))
385 }
386
387 fn delete(&self, tuple: &Tuple) -> Result<bool, AuthzError> {
388 let mut tuples = self.tuples.write().unwrap_or_else(|e| e.into_inner());
389 let key = (tuple.object.clone(), tuple.relation.clone());
390 let removed = tuples.get_mut(&key).is_some_and(|subjects| subjects.remove(&tuple.subject));
391 if tuples.get(&key).is_some_and(BTreeSet::is_empty) {
392 tuples.remove(&key);
393 }
394 Ok(removed)
395 }
396
397 fn read(&self, object: &ObjectRef, relation: &str) -> Result<Vec<Subject>, AuthzError> {
398 let tuples = self.tuples.read().unwrap_or_else(|e| e.into_inner());
399 Ok(tuples.get(&(object.clone(), relation.to_string())).map(|s| s.iter().cloned().collect()).unwrap_or_default())
400 }
401
402 fn list(&self, filter: &TupleFilter, limit: usize) -> Result<Vec<Tuple>, AuthzError> {
403 let tuples = self.tuples.read().unwrap_or_else(|e| e.into_inner());
404 let mut out: Vec<Tuple> = tuples
405 .iter()
406 .flat_map(|((object, relation), subjects)| {
407 subjects.iter().map(move |subject| Tuple { object: object.clone(), relation: relation.clone(), subject: subject.clone() })
408 })
409 .filter(|t| filter.matches(t))
410 .collect();
411 out.sort();
412 out.truncate(limit);
413 Ok(out)
414 }
415}
416
417#[derive(Clone)]
420pub struct Authorizer {
421 schema: Arc<Schema>,
422 store: Arc<dyn TupleStore>,
423}
424
425impl fmt::Debug for Authorizer {
426 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 f.debug_struct("Authorizer").field("types", &self.schema.types()).finish_non_exhaustive()
428 }
429}
430
431impl Authorizer {
432 pub fn new(schema: Schema, store: impl TupleStore + 'static) -> Result<Authorizer, AuthzError> {
434 Authorizer::with_store(schema, Arc::new(store))
435 }
436
437 pub fn with_store(schema: Schema, store: Arc<dyn TupleStore>) -> Result<Authorizer, AuthzError> {
438 Ok(Authorizer { schema: Arc::new(schema.validate()?), store })
439 }
440
441 pub fn schema(&self) -> &Schema {
442 &self.schema
443 }
444
445 pub fn store(&self) -> &Arc<dyn TupleStore> {
446 &self.store
447 }
448
449 pub fn write(&self, tuple: &Tuple) -> Result<bool, AuthzError> {
451 self.schema.allows(tuple)?;
452 self.store.write(tuple)
453 }
454
455 pub fn write_str(&self, tuple: &str) -> Result<bool, AuthzError> {
457 self.write(&Tuple::parse(tuple)?)
458 }
459
460 pub fn delete(&self, tuple: &Tuple) -> Result<bool, AuthzError> {
461 self.store.delete(tuple)
462 }
463
464 pub fn check(&self, subject: &str, relation: &str, object: &str) -> Result<bool, AuthzError> {
467 Ok(self.explain(subject, relation, object)?.is_some())
468 }
469
470 pub fn explain(&self, subject: &str, relation: &str, object: &str) -> Result<Option<Vec<String>>, AuthzError> {
473 self.explain_subject(&Subject::parse(subject)?, relation, &ObjectRef::parse(object)?)
474 }
475
476 pub fn check_subject(&self, subject: &Subject, relation: &str, object: &ObjectRef) -> Result<bool, AuthzError> {
479 Ok(self.explain_subject(subject, relation, object)?.is_some())
480 }
481
482 pub fn explain_subject(&self, subject: &Subject, relation: &str, object: &ObjectRef) -> Result<Option<Vec<String>>, AuthzError> {
484 if !self.schema.has_relation(&object.kind, relation) {
485 return Err(AuthzError::Schema(format!("{}#{relation} isn't defined", object.kind)));
486 }
487 Walk::new(self).run(subject, object.clone(), relation.to_string())
488 }
489}
490
491struct Walk<'a> {
494 authz: &'a Authorizer,
495 reads: usize,
496 nodes: Vec<(ObjectRef, String, Option<usize>, usize)>,
498}
499
500impl<'a> Walk<'a> {
501 fn new(authz: &'a Authorizer) -> Walk<'a> {
502 Walk { authz, reads: 0, nodes: Vec::new() }
503 }
504
505 fn read(&mut self, object: &ObjectRef, relation: &str) -> Result<Vec<Subject>, AuthzError> {
506 self.reads += 1;
507 if self.reads > MAX_READS {
508 return Err(AuthzError::Limit(format!("more than {MAX_READS} tuple reads")));
509 }
510 self.authz.store.read(object, relation)
511 }
512
513 fn path(&self, mut at: usize, last: String) -> Vec<String> {
514 let mut steps = vec![last];
515 loop {
516 let (object, relation, parent, _) = &self.nodes[at];
517 steps.push(format!("{object}#{relation}"));
518 match parent {
519 Some(p) => at = *p,
520 None => break,
521 }
522 }
523 steps.reverse();
524 steps
525 }
526
527 fn run(mut self, subject: &Subject, object: ObjectRef, relation: String) -> Result<Option<Vec<String>>, AuthzError> {
528 let mut visited: HashSet<(ObjectRef, String)> = HashSet::new();
529 self.nodes.push((object, relation, None, 0));
530 let mut stack = vec![0usize];
531 while let Some(at) = stack.pop() {
532 let (object, relation, _, depth) = self.nodes[at].clone();
533 if !visited.insert((object.clone(), relation.clone())) {
534 continue;
535 }
536 if subject.relation.as_deref() == Some(relation.as_str()) && subject.object == object {
538 return Ok(Some(self.path(at, format!("{subject}"))));
539 }
540 if depth >= MAX_DEPTH {
541 return Err(AuthzError::Limit(format!("deeper than {MAX_DEPTH} relations")));
542 }
543 let Some(terms) = self.authz.schema.terms(&object.kind, &relation) else { continue };
544 let terms = terms.to_vec();
545 let mut direct_read = false;
546 for term in &terms {
547 match term {
548 Term::Direct(_) | Term::DirectSet(..) => {
549 if direct_read {
550 continue;
551 }
552 direct_read = true;
553 for found in self.read(&object, &relation)? {
554 if found == *subject {
555 return Ok(Some(self.path(at, format!("{object}#{relation}@{found}"))));
556 }
557 if let Some(set_relation) = &found.relation {
558 self.nodes.push((found.object.clone(), set_relation.clone(), Some(at), depth + 1));
559 stack.push(self.nodes.len() - 1);
560 }
561 }
562 }
563 Term::Computed(other) => {
564 self.nodes.push((object.clone(), other.clone(), Some(at), depth + 1));
565 stack.push(self.nodes.len() - 1);
566 }
567 Term::TupleToUserset { tupleset, computed } => {
568 for found in self.read(&object, tupleset)? {
569 if found.relation.is_none() {
570 self.nodes.push((found.object.clone(), computed.clone(), Some(at), depth + 1));
571 stack.push(self.nodes.len() - 1);
572 }
573 }
574 }
575 }
576 }
577 }
578 Ok(None)
579 }
580}
581
582#[cfg(test)]
583mod tests;