1mod constraint;
34mod interp;
35mod library;
36mod list;
37mod time;
38mod trace;
39mod value;
40
41pub use constraint::Comparator;
42pub use trace::{KeyStatus, Observed, Outcome, Segment, Trace};
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},
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)]
143pub struct Query {
144 pub limit: Option<i64>,
145 pub offset: Option<i64>,
146 pub order_by: Vec<(String, String)>,
148 pub filter: Option<Filter>,
149 pub group: Option<String>,
152}
153
154#[derive(Debug, Clone, PartialEq)]
156pub enum Filter {
157 And(Vec<Filter>),
158 Or(Vec<Filter>),
159 Field {
162 field: Vec<String>,
163 op: FilterOp,
164 value: Value,
165 },
166 Name {
168 op: FilterOp,
169 value: Value,
170 },
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum FilterOp {
175 Lt,
176 Le,
177 Gt,
178 Ge,
179 Eq,
180 Ne,
181 ArrayContains,
182 In,
183 ArrayContainsAny,
184 NotIn,
185 IsNan,
186 IsNull,
187 IsNotNan,
188 IsNotNull,
189}
190
191#[derive(Debug, Clone, PartialEq)]
193pub struct Request {
194 pub method: Method,
195 pub upsert: bool,
200 pub path: Vec<String>,
202 pub auth: Option<Auth>,
203 pub time: Timestamp,
204 pub resource: Option<Document>,
206 pub query: Option<Query>,
207}
208
209pub trait Documents {
212 fn get(&self, path: &[String]) -> Option<Document>;
213 fn get_after(&self, path: &[String]) -> Option<Document>;
214}
215
216#[derive(Debug, Clone)]
222pub struct Budget {
223 pub looked_up: Vec<Vec<String>>,
225 pub this_part: usize,
227 pub part_limit: usize,
228 pub total_limit: usize,
229 pub written: Vec<Vec<String>>,
231}
232
233impl Budget {
234 fn with(part_limit: usize, total_limit: usize) -> Self {
235 Self {
236 looked_up: Vec::new(),
237 this_part: 0,
238 part_limit,
239 total_limit,
240 written: Vec::new(),
241 }
242 }
243
244 pub fn add_written(&mut self, path: Vec<String>) {
247 if !self.written.contains(&path) {
248 self.written.push(path);
249 }
250 }
251
252 pub fn single() -> Self {
254 Self::with(10, 10)
255 }
256
257 pub fn query() -> Self {
259 Self::with(20, 20)
260 }
261
262 pub fn batch() -> Self {
264 Self::with(10, 20)
265 }
266
267 pub fn next_write(&mut self) {
268 self.this_part = 0;
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
274pub enum Decision {
275 Allow,
276 Deny(String),
278}
279
280pub fn authorize(
293 ruleset: &Ruleset,
294 request: &Request,
295 resource: Option<&Document>,
296 documents: &dyn Documents,
297 budget: &mut Budget,
298) -> Decision {
299 authorize_traced(
300 ruleset,
301 request,
302 resource,
303 documents,
304 budget,
305 &mut Trace::new(),
306 )
307}
308
309pub fn authorize_traced(
315 ruleset: &Ruleset,
316 request: &Request,
317 resource: Option<&Document>,
318 documents: &dyn Documents,
319 budget: &mut Budget,
320 trace: &mut Trace,
321) -> Decision {
322 let Some(service) = ruleset.service("cloud.firestore") else {
323 return no_match();
324 };
325 if request.method == Method::List {
326 return list::authorize(ruleset, service, request, documents, budget, trace);
327 }
328 let path: Vec<&str> = request.path.iter().map(String::as_str).collect();
329 let coverings = crate::paths::covering(ruleset.version, service, &path);
330 let other = match request.method {
331 Method::Create if request.upsert => Some(Method::Update),
332 Method::Update if request.upsert => Some(Method::Create),
333 _ => None,
334 };
335 let applying = statements(&coverings, request.method);
336 let others = other.map(|other| (other, statements(&coverings, other)));
337 if applying.is_empty() && others.as_ref().is_none_or(|(_, s)| s.is_empty()) {
338 return no_match();
339 }
340
341 let context = Context {
342 ruleset,
343 service,
344 request: request_value(request),
345 resource: resource.map_or(Val::Null, value::resource),
346 target: &request.path,
347 documents,
348 };
349 let Some((other, others)) = others else {
350 return match context.decide(&applying, request.method, budget, trace) {
351 Ok(()) => Decision::Allow,
352 Err(entries) => denied(&entries),
353 };
354 };
355 if other == Method::Create {
358 context.each(&others, other, &mut budget.clone(), trace);
359 }
360 let decided = context.decide(&applying, request.method, budget, trace);
361 let Err(entries) = decided else {
362 if other == Method::Update {
363 context.each(&others, other, &mut budget.clone(), trace);
364 }
365 return Decision::Allow;
366 };
367 let listed = match other {
370 Method::Create => context.listed(&others, other, &mut budget.clone(), &mut Trace::new()),
371 _ => context.listed(&others, other, &mut budget.clone(), trace),
372 };
373 let entries: Vec<String> = match other {
374 Method::Create => listed.into_iter().chain(entries).collect(),
375 _ => entries.into_iter().chain(listed).collect(),
376 };
377 denied(&entries)
378}
379
380fn no_match() -> Decision {
381 Decision::Deny("No matching allow statements".to_owned())
382}
383
384fn denied(entries: &[String]) -> Decision {
386 Decision::Deny(format!("\n{}", entries.join(", ")))
387}
388
389fn statements<'c, 'a>(
391 coverings: &'c [Covering<'a>],
392 method: Method,
393) -> Vec<(&'c ast::Allow, &'c Covering<'a>)> {
394 let method = method.statement();
395 let mut statements = Vec::new();
396 for covering in coverings {
397 for allow in &covering.target().body.allows {
398 if allow
399 .methods
400 .iter()
401 .any(|m| m.method.is_some_and(|m| m.covers(method)))
402 {
403 statements.push((allow, covering));
404 }
405 }
406 }
407 statements.sort_by_key(|(allow, _)| allow.span.start);
408 statements
409}
410
411fn entry(text: &str, method: Method, line: u32) -> String {
412 format!("{text} for '{}' @ L{line}", method.name())
413}
414
415struct Context<'a> {
417 ruleset: &'a Ruleset,
418 service: &'a ast::Service,
419 request: Val,
420 resource: Val,
421 target: &'a [String],
423 documents: &'a dyn Documents,
424}
425
426impl Context<'_> {
427 fn decide(
431 &self,
432 statements: &[(&ast::Allow, &Covering<'_>)],
433 method: Method,
434 budget: &mut Budget,
435 trace: &mut Trace,
436 ) -> Result<(), Vec<String>> {
437 let (entries, allowed) = self.each(statements, method, budget, trace);
438 if allowed { Ok(()) } else { Err(entries) }
439 }
440
441 fn listed(
444 &self,
445 statements: &[(&ast::Allow, &Covering<'_>)],
446 method: Method,
447 budget: &mut Budget,
448 trace: &mut Trace,
449 ) -> Vec<String> {
450 let mut entries = Vec::with_capacity(statements.len());
451 let mut marks = Vec::with_capacity(statements.len());
452 for (allow, covering) in statements {
453 marks.push(trace.mark());
454 let (text, line) = match &allow.condition {
455 Some(condition) => (
456 self.evaluate(covering, condition, budget, trace).err(),
457 condition.span.line,
458 ),
459 None => (None, allow.span.line),
460 };
461 entries.push(entry(text.as_deref().unwrap_or("false"), method, line));
462 }
463 trace.repeat(&marks);
464 entries
465 }
466
467 fn each(
470 &self,
471 statements: &[(&ast::Allow, &Covering<'_>)],
472 method: Method,
473 budget: &mut Budget,
474 trace: &mut Trace,
475 ) -> (Vec<String>, bool) {
476 let mut entries = Vec::with_capacity(statements.len());
477 let mut marks = Vec::with_capacity(statements.len());
478 let mut allowed: Vec<*const ast::Match> = Vec::new();
479 for (allow, covering) in statements {
480 let target = std::ptr::from_ref(covering.target());
481 if allowed.contains(&target) {
482 continue;
483 }
484 marks.push(trace.mark());
485 let Some(condition) = &allow.condition else {
486 allowed.push(target);
487 continue;
488 };
489 let text = match self.evaluate(covering, condition, budget, trace) {
490 Ok(true) => {
491 allowed.push(target);
492 continue;
493 }
494 Ok(false) => "false".to_owned(),
495 Err(e) => e,
496 };
497 entries.push(entry(&text, method, condition.span.line));
498 }
499 trace.repeat(&marks);
500 (entries, !allowed.is_empty())
501 }
502
503 fn evaluate(
505 &self,
506 covering: &Covering<'_>,
507 condition: &ast::Expr,
508 budget: &mut Budget,
509 trace: &mut Trace,
510 ) -> Result<bool, String> {
511 let mut interp = Interp::new(
512 levels(self.ruleset, self.service, covering),
513 self.request.clone(),
514 self.resource.clone(),
515 self.target,
516 self.documents,
517 budget,
518 trace,
519 &self.ruleset.parens,
520 );
521 let frame = Frame::condition(covering.chain.len() + 1);
522 interp
523 .eval(condition, frame.scope())
524 .and_then(interp::boolean)
525 .map_err(|e| {
526 if e.message.is_empty() {
527 format!(
529 "evaluation error at L{}:{}",
530 condition.span.line, condition.span.column
531 )
532 } else {
533 e.message
534 }
535 })
536 }
537}
538
539fn levels<'a>(
542 ruleset: &'a Ruleset,
543 service: &'a ast::Service,
544 covering: &'a Covering<'a>,
545) -> Vec<Level<'a>> {
546 let mut levels = vec![
547 Level {
548 functions: &ruleset.functions,
549 variables: Vec::new(),
550 },
551 Level {
552 functions: &service.body.functions,
553 variables: Vec::new(),
554 },
555 ];
556 let mut bindings = covering.bindings.iter();
557 for m in &covering.chain {
558 let count = m
559 .path
560 .iter()
561 .filter(|s| s.kind.variable().is_some())
562 .count();
563 let variables = bindings
564 .by_ref()
565 .take(count)
566 .filter_map(|(name, binding)| {
569 let value = match binding {
570 Binding::Segment(s) => Val::str(s),
571 Binding::Path(p) => Val::path(p.clone()),
572 Binding::Unbound => return None,
573 };
574 Some((name.as_str(), value))
575 })
576 .collect();
577 levels.push(Level {
578 functions: &m.body.functions,
579 variables,
580 });
581 }
582 levels
583}
584
585fn request_value(request: &Request) -> Val {
588 let mut map = IndexMap::new();
589 let auth = match &request.auth {
590 None => Val::Null,
591 Some(auth) => {
592 let mut a = IndexMap::new();
593 a.insert(
594 "uid".to_owned(),
595 auth.uid.as_deref().map_or(Val::Null, Val::str),
596 );
597 a.insert(
598 "token".to_owned(),
599 Val::from(&Value::Map(auth.token.clone())),
600 );
601 Val::Map(Rc::new(a))
602 }
603 };
604 map.insert("auth".to_owned(), auth);
605 map.insert("method".to_owned(), Val::str(request.method.name()));
606 map.insert("path".to_owned(), Val::path(request.path.clone()));
607 map.insert(
608 "time".to_owned(),
609 Val::from(&Value::Timestamp(request.time)),
610 );
611 if matches!(
612 request.method,
613 Method::Create | Method::Update | Method::Delete
614 ) {
615 let resource = request.resource.as_ref().map_or(Val::Null, value::resource);
616 map.insert("resource".to_owned(), resource);
617 }
618 if let Some(query) = &request.query {
619 let mut q = IndexMap::new();
620 q.insert("limit".to_owned(), query.limit.map_or(Val::Null, Val::Int));
621 q.insert("offset".to_owned(), Val::Int(query.offset.unwrap_or(0)));
622 let order_by = query
623 .order_by
624 .iter()
625 .map(|(field, direction)| (field.clone(), Val::str(direction)))
626 .collect();
627 q.insert("orderBy".to_owned(), Val::Map(Rc::new(order_by)));
628 map.insert("query".to_owned(), Val::Map(Rc::new(q)));
629 }
630 Val::Map(Rc::new(map))
631}