mod constraint;
mod interp;
mod library;
mod list;
mod time;
mod trace;
mod value;
pub use constraint::Comparator;
pub use trace::{Evaluation, KeyStatus, Observed, Outcome, Segment, Trace, Verdict};
pub(crate) use trace::{NodeKey, Traced, hash_val, observe, shows};
pub use value::java_double;
use std::rc::Rc;
use indexmap::IndexMap;
use crate::{
Ruleset, ast,
paths::{Binding, Covering, Part},
};
use interp::{Frame, Interp, Level};
pub(crate) use value::Val;
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Null,
Bool(bool),
Int(i64),
Float(f64),
String(String),
Bytes(Vec<u8>),
Timestamp(Timestamp),
Duration(i64, i32),
LatLng(f64, f64),
Path(Vec<String>),
List(Vec<Value>),
Map(Map),
}
pub type Map = IndexMap<String, Value>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Timestamp {
pub seconds: i64,
pub nanos: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Method {
Get,
List,
Create,
Update,
Delete,
}
impl Method {
pub fn name(self) -> &'static str {
match self {
Self::Get => "get",
Self::List => "list",
Self::Create => "create",
Self::Update => "update",
Self::Delete => "delete",
}
}
fn statement(self) -> ast::Method {
match self {
Self::Get => ast::Method::Get,
Self::List => ast::Method::List,
Self::Create => ast::Method::Create,
Self::Update => ast::Method::Update,
Self::Delete => ast::Method::Delete,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Document {
pub path: Vec<String>,
pub data: Map,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Auth {
pub uid: Option<String>,
pub token: Map,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Query {
pub limit: Option<i64>,
pub offset: Option<i64>,
pub order_by: Vec<(String, String)>,
pub filter: Option<Filter>,
pub group: Option<String>,
pub children: bool,
pub fields: Option<Vec<String>>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Filter {
And(Vec<Filter>),
Or(Vec<Filter>),
Field {
field: Vec<String>,
op: FilterOp,
value: Value,
},
Name {
op: FilterOp,
value: Value,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FilterOp {
Lt,
Le,
Gt,
Ge,
Eq,
Ne,
ArrayContains,
In,
ArrayContainsAny,
NotIn,
IsNan,
IsNull,
IsNotNan,
IsNotNull,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Request {
pub method: Method,
pub upsert: bool,
pub path: Vec<String>,
pub auth: Option<Auth>,
pub time: Timestamp,
pub resource: Option<Document>,
pub query: Option<Query>,
pub in_transaction: bool,
pub write_fields: Option<Vec<String>>,
pub transforms: Option<Vec<String>>,
pub auto_id: bool,
}
pub trait Documents {
fn get(&self, path: &[String]) -> Option<Document>;
fn get_after(&self, path: &[String]) -> Option<Document>;
}
#[derive(Debug, Clone)]
pub struct Budget {
pub looked_up: Vec<Vec<String>>,
pub this_part: usize,
pub part_limit: usize,
pub total_limit: usize,
pub written: Vec<Vec<String>>,
}
impl Budget {
fn with(part_limit: usize, total_limit: usize) -> Self {
Self {
looked_up: Vec::new(),
this_part: 0,
part_limit,
total_limit,
written: Vec::new(),
}
}
pub fn add_written(&mut self, path: Vec<String>) {
if !self.written.contains(&path) {
self.written.push(path);
}
}
pub fn single() -> Self {
Self::with(10, 10)
}
pub fn query() -> Self {
Self::with(20, 20)
}
pub fn batch() -> Self {
Self::with(10, 20)
}
pub fn next_write(&mut self) {
self.this_part = 0;
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Allow,
Deny(String),
}
pub fn authorize(
ruleset: &Ruleset,
request: &Request,
resource: Option<&Document>,
documents: &dyn Documents,
budget: &mut Budget,
) -> Decision {
authorize_traced(
ruleset,
request,
resource,
documents,
budget,
&mut Trace::new(),
)
}
pub fn authorize_traced(
ruleset: &Ruleset,
request: &Request,
resource: Option<&Document>,
documents: &dyn Documents,
budget: &mut Budget,
trace: &mut Trace,
) -> Decision {
let Some(service) = ruleset.service("cloud.firestore") else {
return unmatched(request, resource.map_or(Val::Null, value::resource), trace);
};
if request.method == Method::List {
return list::authorize(ruleset, service, request, documents, budget, trace);
}
let mut parts: Vec<Part<'_>> = request.path.iter().map(|s| Part::Segment(s)).collect();
if request.auto_id {
parts.push(Part::UnknownId);
}
let coverings = crate::paths::covering_parts(ruleset.version, service, &parts);
let other = match request.method {
Method::Create if request.upsert => Some(Method::Update),
Method::Update if request.upsert => Some(Method::Create),
_ => None,
};
let applying = statements(&coverings, request.method);
let others = other.map(|other| (other, statements(&coverings, other)));
if applying.is_empty() && others.as_ref().is_none_or(|(_, s)| s.is_empty()) {
return unmatched(request, resource.map_or(Val::Null, value::resource), trace);
}
let context_for = |method| Context {
ruleset,
service,
request: request_value(request, method),
resource: resource.map_or(Val::Null, value::resource),
target: &request.path,
documents,
};
let context = context_for(request.method);
let Some((other, others)) = others else {
return match context.decide(&applying, request.method, budget, trace) {
Ok(()) => Decision::Allow,
Err(entries) => denied(&entries),
};
};
let other_context = context_for(other);
if other == Method::Create {
other_context.each(&others, other, &mut budget.clone(), trace);
}
let decided = context.decide(&applying, request.method, budget, trace);
let Err(entries) = decided else {
if other == Method::Update {
other_context.each(&others, other, &mut budget.clone(), trace);
}
return Decision::Allow;
};
let listed = match other {
Method::Create => {
other_context.listed(&others, other, &mut budget.clone(), &mut Trace::new())
}
_ => other_context.listed(&others, other, &mut budget.clone(), trace),
};
let entries: Vec<String> = match other {
Method::Create => listed.into_iter().chain(entries).collect(),
_ => entries.into_iter().chain(listed).collect(),
};
denied(&entries)
}
fn no_match() -> Decision {
Decision::Deny("No matching allow statements".to_owned())
}
fn unmatched(request: &Request, resource: Val, trace: &mut Trace) -> Decision {
if trace.evaluations.is_some() {
trace.evaluating(
request.method,
&request_value(request, request.method),
&resource,
);
}
no_match()
}
fn denied(entries: &[String]) -> Decision {
Decision::Deny(format!("\n{}", entries.join(", ")))
}
fn statements<'c, 'a>(
coverings: &'c [Covering<'a>],
method: Method,
) -> Vec<(&'c ast::Allow, &'c Covering<'a>)> {
let method = method.statement();
let mut statements = Vec::new();
for covering in coverings {
for allow in &covering.target().body.allows {
if allow
.methods
.iter()
.any(|m| m.method.is_some_and(|m| m.covers(method)))
{
statements.push((allow, covering));
}
}
}
statements.sort_by_key(|(allow, _)| allow.span.start);
statements
}
fn verdict(result: &Result<bool, String>) -> Verdict {
match result {
Ok(true) => Verdict::Allow,
Ok(false) => Verdict::False,
Err(_) => Verdict::Error,
}
}
fn entry(text: &str, method: Method, line: u32) -> String {
format!("{text} for '{}' @ L{line}", method.name())
}
struct Context<'a> {
ruleset: &'a Ruleset,
service: &'a ast::Service,
request: Val,
resource: Val,
target: &'a [String],
documents: &'a dyn Documents,
}
impl Context<'_> {
fn decide(
&self,
statements: &[(&ast::Allow, &Covering<'_>)],
method: Method,
budget: &mut Budget,
trace: &mut Trace,
) -> Result<(), Vec<String>> {
let (entries, allowed) = self.each(statements, method, budget, trace);
if allowed { Ok(()) } else { Err(entries) }
}
fn listed(
&self,
statements: &[(&ast::Allow, &Covering<'_>)],
method: Method,
budget: &mut Budget,
trace: &mut Trace,
) -> Vec<String> {
let mut entries = Vec::with_capacity(statements.len());
let mut marks = Vec::with_capacity(statements.len());
let at = trace.evaluating(method, &self.request, &self.resource);
for (allow, covering) in statements {
marks.push(trace.mark());
let (result, line) = match &allow.condition {
Some(condition) => (
self.evaluate(covering, condition, budget, trace),
condition.span.line,
),
None => (Ok(true), allow.span.line),
};
trace.verdict(at, line, verdict(&result));
let text = result.err();
entries.push(entry(text.as_deref().unwrap_or("false"), method, line));
}
trace.repeat(&marks);
entries
}
fn each(
&self,
statements: &[(&ast::Allow, &Covering<'_>)],
method: Method,
budget: &mut Budget,
trace: &mut Trace,
) -> (Vec<String>, bool) {
let mut entries = Vec::with_capacity(statements.len());
let mut marks = Vec::with_capacity(statements.len());
let mut allowed: Vec<*const ast::Match> = Vec::new();
let at = trace.evaluating(method, &self.request, &self.resource);
for (allow, covering) in statements {
let target = std::ptr::from_ref(covering.target());
if allowed.contains(&target) {
continue;
}
marks.push(trace.mark());
let Some(condition) = &allow.condition else {
trace.verdict(at, allow.span.line, Verdict::Allow);
allowed.push(target);
continue;
};
let result = self.evaluate(covering, condition, budget, trace);
trace.verdict(at, condition.span.line, verdict(&result));
let text = match result {
Ok(true) => {
allowed.push(target);
continue;
}
Ok(false) => "false".to_owned(),
Err(e) => e,
};
entries.push(entry(&text, method, condition.span.line));
}
trace.repeat(&marks);
(entries, !allowed.is_empty())
}
fn evaluate(
&self,
covering: &Covering<'_>,
condition: &ast::Expr,
budget: &mut Budget,
trace: &mut Trace,
) -> Result<bool, String> {
let mut interp = Interp::new(
levels(self.ruleset, self.service, covering),
self.request.clone(),
self.resource.clone(),
self.target,
self.documents,
budget,
trace,
&self.ruleset.parens,
);
let frame = Frame::condition(covering.chain.len() + 1);
interp
.eval(condition, frame.scope())
.and_then(interp::boolean)
.map_err(|e| {
if e.message.is_empty() {
format!(
"evaluation error at L{}:{}",
condition.span.line, condition.span.column
)
} else {
e.message
}
})
}
}
fn levels<'a>(
ruleset: &'a Ruleset,
service: &'a ast::Service,
covering: &'a Covering<'a>,
) -> Vec<Level<'a>> {
let mut levels = vec![
Level {
functions: &ruleset.functions,
variables: Vec::new(),
},
Level {
functions: &service.body.functions,
variables: Vec::new(),
},
];
let mut bindings = covering.bindings.iter();
for m in &covering.chain {
let count = m
.path
.iter()
.filter(|s| s.kind.variable().is_some())
.count();
let variables = bindings
.by_ref()
.take(count)
.filter_map(|(name, binding)| {
let value = match binding {
Binding::Segment(s) => Val::str(s),
Binding::Path(p) => Val::path(p.clone()),
Binding::Unbound => return None,
};
Some((name.as_str(), value))
})
.collect();
levels.push(Level {
functions: &m.body.functions,
variables,
});
}
levels
}
fn request_value(request: &Request, method: Method) -> Val {
let strings = |list: &Option<Vec<String>>| {
list.as_ref().map_or(Val::Null, |items| {
Val::list(items.iter().map(|s| Val::str(s)).collect())
})
};
let mut map = IndexMap::new();
map.insert(
"time".to_owned(),
Val::from(&Value::Timestamp(request.time)),
);
map.insert("auth".to_owned(), auth_value(request));
map.insert(
"inTransaction".to_owned(),
Val::Bool(request.in_transaction),
);
let path = Val::path(request.path.clone());
match method {
Method::Get => {
map.insert("path".to_owned(), path);
map.insert("fields".to_owned(), Val::Null);
}
Method::List => {
let query = request.query.clone().unwrap_or_default();
map.insert("fields".to_owned(), strings(&query.fields));
map.insert("query".to_owned(), query_value(request, &query));
map.insert("path".to_owned(), path);
}
Method::Create | Method::Update | Method::Delete => {
map.insert("path".to_owned(), path);
map.insert("transforms".to_owned(), strings(&request.transforms));
map.insert("writeFields".to_owned(), strings(&request.write_fields));
map.insert("readFields".to_owned(), Val::Null);
if method == Method::Create && !request.upsert {
map.insert("autoId".to_owned(), Val::Bool(request.auto_id));
}
let resource = match &request.resource {
None => Val::Null,
Some(doc) if request.auto_id => match value::resource(doc) {
Val::Map(map) => {
let data = map.get("data").cloned().unwrap_or(Val::Null);
Val::Map(Rc::new(IndexMap::from([("data".to_owned(), data)])))
}
other => other,
},
Some(doc) => value::resource(doc),
};
map.insert("resource".to_owned(), resource);
}
}
map.insert("method".to_owned(), Val::str(method.name()));
Val::Map(Rc::new(map))
}
fn query_value(request: &Request, query: &Query) -> Val {
let all_descendants = query.group.is_some() && !query.children;
let (kind, parent) = match &query.group {
None => match request.path.split_last() {
Some((id, parent)) => (Val::str(id), parent),
None => (Val::Null, &[][..]),
},
Some(id) if id.is_empty() => (Val::Null, request.path.as_slice()),
Some(id) => (Val::str(id), request.path.as_slice()),
};
let parent = if parent.len() > 3 {
Val::path(parent.to_vec())
} else {
Val::Null
};
let order_by = query
.order_by
.iter()
.map(|(field, direction)| (field.clone(), Val::str(direction)))
.collect();
let mut q = IndexMap::new();
q.insert("distinct".to_owned(), Val::Bool(false));
q.insert("selectOnlyKeys".to_owned(), Val::Bool(false));
q.insert("groupBy".to_owned(), Val::Map(Rc::default()));
q.insert("kind".to_owned(), kind);
let parent_key = if all_descendants {
"ancestor"
} else {
"parent"
};
q.insert(parent_key.to_owned(), parent);
q.insert("orderBy".to_owned(), Val::Map(Rc::new(order_by)));
q.insert("limit".to_owned(), query.limit.map_or(Val::Null, Val::Int));
q.insert("offset".to_owned(), Val::Int(query.offset.unwrap_or(0)));
q.insert("allDescendants".to_owned(), Val::Bool(all_descendants));
Val::Map(Rc::new(q))
}
fn auth_value(request: &Request) -> Val {
match &request.auth {
None => Val::Null,
Some(auth) => {
let mut a = IndexMap::new();
a.insert(
"uid".to_owned(),
auth.uid.as_deref().map_or(Val::Null, Val::str),
);
a.insert(
"token".to_owned(),
Val::from(&Value::Map(auth.token.clone())),
);
Val::Map(Rc::new(a))
}
}
}