1mod constraint;
34mod interp;
35mod library;
36mod list;
37mod time;
38mod trace;
39mod value;
40
41pub use constraint::Comparator;
42pub use trace::{Evaluation, KeyStatus, Observed, Outcome, Segment, Trace, Verdict};
43pub(crate) use trace::{NodeKey, Traced, hash_val, observe, shows};
44pub use value::java_double;
45
46use std::rc::Rc;
47
48use indexmap::IndexMap;
49
50use crate::{
51 Ruleset, ast,
52 paths::{Binding, Covering, Part},
53};
54use interp::{Frame, Interp, Level};
55pub(crate) use value::Val;
56
57#[derive(Debug, Clone, PartialEq)]
60pub enum Value {
61 Null,
62 Bool(bool),
63 Int(i64),
64 Float(f64),
65 String(String),
66 Bytes(Vec<u8>),
67 Timestamp(Timestamp),
68 Duration(i64, i32),
70 LatLng(f64, f64),
72 Path(Vec<String>),
75 List(Vec<Value>),
76 Map(Map),
77}
78
79pub type Map = IndexMap<String, Value>;
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
84pub struct Timestamp {
85 pub seconds: i64,
86 pub nanos: i32,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum Method {
93 Get,
94 List,
95 Create,
96 Update,
97 Delete,
98}
99
100impl Method {
101 pub fn name(self) -> &'static str {
103 match self {
104 Self::Get => "get",
105 Self::List => "list",
106 Self::Create => "create",
107 Self::Update => "update",
108 Self::Delete => "delete",
109 }
110 }
111
112 fn statement(self) -> ast::Method {
113 match self {
114 Self::Get => ast::Method::Get,
115 Self::List => ast::Method::List,
116 Self::Create => ast::Method::Create,
117 Self::Update => ast::Method::Update,
118 Self::Delete => ast::Method::Delete,
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq)]
125pub struct Document {
126 pub path: Vec<String>,
129 pub data: Map,
130}
131
132#[derive(Debug, Clone, PartialEq)]
134pub struct Auth {
135 pub uid: Option<String>,
137 pub token: Map,
139}
140
141#[derive(Debug, Clone, PartialEq, Default)]
150pub struct Query {
151 pub limit: Option<i64>,
152 pub offset: Option<i64>,
153 pub order_by: Vec<(String, String)>,
156 pub filter: Option<Filter>,
157 pub group: Option<String>,
160 pub children: bool,
163 pub fields: Option<Vec<String>>,
166}
167
168#[derive(Debug, Clone, PartialEq)]
170pub enum Filter {
171 And(Vec<Filter>),
172 Or(Vec<Filter>),
173 Field {
176 field: Vec<String>,
177 op: FilterOp,
178 value: Value,
179 },
180 Name {
182 op: FilterOp,
183 value: Value,
184 },
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum FilterOp {
189 Lt,
190 Le,
191 Gt,
192 Ge,
193 Eq,
194 Ne,
195 ArrayContains,
196 In,
197 ArrayContainsAny,
198 NotIn,
199 IsNan,
200 IsNull,
201 IsNotNan,
202 IsNotNull,
203}
204
205#[derive(Debug, Clone, PartialEq)]
207pub struct Request {
208 pub method: Method,
209 pub upsert: bool,
214 pub path: Vec<String>,
216 pub auth: Option<Auth>,
217 pub time: Timestamp,
218 pub resource: Option<Document>,
220 pub query: Option<Query>,
221 pub in_transaction: bool,
226 pub write_fields: Option<Vec<String>>,
229 pub transforms: Option<Vec<String>>,
232 pub auto_id: bool,
238}
239
240pub trait Documents {
243 fn get(&self, path: &[String]) -> Option<Document>;
244 fn get_after(&self, path: &[String]) -> Option<Document>;
245}
246
247#[derive(Debug, Clone)]
253pub struct Budget {
254 pub looked_up: Vec<Vec<String>>,
256 pub this_part: usize,
258 pub part_limit: usize,
259 pub total_limit: usize,
260 pub written: Vec<Vec<String>>,
262}
263
264impl Budget {
265 fn with(part_limit: usize, total_limit: usize) -> Self {
266 Self {
267 looked_up: Vec::new(),
268 this_part: 0,
269 part_limit,
270 total_limit,
271 written: Vec::new(),
272 }
273 }
274
275 pub fn add_written(&mut self, path: Vec<String>) {
278 if !self.written.contains(&path) {
279 self.written.push(path);
280 }
281 }
282
283 pub fn single() -> Self {
285 Self::with(10, 10)
286 }
287
288 pub fn query() -> Self {
290 Self::with(20, 20)
291 }
292
293 pub fn batch() -> Self {
295 Self::with(10, 20)
296 }
297
298 pub fn next_write(&mut self) {
299 self.this_part = 0;
300 }
301}
302
303#[derive(Debug, Clone, PartialEq, Eq)]
305pub enum Decision {
306 Allow,
307 Deny(String),
309}
310
311pub fn authorize(
324 ruleset: &Ruleset,
325 request: &Request,
326 resource: Option<&Document>,
327 documents: &dyn Documents,
328 budget: &mut Budget,
329) -> Decision {
330 authorize_traced(
331 ruleset,
332 request,
333 resource,
334 documents,
335 budget,
336 &mut Trace::new(),
337 )
338}
339
340pub fn authorize_traced(
346 ruleset: &Ruleset,
347 request: &Request,
348 resource: Option<&Document>,
349 documents: &dyn Documents,
350 budget: &mut Budget,
351 trace: &mut Trace,
352) -> Decision {
353 let Some(service) = ruleset.service("cloud.firestore") else {
354 return unmatched(request, resource.map_or(Val::Null, value::resource), trace);
355 };
356 if request.method == Method::List {
357 return list::authorize(ruleset, service, request, documents, budget, trace);
358 }
359 let mut parts: Vec<Part<'_>> = request.path.iter().map(|s| Part::Segment(s)).collect();
360 if request.auto_id {
361 parts.push(Part::UnknownId);
362 }
363 let coverings = crate::paths::covering_parts(ruleset.version, service, &parts);
364 let other = match request.method {
365 Method::Create if request.upsert => Some(Method::Update),
366 Method::Update if request.upsert => Some(Method::Create),
367 _ => None,
368 };
369 let applying = statements(&coverings, request.method);
370 let others = other.map(|other| (other, statements(&coverings, other)));
371 if applying.is_empty() && others.as_ref().is_none_or(|(_, s)| s.is_empty()) {
372 return unmatched(request, resource.map_or(Val::Null, value::resource), trace);
373 }
374
375 let context_for = |method| Context {
377 ruleset,
378 service,
379 request: request_value(request, method),
380 resource: resource.map_or(Val::Null, value::resource),
381 target: &request.path,
382 documents,
383 };
384 let context = context_for(request.method);
385 let Some((other, others)) = others else {
386 return match context.decide(&applying, request.method, budget, trace) {
387 Ok(()) => Decision::Allow,
388 Err(entries) => denied(&entries),
389 };
390 };
391 let other_context = context_for(other);
392 if other == Method::Create {
395 other_context.each(&others, other, &mut budget.clone(), trace);
396 }
397 let decided = context.decide(&applying, request.method, budget, trace);
398 let Err(entries) = decided else {
399 if other == Method::Update {
400 other_context.each(&others, other, &mut budget.clone(), trace);
401 }
402 return Decision::Allow;
403 };
404 let listed = match other {
407 Method::Create => {
408 other_context.listed(&others, other, &mut budget.clone(), &mut Trace::new())
409 }
410 _ => other_context.listed(&others, other, &mut budget.clone(), trace),
411 };
412 let entries: Vec<String> = match other {
413 Method::Create => listed.into_iter().chain(entries).collect(),
414 _ => entries.into_iter().chain(listed).collect(),
415 };
416 denied(&entries)
417}
418
419fn no_match() -> Decision {
420 Decision::Deny("No matching allow statements".to_owned())
421}
422
423fn unmatched(request: &Request, resource: Val, trace: &mut Trace) -> Decision {
425 if trace.evaluations.is_some() {
426 trace.evaluating(
427 request.method,
428 &request_value(request, request.method),
429 &resource,
430 );
431 }
432 no_match()
433}
434
435fn denied(entries: &[String]) -> Decision {
437 Decision::Deny(format!("\n{}", entries.join(", ")))
438}
439
440fn statements<'c, 'a>(
442 coverings: &'c [Covering<'a>],
443 method: Method,
444) -> Vec<(&'c ast::Allow, &'c Covering<'a>)> {
445 let method = method.statement();
446 let mut statements = Vec::new();
447 for covering in coverings {
448 for allow in &covering.target().body.allows {
449 if allow
450 .methods
451 .iter()
452 .any(|m| m.method.is_some_and(|m| m.covers(method)))
453 {
454 statements.push((allow, covering));
455 }
456 }
457 }
458 statements.sort_by_key(|(allow, _)| allow.span.start);
459 statements
460}
461
462fn verdict(result: &Result<bool, String>) -> Verdict {
464 match result {
465 Ok(true) => Verdict::Allow,
466 Ok(false) => Verdict::False,
467 Err(_) => Verdict::Error,
468 }
469}
470
471fn entry(text: &str, method: Method, line: u32) -> String {
472 format!("{text} for '{}' @ L{line}", method.name())
473}
474
475struct Context<'a> {
477 ruleset: &'a Ruleset,
478 service: &'a ast::Service,
479 request: Val,
480 resource: Val,
481 target: &'a [String],
483 documents: &'a dyn Documents,
484}
485
486impl Context<'_> {
487 fn decide(
491 &self,
492 statements: &[(&ast::Allow, &Covering<'_>)],
493 method: Method,
494 budget: &mut Budget,
495 trace: &mut Trace,
496 ) -> Result<(), Vec<String>> {
497 let (entries, allowed) = self.each(statements, method, budget, trace);
498 if allowed { Ok(()) } else { Err(entries) }
499 }
500
501 fn listed(
504 &self,
505 statements: &[(&ast::Allow, &Covering<'_>)],
506 method: Method,
507 budget: &mut Budget,
508 trace: &mut Trace,
509 ) -> Vec<String> {
510 let mut entries = Vec::with_capacity(statements.len());
511 let mut marks = Vec::with_capacity(statements.len());
512 let at = trace.evaluating(method, &self.request, &self.resource);
513 for (allow, covering) in statements {
514 marks.push(trace.mark());
515 let (result, line) = match &allow.condition {
516 Some(condition) => (
517 self.evaluate(covering, condition, budget, trace),
518 condition.span.line,
519 ),
520 None => (Ok(true), allow.span.line),
521 };
522 trace.verdict(at, line, verdict(&result));
523 let text = result.err();
524 entries.push(entry(text.as_deref().unwrap_or("false"), method, line));
525 }
526 trace.repeat(&marks);
527 entries
528 }
529
530 fn each(
533 &self,
534 statements: &[(&ast::Allow, &Covering<'_>)],
535 method: Method,
536 budget: &mut Budget,
537 trace: &mut Trace,
538 ) -> (Vec<String>, bool) {
539 let mut entries = Vec::with_capacity(statements.len());
540 let mut marks = Vec::with_capacity(statements.len());
541 let mut allowed: Vec<*const ast::Match> = Vec::new();
542 let at = trace.evaluating(method, &self.request, &self.resource);
543 for (allow, covering) in statements {
544 let target = std::ptr::from_ref(covering.target());
545 if allowed.contains(&target) {
546 continue;
547 }
548 marks.push(trace.mark());
549 let Some(condition) = &allow.condition else {
550 trace.verdict(at, allow.span.line, Verdict::Allow);
551 allowed.push(target);
552 continue;
553 };
554 let result = self.evaluate(covering, condition, budget, trace);
555 trace.verdict(at, condition.span.line, verdict(&result));
556 let text = match result {
557 Ok(true) => {
558 allowed.push(target);
559 continue;
560 }
561 Ok(false) => "false".to_owned(),
562 Err(e) => e,
563 };
564 entries.push(entry(&text, method, condition.span.line));
565 }
566 trace.repeat(&marks);
567 (entries, !allowed.is_empty())
568 }
569
570 fn evaluate(
572 &self,
573 covering: &Covering<'_>,
574 condition: &ast::Expr,
575 budget: &mut Budget,
576 trace: &mut Trace,
577 ) -> Result<bool, String> {
578 let mut interp = Interp::new(
579 levels(self.ruleset, self.service, covering),
580 self.request.clone(),
581 self.resource.clone(),
582 self.target,
583 self.documents,
584 budget,
585 trace,
586 &self.ruleset.parens,
587 );
588 let frame = Frame::condition(covering.chain.len() + 1);
589 interp
590 .eval(condition, frame.scope())
591 .and_then(interp::boolean)
592 .map_err(|e| {
593 if e.message.is_empty() {
594 format!(
596 "evaluation error at L{}:{}",
597 condition.span.line, condition.span.column
598 )
599 } else {
600 e.message
601 }
602 })
603 }
604}
605
606fn levels<'a>(
609 ruleset: &'a Ruleset,
610 service: &'a ast::Service,
611 covering: &'a Covering<'a>,
612) -> Vec<Level<'a>> {
613 let mut levels = vec![
614 Level {
615 functions: &ruleset.functions,
616 variables: Vec::new(),
617 },
618 Level {
619 functions: &service.body.functions,
620 variables: Vec::new(),
621 },
622 ];
623 let mut bindings = covering.bindings.iter();
624 for m in &covering.chain {
625 let count = m
626 .path
627 .iter()
628 .filter(|s| s.kind.variable().is_some())
629 .count();
630 let variables = bindings
631 .by_ref()
632 .take(count)
633 .filter_map(|(name, binding)| {
636 let value = match binding {
637 Binding::Segment(s) => Val::str(s),
638 Binding::Path(p) => Val::path(p.clone()),
639 Binding::Unbound => return None,
640 };
641 Some((name.as_str(), value))
642 })
643 .collect();
644 levels.push(Level {
645 functions: &m.body.functions,
646 variables,
647 });
648 }
649 levels
650}
651
652fn request_value(request: &Request, method: Method) -> Val {
663 let strings = |list: &Option<Vec<String>>| {
664 list.as_ref().map_or(Val::Null, |items| {
665 Val::list(items.iter().map(|s| Val::str(s)).collect())
666 })
667 };
668 let mut map = IndexMap::new();
669 map.insert(
670 "time".to_owned(),
671 Val::from(&Value::Timestamp(request.time)),
672 );
673 map.insert("auth".to_owned(), auth_value(request));
674 map.insert(
675 "inTransaction".to_owned(),
676 Val::Bool(request.in_transaction),
677 );
678 let path = Val::path(request.path.clone());
679 match method {
680 Method::Get => {
681 map.insert("path".to_owned(), path);
682 map.insert("fields".to_owned(), Val::Null);
683 }
684 Method::List => {
685 let query = request.query.clone().unwrap_or_default();
686 map.insert("fields".to_owned(), strings(&query.fields));
687 map.insert("query".to_owned(), query_value(request, &query));
688 map.insert("path".to_owned(), path);
689 }
690 Method::Create | Method::Update | Method::Delete => {
691 map.insert("path".to_owned(), path);
692 map.insert("transforms".to_owned(), strings(&request.transforms));
693 map.insert("writeFields".to_owned(), strings(&request.write_fields));
694 map.insert("readFields".to_owned(), Val::Null);
695 if method == Method::Create && !request.upsert {
696 map.insert("autoId".to_owned(), Val::Bool(request.auto_id));
697 }
698 let resource = match &request.resource {
699 None => Val::Null,
700 Some(doc) if request.auto_id => match value::resource(doc) {
702 Val::Map(map) => {
703 let data = map.get("data").cloned().unwrap_or(Val::Null);
704 Val::Map(Rc::new(IndexMap::from([("data".to_owned(), data)])))
705 }
706 other => other,
707 },
708 Some(doc) => value::resource(doc),
709 };
710 map.insert("resource".to_owned(), resource);
711 }
712 }
713 map.insert("method".to_owned(), Val::str(method.name()));
714 Val::Map(Rc::new(map))
715}
716
717fn query_value(request: &Request, query: &Query) -> Val {
719 let all_descendants = query.group.is_some() && !query.children;
720 let (kind, parent) = match &query.group {
722 None => match request.path.split_last() {
723 Some((id, parent)) => (Val::str(id), parent),
724 None => (Val::Null, &[][..]),
725 },
726 Some(id) if id.is_empty() => (Val::Null, request.path.as_slice()),
727 Some(id) => (Val::str(id), request.path.as_slice()),
728 };
729 let parent = if parent.len() > 3 {
731 Val::path(parent.to_vec())
732 } else {
733 Val::Null
734 };
735 let order_by = query
736 .order_by
737 .iter()
738 .map(|(field, direction)| (field.clone(), Val::str(direction)))
739 .collect();
740 let mut q = IndexMap::new();
741 q.insert("distinct".to_owned(), Val::Bool(false));
742 q.insert("selectOnlyKeys".to_owned(), Val::Bool(false));
743 q.insert("groupBy".to_owned(), Val::Map(Rc::default()));
744 q.insert("kind".to_owned(), kind);
745 let parent_key = if all_descendants {
746 "ancestor"
747 } else {
748 "parent"
749 };
750 q.insert(parent_key.to_owned(), parent);
751 q.insert("orderBy".to_owned(), Val::Map(Rc::new(order_by)));
752 q.insert("limit".to_owned(), query.limit.map_or(Val::Null, Val::Int));
753 q.insert("offset".to_owned(), Val::Int(query.offset.unwrap_or(0)));
754 q.insert("allDescendants".to_owned(), Val::Bool(all_descendants));
755 Val::Map(Rc::new(q))
756}
757
758fn auth_value(request: &Request) -> Val {
760 match &request.auth {
761 None => Val::Null,
762 Some(auth) => {
763 let mut a = IndexMap::new();
764 a.insert(
765 "uid".to_owned(),
766 auth.uid.as_deref().map_or(Val::Null, Val::str),
767 );
768 a.insert(
769 "token".to_owned(),
770 Val::from(&Value::Map(auth.token.clone())),
771 );
772 Val::Map(Rc::new(a))
773 }
774 }
775}