From bd18466c00d089bc3aef6a09a3fd1927ab5616d8 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Thu, 23 May 2024 15:32:25 +0200 Subject: [PATCH 01/27] add a new Array type this tries to enforce that all array elements are of the same type, but this is not very strict at the moment, it does not look at lower levels of composite types. **breaking change**: in the Datalog language, sets will now be delimited by '{' and '}' instead of '[' and ]'. Arrays are now delimited by '[' and ']' --- biscuit-auth/src/datalog/mod.rs | 2 + biscuit-auth/src/datalog/symbol.rs | 9 ++++- biscuit-auth/src/format/convert.rs | 51 +++++++++++++++++++++++ biscuit-auth/src/format/schema.proto | 7 ++++ biscuit-auth/src/format/schema.rs | 9 ++++- biscuit-auth/src/token/builder.rs | 19 ++++++++- biscuit-auth/src/token/mod.rs | 4 +- biscuit-auth/tests/macros.rs | 10 ++--- biscuit-parser/src/builder.rs | 14 ++++++- biscuit-parser/src/parser.rs | 60 +++++++++++++++++++++++++--- 10 files changed, 168 insertions(+), 17 deletions(-) diff --git a/biscuit-auth/src/datalog/mod.rs b/biscuit-auth/src/datalog/mod.rs index 982ad78e..3d7829a7 100644 --- a/biscuit-auth/src/datalog/mod.rs +++ b/biscuit-auth/src/datalog/mod.rs @@ -26,6 +26,7 @@ pub enum Term { Bool(bool), Set(BTreeSet), Null, + Array(Vec), } impl From<&Term> for Term { @@ -39,6 +40,7 @@ impl From<&Term> for Term { Term::Bool(ref b) => Term::Bool(*b), Term::Set(ref s) => Term::Set(s.clone()), Term::Null => Term::Null, + Term::Array(ref a) => Term::Array(a.clone()), } } } diff --git a/biscuit-auth/src/datalog/symbol.rs b/biscuit-auth/src/datalog/symbol.rs index ea1d10a5..959c920d 100644 --- a/biscuit-auth/src/datalog/symbol.rs +++ b/biscuit-auth/src/datalog/symbol.rs @@ -202,9 +202,16 @@ impl SymbolTable { .iter() .map(|term| self.print_term(term)) .collect::>(); - format!("[{}]", terms.join(", ")) + format!("{{{}}}", terms.join(", ")) } Term::Null => "null".to_string(), + Term::Array(a) => { + let terms = a + .iter() + .map(|term| self.print_term(term)) + .collect::>(); + format!("[{}]", terms.join(", ")) + } } } pub fn print_fact(&self, f: &Fact) -> String { diff --git a/biscuit-auth/src/format/convert.rs b/biscuit-auth/src/format/convert.rs index 3d62c811..e342ff42 100644 --- a/biscuit-auth/src/format/convert.rs +++ b/biscuit-auth/src/format/convert.rs @@ -523,6 +523,11 @@ pub mod v2 { Term::Null => schema::TermV2 { content: Some(Content::Null(Empty {})), }, + Term::Array(a) => schema::TermV2 { + content: Some(Content::Array(schema::Array { + array: a.iter().map(token_term_to_proto_id).collect(), + })), + }, } } @@ -561,6 +566,7 @@ pub mod v2 { )); } Some(Content::Null(_)) => 8, + Some(Content::Array(_)) => 9, None => { return Err(error::Format::DeserializationError( "deserialization error: ID content enum is empty".to_string(), @@ -585,6 +591,51 @@ pub mod v2 { Ok(Term::Set(set)) } Some(Content::Null(_)) => Ok(Term::Null), + Some(Content::Array(a)) => { + let mut kind: Option = None; + let mut array = Vec::new(); + + for term in a.array.iter() { + // FIXME: this is not a recursive type check: an array of arrays or array + // of sets could have different types one level down + let index = match term.content { + Some(Content::Variable(_)) => { + return Err(error::Format::DeserializationError( + "deserialization error: arrays cannot contain variables" + .to_string(), + )); + } + Some(Content::Integer(_)) => 2, + Some(Content::String(_)) => 3, + Some(Content::Date(_)) => 4, + Some(Content::Bytes(_)) => 5, + Some(Content::Bool(_)) => 6, + Some(Content::Set(_)) => 7, + Some(Content::Null(_)) => 8, + Some(Content::Array(_)) => 9, + None => { + return Err(error::Format::DeserializationError( + "deserialization error: ID content enum is empty".to_string(), + )) + } + }; + + if let Some(k) = kind.as_ref() { + if *k != index { + return Err(error::Format::DeserializationError( + "deserialization error: array elements must have the same type" + .to_string(), + )); + } + } else { + kind = Some(index); + } + + array.push(proto_id_to_token_term(term)?); + } + + Ok(Term::Array(array)) + } } } diff --git a/biscuit-auth/src/format/schema.proto b/biscuit-auth/src/format/schema.proto index 6bb4fcd8..a2b7a398 100644 --- a/biscuit-auth/src/format/schema.proto +++ b/biscuit-auth/src/format/schema.proto @@ -99,6 +99,7 @@ message TermV2 { bool bool = 6; TermSet set = 7; Empty null = 8; + Array array = 9; } } @@ -106,6 +107,12 @@ message TermSet { repeated TermV2 set = 1; } + + +message Array { + repeated TermV2 array = 1; +} + message ExpressionV2 { repeated Op ops = 1; } diff --git a/biscuit-auth/src/format/schema.rs b/biscuit-auth/src/format/schema.rs index 3d581c4e..4d417e83 100644 --- a/biscuit-auth/src/format/schema.rs +++ b/biscuit-auth/src/format/schema.rs @@ -139,7 +139,7 @@ pub struct PredicateV2 { } #[derive(Clone, PartialEq, ::prost::Message)] pub struct TermV2 { - #[prost(oneof="term_v2::Content", tags="1, 2, 3, 4, 5, 6, 7, 8")] + #[prost(oneof="term_v2::Content", tags="1, 2, 3, 4, 5, 6, 7, 8, 9")] pub content: ::core::option::Option, } /// Nested message and enum types in `TermV2`. @@ -162,6 +162,8 @@ pub mod term_v2 { Set(super::TermSet), #[prost(message, tag="8")] Null(super::Empty), + #[prost(message, tag="9")] + Array(super::Array), } } #[derive(Clone, PartialEq, ::prost::Message)] @@ -170,6 +172,11 @@ pub struct TermSet { pub set: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] +pub struct Array { + #[prost(message, repeated, tag="1")] + pub array: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] pub struct ExpressionV2 { #[prost(message, repeated, tag="1")] pub ops: ::prost::alloc::vec::Vec, diff --git a/biscuit-auth/src/token/builder.rs b/biscuit-auth/src/token/builder.rs index 88779063..8121410c 100644 --- a/biscuit-auth/src/token/builder.rs +++ b/biscuit-auth/src/token/builder.rs @@ -430,6 +430,7 @@ pub enum Term { Set(BTreeSet), Parameter(String), Null, + Array(Vec), } impl Convert for Term { @@ -446,6 +447,7 @@ impl Convert for Term { // The error is caught in the `add_xxx` functions, so this should // not happen™ Term::Parameter(s) => panic!("Remaining parameter {}", &s), + Term::Array(a) => datalog::Term::Array(a.iter().map(|i| i.convert(symbols)).collect()), } } @@ -463,6 +465,11 @@ impl Convert for Term { .collect::, error::Format>>()?, ), datalog::Term::Null => Term::Null, + datalog::Term::Array(a) => Term::Array( + a.iter() + .map(|i| Term::convert_from(i, symbols)) + .collect::, error::Format>>()?, + ), }) } } @@ -479,6 +486,7 @@ impl From<&Term> for Term { Term::Set(ref s) => Term::Set(s.clone()), Term::Parameter(ref p) => Term::Parameter(p.clone()), Term::Null => Term::Null, + Term::Array(ref a) => Term::Array(a.clone()), } } } @@ -497,6 +505,9 @@ impl From for Term { } biscuit_parser::builder::Term::Null => Term::Null, biscuit_parser::builder::Term::Parameter(ref p) => Term::Parameter(p.clone()), + biscuit_parser::builder::Term::Array(a) => { + Term::Array(a.into_iter().map(|t| t.into()).collect()) + } } } } @@ -534,12 +545,16 @@ impl fmt::Display for Term { } Term::Set(s) => { let terms = s.iter().map(|term| term.to_string()).collect::>(); - write!(f, "[{}]", terms.join(", ")) + write!(f, "{{{}}}", terms.join(", ")) } Term::Parameter(s) => { write!(f, "{{{}}}", s) } Term::Null => write!(f, "null"), + Term::Array(a) => { + let terms = a.iter().map(|term| term.to_string()).collect::>(); + write!(f, "[{}]", terms.join(", ")) + } } } } @@ -2357,7 +2372,7 @@ mod tests { rule.set("p5", term_set).unwrap(); let s = rule.to_string(); - assert_eq!(s, "fact($var1, \"hello\", [0]) <- f1($var1, $var3), f2(\"hello\", $var3, 1), $var3.starts_with(\"hello\")"); + assert_eq!(s, "fact($var1, \"hello\", {0}) <- f1($var1, $var3), f2(\"hello\", $var3, 1), $var3.starts_with(\"hello\")"); } #[test] diff --git a/biscuit-auth/src/token/mod.rs b/biscuit-auth/src/token/mod.rs index b612870e..cd8347d6 100644 --- a/biscuit-auth/src/token/mod.rs +++ b/biscuit-auth/src/token/mod.rs @@ -1299,14 +1299,14 @@ mod tests { let mut block2 = BlockBuilder::new(); block2 - .add_rule("has_bytes($0) <- bytes($0), [ hex:00000000, hex:0102AB ].contains($0)") + .add_rule("has_bytes($0) <- bytes($0), { hex:00000000, hex:0102AB }.contains($0)") .unwrap(); let keypair2 = KeyPair::new_with_rng(&mut rng); let biscuit2 = biscuit1.append_with_keypair(&keypair2, block2).unwrap(); let mut authorizer = biscuit2.authorizer().unwrap(); authorizer - .add_check("check if bytes($0), [ hex:00000000, hex:0102AB ].contains($0)") + .add_check("check if bytes($0), { hex:00000000, hex:0102AB }.contains($0)") .unwrap(); authorizer.allow().unwrap(); diff --git a/biscuit-auth/tests/macros.rs b/biscuit-auth/tests/macros.rs index 49244655..b2d8affa 100644 --- a/biscuit-auth/tests/macros.rs +++ b/biscuit-auth/tests/macros.rs @@ -22,7 +22,7 @@ fn block_macro() { assert_eq!( b.to_string(), - r#"fact("test", hex:aabbcc, [true], "my_value", [0]); + r#"fact("test", hex:aabbcc, [true], "my_value", {0}); appended(true); rule($0, true) <- fact($0, $1, $2, "my_value"); check if "my_value".starts_with("my"); @@ -165,7 +165,7 @@ fn rule_macro() { assert_eq!( r.to_string(), - r#"rule($0, true) <- fact($0, $1, $2, "my_value", [0]) trusting ed25519/6e9e6d5a75cf0c0e87ec1256b4dfed0ca3ba452912d213fcc70f8516583db9db"#, + r#"rule($0, true) <- fact($0, $1, $2, "my_value", {0}) trusting ed25519/6e9e6d5a75cf0c0e87ec1256b4dfed0ca3ba452912d213fcc70f8516583db9db"#, ); } @@ -175,7 +175,7 @@ fn fact_macro() { term_set.insert(builder::int(0i64)); let f = fact!(r#"fact({my_key}, {term_set})"#, my_key = "my_value",); - assert_eq!(f.to_string(), r#"fact("my_value", [0])"#,); + assert_eq!(f.to_string(), r#"fact("my_value", {0})"#,); } #[test] @@ -194,7 +194,7 @@ fn check_macro() { assert_eq!( c.to_string(), - r#"check if fact("my_value", [0]) trusting ed25519/6e9e6d5a75cf0c0e87ec1256b4dfed0ca3ba452912d213fcc70f8516583db9db"#, + r#"check if fact("my_value", {0}) trusting ed25519/6e9e6d5a75cf0c0e87ec1256b4dfed0ca3ba452912d213fcc70f8516583db9db"#, ); } @@ -214,6 +214,6 @@ fn policy_macro() { assert_eq!( p.to_string(), - r#"allow if fact("my_value", [0]) trusting ed25519/6e9e6d5a75cf0c0e87ec1256b4dfed0ca3ba452912d213fcc70f8516583db9db"#, + r#"allow if fact("my_value", {0}) trusting ed25519/6e9e6d5a75cf0c0e87ec1256b4dfed0ca3ba452912d213fcc70f8516583db9db"#, ); } diff --git a/biscuit-parser/src/builder.rs b/biscuit-parser/src/builder.rs index 3d4782e3..1e78172b 100644 --- a/biscuit-parser/src/builder.rs +++ b/biscuit-parser/src/builder.rs @@ -19,6 +19,7 @@ pub enum Term { Set(BTreeSet), Parameter(String), Null, + Array(Vec), } impl From<&Term> for Term { @@ -33,6 +34,7 @@ impl From<&Term> for Term { Term::Set(ref s) => Term::Set(s.clone()), Term::Parameter(ref p) => Term::Parameter(p.clone()), Term::Null => Term::Null, + Term::Array(ref a) => Term::Array(a.clone()), } } } @@ -61,7 +63,12 @@ impl ToTokens for Term { }} } Term::Null => quote! { ::biscuit_auth::builder::Term::Null }, - + Term::Array(v) => { + quote! {{ + use std::iter::FromIterator; + ::biscuit_auth::builder::Term::Array(::std::vec::Vec::from_iter(<[::biscuit_auth::builder::Term]>::into_vec(Box::new([ #(#v),*])))) + }} + } }) } } @@ -577,6 +584,11 @@ pub fn null() -> Term { Term::Null } +/// creates an array +pub fn array(a: Vec) -> Term { + Term::Array(a) +} + /// creates a parameter pub fn parameter(p: &str) -> Term { Term::Parameter(p.to_string()) diff --git a/biscuit-parser/src/parser.rs b/biscuit-parser/src/parser.rs index bf06ac9f..3dfaa2b0 100644 --- a/biscuit-parser/src/parser.rs +++ b/biscuit-parser/src/parser.rs @@ -767,8 +767,7 @@ fn null(i: &str) -> IResult<&str, builder::Term, Error> { } fn set(i: &str) -> IResult<&str, builder::Term, Error> { - //println!("set:\t{}", i); - let (i, _) = preceded(space0, char('['))(i)?; + let (i, _) = preceded(space0, char('{'))(i)?; let (i, mut list) = cut(separated_list0(preceded(space0, char(',')), term_in_set))(i)?; let mut set = BTreeSet::new(); @@ -797,6 +796,7 @@ fn set(i: &str) -> IResult<&str, builder::Term, Error> { } builder::Term::Parameter(_) => 7, builder::Term::Null => 8, + builder::Term::Array(_) => 9, }; if let Some(k) = kind { @@ -814,16 +814,64 @@ fn set(i: &str) -> IResult<&str, builder::Term, Error> { set.insert(term); } - let (i, _) = preceded(space0, char(']'))(i)?; + let (i, _) = preceded(space0, char('}'))(i)?; Ok((i, builder::set(set))) } +fn array(i: &str) -> IResult<&str, builder::Term, Error> { + //println!("set:\t{}", i); + let (i, _) = preceded(space0, char('['))(i)?; + let (i, mut list) = cut(separated_list0(preceded(space0, char(',')), term_in_set))(i)?; + + let mut array = Vec::new(); + + let mut kind: Option = None; + for term in list.drain(..) { + let index = match term { + builder::Term::Variable(_) => { + return Err(nom::Err::Failure(Error { + input: i, + code: ErrorKind::Fail, + message: Some("variables are not permitted in arrays".to_string()), + })) + } + builder::Term::Integer(_) => 2, + builder::Term::Str(_) => 3, + builder::Term::Date(_) => 4, + builder::Term::Bytes(_) => 5, + builder::Term::Bool(_) => 6, + builder::Term::Set(_) => 7, + builder::Term::Parameter(_) => 8, + builder::Term::Null => 9, + builder::Term::Array(_) => 10, + }; + + if let Some(k) = kind { + if k != index { + return Err(nom::Err::Failure(Error { + input: i, + code: ErrorKind::Fail, + message: Some("array elements must have the same type".to_string()), + })); + } + } else { + kind = Some(index); + } + + array.push(term); + } + + let (i, _) = preceded(space0, char(']'))(i)?; + + Ok((i, builder::array(array))) +} + fn term(i: &str) -> IResult<&str, builder::Term, Error> { preceded( space0, alt(( - parameter, string, date, variable, integer, bytes, boolean, null, set, + parameter, string, date, variable, integer, bytes, boolean, null, set, array, )), )(i) } @@ -832,7 +880,9 @@ fn term_in_fact(i: &str) -> IResult<&str, builder::Term, Error> { preceded( space0, error( - alt((parameter, string, date, integer, bytes, boolean, null, set)), + alt(( + parameter, string, date, integer, bytes, boolean, null, set, array, + )), |input| match input.chars().next() { None | Some(',') | Some(')') => "missing term".to_string(), Some('$') => "variables are not allowed in facts".to_string(), From 36fe57119655326e3a13716b41037174f45596a3 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Thu, 23 May 2024 15:59:08 +0200 Subject: [PATCH 02/27] add operations on arrays --- biscuit-auth/src/datalog/expression.rs | 119 +++++++++++++++++++++++++ biscuit-auth/src/datalog/mod.rs | 1 + 2 files changed, 120 insertions(+) diff --git a/biscuit-auth/src/datalog/expression.rs b/biscuit-auth/src/datalog/expression.rs index f5fc6ed3..b69da6b5 100644 --- a/biscuit-auth/src/datalog/expression.rs +++ b/biscuit-auth/src/datalog/expression.rs @@ -40,6 +40,7 @@ impl Unary { .ok_or(error::Expression::UnknownSymbol(i)), (Unary::Length, Term::Bytes(s)) => Ok(Term::Integer(s.len() as i64)), (Unary::Length, Term::Set(s)) => Ok(Term::Integer(s.len() as i64)), + (Unary::Length, Term::Array(a)) => Ok(Term::Integer(a.len() as i64)), _ => { //println!("unexpected value type on the stack"); Err(error::Expression::InvalidType) @@ -216,6 +217,15 @@ impl Binary { (Binary::NotEqual, Term::Null, _) => Ok(Term::Bool(true)), (Binary::NotEqual, _, Term::Null) => Ok(Term::Bool(true)), + // array + (Binary::Equal, Term::Array(i), Term::Array(j)) => Ok(Term::Bool(i == j)), + (Binary::NotEqual, Term::Array(i), Term::Array(j)) => Ok(Term::Bool(i != j)), + (Binary::Contains, Term::Array(i), j) => { + Ok(Term::Bool(i.iter().any(|elem| elem == &j))) + } + (Binary::Prefix, Term::Array(i), Term::Array(j)) => Ok(Term::Bool(i.starts_with(&j))), + (Binary::Suffix, Term::Array(i), Term::Array(j)) => Ok(Term::Bool(i.ends_with(&j))), + _ => { //println!("unexpected value type on the stack"); Err(error::Expression::InvalidType) @@ -559,4 +569,113 @@ mod tests { let res = e.evaluate(&values, &mut tmp_symbols); assert_eq!(res, Ok(Term::Bool(true))); } + + #[test] + fn array() { + let symbols = SymbolTable::new(); + let mut tmp_symbols = TemporarySymbolTable::new(&symbols); + let ops = vec![ + Op::Value(Term::Array(vec![Term::Integer(0), Term::Integer(1)])), + Op::Value(Term::Array(vec![Term::Integer(0), Term::Integer(1)])), + Op::Binary(Binary::Equal), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(true))); + + let ops = vec![ + Op::Value(Term::Array(vec![Term::Integer(0), Term::Integer(1)])), + Op::Value(Term::Array(vec![Term::Integer(0)])), + Op::Binary(Binary::Equal), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(false))); + + let ops = vec![ + Op::Value(Term::Array(vec![Term::Integer(0), Term::Integer(1)])), + Op::Value(Term::Integer(1)), + Op::Binary(Binary::Contains), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(true))); + + let ops = vec![ + Op::Value(Term::Array(vec![Term::Integer(0), Term::Integer(1)])), + Op::Value(Term::Integer(2)), + Op::Binary(Binary::Contains), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(false))); + + let ops = vec![ + Op::Value(Term::Array(vec![ + Term::Integer(0), + Term::Integer(1), + Term::Integer(2), + ])), + Op::Value(Term::Array(vec![Term::Integer(0), Term::Integer(1)])), + Op::Binary(Binary::Prefix), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(true))); + + let ops = vec![ + Op::Value(Term::Array(vec![ + Term::Integer(0), + Term::Integer(1), + Term::Integer(2), + ])), + Op::Value(Term::Array(vec![Term::Integer(2), Term::Integer(1)])), + Op::Binary(Binary::Prefix), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(false))); + + let ops = vec![ + Op::Value(Term::Array(vec![ + Term::Integer(0), + Term::Integer(1), + Term::Integer(2), + ])), + Op::Value(Term::Array(vec![Term::Integer(1), Term::Integer(2)])), + Op::Binary(Binary::Suffix), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(true))); + + let ops = vec![ + Op::Value(Term::Array(vec![ + Term::Integer(0), + Term::Integer(1), + Term::Integer(2), + ])), + Op::Value(Term::Array(vec![Term::Integer(0), Term::Integer(2)])), + Op::Binary(Binary::Suffix), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(false))); + } } diff --git a/biscuit-auth/src/datalog/mod.rs b/biscuit-auth/src/datalog/mod.rs index 3d7829a7..81680e21 100644 --- a/biscuit-auth/src/datalog/mod.rs +++ b/biscuit-auth/src/datalog/mod.rs @@ -566,6 +566,7 @@ pub fn match_preds(rule_pred: &Predicate, fact_pred: &Predicate) -> bool { (Term::Bool(i), Term::Bool(j)) => i == j, (Term::Null, Term::Null) => true, (Term::Set(i), Term::Set(j)) => i == j, + (Term::Array(i), Term::Array(j)) => i == j, _ => false, }) } From 579ea358436c35e078253f710159dfaa6f971692 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Thu, 23 May 2024 16:25:49 +0200 Subject: [PATCH 03/27] fix the parser This introduces a breaking change: now parameter names need to start with a letter --- biscuit-parser/src/parser.rs | 57 ++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/biscuit-parser/src/parser.rs b/biscuit-parser/src/parser.rs index 3dfaa2b0..c10e5715 100644 --- a/biscuit-parser/src/parser.rs +++ b/biscuit-parser/src/parser.rs @@ -3,8 +3,8 @@ use nom::{ branch::alt, bytes::complete::{escaped_transform, tag, tag_no_case, take_until, take_while, take_while1}, character::{ - complete::{char, digit1, multispace0 as space0}, - is_alphanumeric, + complete::{char, digit1, multispace0 as space0, satisfy}, + is_alphabetic, is_alphanumeric, }, combinator::{consumed, cut, eof, map, map_res, opt, recognize, value}, error::{ErrorKind, FromExternalError, ParseError}, @@ -679,6 +679,18 @@ fn name(i: &str) -> IResult<&str, &str, Error> { reduce(take_while1(is_name_char), " ,:(\n;")(i) } +fn parameter_name(i: &str) -> IResult<&str, &str, Error> { + let is_name_char = |c: char| is_alphanumeric(c as u8) || c == '_' || c == ':'; + + reduce( + recognize(preceded( + satisfy(|c: char| is_alphabetic(c as u8)), + take_while1(is_name_char), + )), + " ,:(\n;", + )(i) +} + fn printable(i: &str) -> IResult<&str, &str, Error> { take_while1(|c: char| c != '\\' && c != '"')(i) } @@ -751,7 +763,10 @@ fn variable(i: &str) -> IResult<&str, builder::Term, Error> { } fn parameter(i: &str) -> IResult<&str, builder::Term, Error> { - map(delimited(char('{'), name, char('}')), builder::parameter)(i) + map( + delimited(char('{'), parameter_name, char('}')), + builder::parameter, + )(i) } fn parse_bool(i: &str) -> IResult<&str, bool, Error> { @@ -1236,7 +1251,7 @@ where #[cfg(test)] mod tests { - use crate::builder::{self, Unary}; + use crate::builder::{self, array, int, var, Binary, Op, Unary}; #[test] fn name() { @@ -1476,7 +1491,7 @@ mod tests { let h = [int(1), int(2)].iter().cloned().collect::>(); assert_eq!( - super::expr("[1, 2].contains($0)").map(|(i, o)| (i, o.opcodes())), + super::expr("{1, 2}.contains($0)").map(|(i, o)| (i, o.opcodes())), Ok(( "", vec![ @@ -1488,7 +1503,7 @@ mod tests { ); assert_eq!( - super::expr("![1, 2].contains($0)").map(|(i, o)| (i, o.opcodes())), + super::expr("!{ 1, 2}.contains($0)").map(|(i, o)| (i, o.opcodes())), Ok(( "", vec![ @@ -1553,7 +1568,7 @@ mod tests { .cloned() .collect::>(); assert_eq!( - super::expr("[\"abc\", \"def\"].contains($0)").map(|(i, o)| (i, o.opcodes())), + super::expr("{\"abc\", \"def\"}.contains($0)").map(|(i, o)| (i, o.opcodes())), Ok(( "", vec![ @@ -1565,7 +1580,7 @@ mod tests { ); assert_eq!( - super::expr("![\"abc\", \"def\"].contains($0)").map(|(i, o)| (i, o.opcodes())), + super::expr("!{\"abc\", \"def\"}.contains($0)").map(|(i, o)| (i, o.opcodes())), Ok(( "", vec![ @@ -1582,7 +1597,7 @@ mod tests { .cloned() .collect::>(); assert_eq!( - super::expr("[\"abc\", \"def\"].contains($0)").map(|(i, o)| (i, o.opcodes())), + super::expr("{\"abc\", \"def\"}.contains($0)").map(|(i, o)| (i, o.opcodes())), Ok(( "", vec![ @@ -1594,7 +1609,7 @@ mod tests { ); assert_eq!( - super::expr("![\"abc\", \"def\"].contains($0)").map(|(i, o)| (i, o.opcodes())), + super::expr("!{\"abc\", \"def\"}.contains($0)").map(|(i, o)| (i, o.opcodes())), Ok(( "", vec![ @@ -2254,7 +2269,7 @@ mod tests { use builder::{int, set, Binary, Op}; assert_eq!( - super::expr("[1].intersection([2]).contains(3)").map(|(i, o)| (i, o.opcodes())), + super::expr("{1}.intersection({2}).contains(3)").map(|(i, o)| (i, o.opcodes())), Ok(( "", vec![ @@ -2268,7 +2283,7 @@ mod tests { ); assert_eq!( - super::expr("[1].intersection([2]).union([3]).length()").map(|(i, o)| (i, o.opcodes())), + super::expr("{1}.intersection({2}).union({3}).length()").map(|(i, o)| (i, o.opcodes())), Ok(( "", vec![ @@ -2283,7 +2298,7 @@ mod tests { ); assert_eq!( - super::expr("[1].intersection([2]).length().union([3])").map(|(i, o)| (i, o.opcodes())), + super::expr("{1}.intersection({2}).length().union({3})").map(|(i, o)| (i, o.opcodes())), Ok(( "", vec![ @@ -2297,4 +2312,20 @@ mod tests { )) ); } + + #[test] + fn arrays() { + let h = vec![int(1), int(2)]; + assert_eq!( + super::expr("[1, 2].contains($0)").map(|(i, o)| (i, o.opcodes())), + Ok(( + "", + vec![ + Op::Value(array(h.clone())), + Op::Value(var("0")), + Op::Binary(Binary::Contains), + ], + )) + ); + } } From 366ee5c4d3509272ead7de70b5d34716f2e1feef Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Thu, 23 May 2024 22:05:51 +0200 Subject: [PATCH 04/27] introduce a map type it accepts integers, strings and parameters as keys --- biscuit-auth/src/datalog/mod.rs | 11 +- biscuit-auth/src/datalog/symbol.rs | 18 +++ biscuit-auth/src/format/convert.rs | 44 +++++++ biscuit-auth/src/format/schema.proto | 19 ++- biscuit-auth/src/format/schema.rs | 31 ++++- biscuit-auth/src/token/builder.rs | 166 ++++++++++++++++++++++++--- biscuit-auth/tests/macros.rs | 5 +- biscuit-parser/src/builder.rs | 49 +++++++- biscuit-parser/src/parser.rs | 51 ++++++-- 9 files changed, 360 insertions(+), 34 deletions(-) diff --git a/biscuit-auth/src/datalog/mod.rs b/biscuit-auth/src/datalog/mod.rs index 81680e21..347f37f1 100644 --- a/biscuit-auth/src/datalog/mod.rs +++ b/biscuit-auth/src/datalog/mod.rs @@ -4,7 +4,7 @@ use crate::error::Execution; use crate::time::Instant; use crate::token::{Scope, MIN_SCHEMA_VERSION}; use crate::{builder, error}; -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::convert::AsRef; use std::fmt; use std::time::{Duration, SystemTime, UNIX_EPOCH}; @@ -27,6 +27,13 @@ pub enum Term { Set(BTreeSet), Null, Array(Vec), + Map(BTreeMap), +} + +#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub enum MapKey { + Integer(i64), + Str(SymbolIndex), } impl From<&Term> for Term { @@ -41,6 +48,7 @@ impl From<&Term> for Term { Term::Set(ref s) => Term::Set(s.clone()), Term::Null => Term::Null, Term::Array(ref a) => Term::Array(a.clone()), + Term::Map(m) => Term::Map(m.clone()), } } } @@ -567,6 +575,7 @@ pub fn match_preds(rule_pred: &Predicate, fact_pred: &Predicate) -> bool { (Term::Null, Term::Null) => true, (Term::Set(i), Term::Set(j)) => i == j, (Term::Array(i), Term::Array(j)) => i == j, + (Term::Map(i), Term::Map(j)) => i == j, _ => false, }) } diff --git a/biscuit-auth/src/datalog/symbol.rs b/biscuit-auth/src/datalog/symbol.rs index 959c920d..e9080cf7 100644 --- a/biscuit-auth/src/datalog/symbol.rs +++ b/biscuit-auth/src/datalog/symbol.rs @@ -212,6 +212,24 @@ impl SymbolTable { .collect::>(); format!("[{}]", terms.join(", ")) } + Term::Map(m) => { + let terms = m + .iter() + .map(|(key, term)| match key { + crate::datalog::MapKey::Integer(i) => { + format!("{}: {}", i, self.print_term(term)) + } + crate::datalog::MapKey::Str(s) => { + format!( + "\"{}\": {}", + self.print_symbol_default(*s as u64), + self.print_term(term) + ) + } + }) + .collect::>(); + format!("{{{}}}", terms.join(", ")) + } } } pub fn print_fact(&self, f: &Fact) -> String { diff --git a/biscuit-auth/src/format/convert.rs b/biscuit-auth/src/format/convert.rs index e342ff42..26dd66fd 100644 --- a/biscuit-auth/src/format/convert.rs +++ b/biscuit-auth/src/format/convert.rs @@ -312,8 +312,10 @@ pub mod v2 { use crate::datalog::*; use crate::error; use crate::format::schema::Empty; + use crate::format::schema::MapEntry; use crate::token::Scope; use crate::token::MIN_SCHEMA_VERSION; + use std::collections::BTreeMap; use std::collections::BTreeSet; pub fn token_fact_to_proto_fact(input: &Fact) -> schema::FactV2 { @@ -528,6 +530,27 @@ pub mod v2 { array: a.iter().map(token_term_to_proto_id).collect(), })), }, + Term::Map(m) => schema::TermV2 { + content: Some(Content::Map(schema::Map { + entries: m + .iter() + .map(|(key, term)| { + let key = match key { + MapKey::Integer(i) => schema::MapKey { + content: Some(schema::map_key::Content::Integer(*i)), + }, + MapKey::Str(s) => schema::MapKey { + content: Some(schema::map_key::Content::String(*s)), + }, + }; + schema::MapEntry { + key, + value: token_term_to_proto_id(term), + } + }) + .collect(), + })), + }, } } @@ -567,6 +590,7 @@ pub mod v2 { } Some(Content::Null(_)) => 8, Some(Content::Array(_)) => 9, + Some(Content::Map(_)) => 10, None => { return Err(error::Format::DeserializationError( "deserialization error: ID content enum is empty".to_string(), @@ -613,6 +637,7 @@ pub mod v2 { Some(Content::Set(_)) => 7, Some(Content::Null(_)) => 8, Some(Content::Array(_)) => 9, + Some(Content::Map(_)) => 10, None => { return Err(error::Format::DeserializationError( "deserialization error: ID content enum is empty".to_string(), @@ -636,6 +661,25 @@ pub mod v2 { Ok(Term::Array(array)) } + Some(Content::Map(m)) => { + let mut map = BTreeMap::new(); + + for MapEntry { key, value } in m.entries.iter() { + let key = match key.content { + Some(schema::map_key::Content::Integer(i)) => MapKey::Integer(i), + Some(schema::map_key::Content::String(s)) => MapKey::Str(s), + None => { + return Err(error::Format::DeserializationError( + "deserialization error: ID content enum is empty".to_string(), + )) + } + }; + + map.insert(key, proto_id_to_token_term(&value)?); + } + + Ok(Term::Map(map)) + } } } diff --git a/biscuit-auth/src/format/schema.proto b/biscuit-auth/src/format/schema.proto index a2b7a398..2a410bf6 100644 --- a/biscuit-auth/src/format/schema.proto +++ b/biscuit-auth/src/format/schema.proto @@ -100,6 +100,7 @@ message TermV2 { TermSet set = 7; Empty null = 8; Array array = 9; + Map map = 10; } } @@ -107,12 +108,26 @@ message TermSet { repeated TermV2 set = 1; } - - message Array { repeated TermV2 array = 1; } +message Map { + repeated MapEntry entries = 1; +} + +message MapEntry { + required MapKey key = 1; + required TermV2 value = 2; +} + +message MapKey { + oneof Content { + int64 integer = 1; + uint64 string = 2; + } +} + message ExpressionV2 { repeated Op ops = 1; } diff --git a/biscuit-auth/src/format/schema.rs b/biscuit-auth/src/format/schema.rs index 4d417e83..ab46ade2 100644 --- a/biscuit-auth/src/format/schema.rs +++ b/biscuit-auth/src/format/schema.rs @@ -139,7 +139,7 @@ pub struct PredicateV2 { } #[derive(Clone, PartialEq, ::prost::Message)] pub struct TermV2 { - #[prost(oneof="term_v2::Content", tags="1, 2, 3, 4, 5, 6, 7, 8, 9")] + #[prost(oneof="term_v2::Content", tags="1, 2, 3, 4, 5, 6, 7, 8, 9, 10")] pub content: ::core::option::Option, } /// Nested message and enum types in `TermV2`. @@ -164,6 +164,8 @@ pub mod term_v2 { Null(super::Empty), #[prost(message, tag="9")] Array(super::Array), + #[prost(message, tag="10")] + Map(super::Map), } } #[derive(Clone, PartialEq, ::prost::Message)] @@ -177,6 +179,33 @@ pub struct Array { pub array: ::prost::alloc::vec::Vec, } #[derive(Clone, PartialEq, ::prost::Message)] +pub struct Map { + #[prost(message, repeated, tag="1")] + pub entries: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MapEntry { + #[prost(message, required, tag="1")] + pub key: MapKey, + #[prost(message, required, tag="2")] + pub value: TermV2, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct MapKey { + #[prost(oneof="map_key::Content", tags="1, 2")] + pub content: ::core::option::Option, +} +/// Nested message and enum types in `MapKey`. +pub mod map_key { + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Content { + #[prost(int64, tag="1")] + Integer(i64), + #[prost(uint64, tag="2")] + String(u64), + } +} +#[derive(Clone, PartialEq, ::prost::Message)] pub struct ExpressionV2 { #[prost(message, repeated, tag="1")] pub ops: ::prost::alloc::vec::Vec, diff --git a/biscuit-auth/src/token/builder.rs b/biscuit-auth/src/token/builder.rs index 8121410c..59e44acd 100644 --- a/biscuit-auth/src/token/builder.rs +++ b/biscuit-auth/src/token/builder.rs @@ -7,6 +7,7 @@ use crate::token::builder_ext::BuilderExt; use biscuit_parser::parser::parse_block_source; use nom::Finish; use rand_core::{CryptoRng, RngCore}; +use std::collections::BTreeMap; use std::str::FromStr; use std::{ collections::{BTreeSet, HashMap}, @@ -431,6 +432,93 @@ pub enum Term { Parameter(String), Null, Array(Vec), + Map(BTreeMap), +} + +impl Term { + fn extract_parameters(&self, parameters: &mut HashMap>) { + match self { + Term::Parameter(name) => { + parameters.insert(name.to_string(), None); + } + Term::Set(s) => { + for term in s { + term.extract_parameters(parameters); + } + } + Term::Array(a) => { + for term in a { + term.extract_parameters(parameters); + } + } + Term::Map(m) => { + for (key, term) in m { + if let MapKey::Parameter(name) = key { + parameters.insert(name.to_string(), None); + } + term.extract_parameters(parameters); + } + } + _ => {} + } + } + + fn apply_parameters(self, parameters: &HashMap>) -> Term { + match self { + Term::Parameter(name) => { + if let Some(Some(term)) = parameters.get(&name) { + term.clone() + } else { + Term::Parameter(name) + } + } + Term::Map(m) => Term::Map( + m.into_iter() + .map(|(key, term)| { + println!("will try to apply parameters on {key:?} -> {term:?}"); + ( + match key { + MapKey::Parameter(name) => { + if let Some(Some(key_term)) = parameters.get(&name) { + println!("found key term: {key_term}"); + match key_term { + Term::Integer(i) => MapKey::Integer(*i), + Term::Str(s) => MapKey::Str(s.clone()), + //FIXME: we should return an error + _ => MapKey::Parameter(name), + } + } else { + MapKey::Parameter(name) + } + } + _ => key, + }, + term.apply_parameters(parameters), + ) + }) + .collect(), + ), + Term::Array(array) => Term::Array( + array + .into_iter() + .map(|term| term.apply_parameters(parameters)) + .collect(), + ), + Term::Set(set) => Term::Set( + set.into_iter() + .map(|term| term.apply_parameters(parameters)) + .collect(), + ), + _ => self, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MapKey { + Integer(i64), + Str(String), + Parameter(String), } impl Convert for Term { @@ -448,6 +536,19 @@ impl Convert for Term { // not happen™ Term::Parameter(s) => panic!("Remaining parameter {}", &s), Term::Array(a) => datalog::Term::Array(a.iter().map(|i| i.convert(symbols)).collect()), + Term::Map(m) => datalog::Term::Map( + m.iter() + .map(|(key, term)| { + let key = match key { + MapKey::Integer(i) => datalog::MapKey::Integer(*i), + MapKey::Str(s) => datalog::MapKey::Str(symbols.insert(s)), + MapKey::Parameter(s) => panic!("Remaining parameter {}", &s), + }; + + (key, term.convert(symbols)) + }) + .collect(), + ), } } @@ -470,6 +571,18 @@ impl Convert for Term { .map(|i| Term::convert_from(i, symbols)) .collect::, error::Format>>()?, ), + datalog::Term::Map(m) => Term::Map( + m.iter() + .map(|(key, term)| { + let key = match key { + datalog::MapKey::Integer(i) => Ok(MapKey::Integer(*i)), + datalog::MapKey::Str(s) => symbols.print_symbol(*s).map(MapKey::Str), + }; + + key.and_then(|k| Term::convert_from(term, symbols).map(|term| (k, term))) + }) + .collect::, error::Format>>()?, + ), }) } } @@ -487,6 +600,7 @@ impl From<&Term> for Term { Term::Parameter(ref p) => Term::Parameter(p.clone()), Term::Null => Term::Null, Term::Array(ref a) => Term::Array(a.clone()), + Term::Map(m) => Term::Map(m.clone()), } } } @@ -508,6 +622,22 @@ impl From for Term { biscuit_parser::builder::Term::Array(a) => { Term::Array(a.into_iter().map(|t| t.into()).collect()) } + biscuit_parser::builder::Term::Map(a) => Term::Map( + a.into_iter() + .map(|(key, term)| { + ( + match key { + biscuit_parser::builder::MapKey::Parameter(s) => { + MapKey::Parameter(s) + } + biscuit_parser::builder::MapKey::Integer(i) => MapKey::Integer(i), + biscuit_parser::builder::MapKey::Str(s) => MapKey::Str(s), + }, + term.into(), + ) + }) + .collect(), + ), } } } @@ -555,6 +685,17 @@ impl fmt::Display for Term { let terms = a.iter().map(|term| term.to_string()).collect::>(); write!(f, "[{}]", terms.join(", ")) } + Term::Map(m) => { + let terms = m + .iter() + .map(|(key, term)| match key { + MapKey::Integer(i) => format!("{i}: {}", term.to_string()), + MapKey::Str(s) => format!("\"{s}\": {}", term.to_string()), + MapKey::Parameter(s) => format!("{{{s}}}: {}", term.to_string()), + }) + .collect::>(); + write!(f, "{{{}}}", terms.join(", ")) + } } } } @@ -711,9 +852,7 @@ impl Fact { let terms: Vec = terms.into(); for term in &terms { - if let Term::Parameter(name) = &term { - parameters.insert(name.to_string(), None); - } + term.extract_parameters(&mut parameters); } Fact { predicate: Predicate::new(name, terms), @@ -817,14 +956,7 @@ impl Fact { .predicate .terms .drain(..) - .map(|t| { - if let Term::Parameter(name) = &t { - if let Some(Some(term)) = parameters.get(name) { - return term.clone(); - } - } - t - }) + .map(|t| t.apply_parameters(¶meters)) .collect(); } } @@ -1014,23 +1146,19 @@ impl Rule { let mut parameters = HashMap::new(); let mut scope_parameters = HashMap::new(); for term in &head.terms { - if let Term::Parameter(name) = &term { - parameters.insert(name.to_string(), None); - } + term.extract_parameters(&mut parameters); } for predicate in &body { for term in &predicate.terms { - if let Term::Parameter(name) = &term { - parameters.insert(name.to_string(), None); - } + term.extract_parameters(&mut parameters); } } for expression in &expressions { for op in &expression.ops { - if let Op::Value(Term::Parameter(name)) = &op { - parameters.insert(name.to_string(), None); + if let Op::Value(term) = &op { + term.extract_parameters(&mut parameters); } } } diff --git a/biscuit-auth/tests/macros.rs b/biscuit-auth/tests/macros.rs index b2d8affa..beb61459 100644 --- a/biscuit-auth/tests/macros.rs +++ b/biscuit-auth/tests/macros.rs @@ -10,8 +10,9 @@ fn block_macro() { let mut term_set = BTreeSet::new(); term_set.insert(builder::int(0i64)); let my_key = "my_value"; + let mapkey = "hello"; let mut b = block!( - r#"fact("test", hex:aabbcc, [true], {my_key}, {term_set}); + r#"fact("test", hex:aabbcc, [true], {my_key}, {term_set}, {"a": 1, 2 : "abcd", {mapkey}: 0 }); rule($0, true) <- fact($0, $1, $2, {my_key}); check if {my_key}.starts_with("my"); "#, @@ -22,7 +23,7 @@ fn block_macro() { assert_eq!( b.to_string(), - r#"fact("test", hex:aabbcc, [true], "my_value", {0}); + r#"fact("test", hex:aabbcc, [true], "my_value", {0}, {2: "abcd", "a": 1, "hello": 0}); appended(true); rule($0, true) <- fact($0, $1, $2, "my_value"); check if "my_value".starts_with("my"); diff --git a/biscuit-parser/src/builder.rs b/biscuit-parser/src/builder.rs index 1e78172b..3539d513 100644 --- a/biscuit-parser/src/builder.rs +++ b/biscuit-parser/src/builder.rs @@ -1,6 +1,6 @@ //! helper functions and structure to create tokens and blocks use std::{ - collections::{BTreeSet, HashMap}, + collections::{BTreeMap, BTreeSet, HashMap}, time::{SystemTime, UNIX_EPOCH}, }; @@ -20,6 +20,14 @@ pub enum Term { Parameter(String), Null, Array(Vec), + Map(BTreeMap), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MapKey { + Parameter(String), + Integer(i64), + Str(String), } impl From<&Term> for Term { @@ -35,6 +43,7 @@ impl From<&Term> for Term { Term::Parameter(ref p) => Term::Parameter(p.clone()), Term::Null => Term::Null, Term::Array(ref a) => Term::Array(a.clone()), + Term::Map(ref m) => Term::Map(m.clone()), } } } @@ -66,13 +75,44 @@ impl ToTokens for Term { Term::Array(v) => { quote! {{ use std::iter::FromIterator; - ::biscuit_auth::builder::Term::Array(::std::vec::Vec::from_iter(<[::biscuit_auth::builder::Term]>::into_vec(Box::new([ #(#v),*])))) + ::biscuit_auth::builder::Term::Array(::std::vec::Vec::from_iter(<[::biscuit_auth::builder::Term]>::into_vec( Box::new([ #(#v),*])))) + }} + } + Term::Map(m) => { + let it = m.iter().map(|(key, term)| MapEntry {key, term }); + quote! {{ + use std::iter::FromIterator; + ::biscuit_auth::builder::Term::Map(::std::collections::BTreeMap::from_iter(<[(::biscuit_auth::builder::MapKey,::biscuit_auth::builder::Term)]>::into_vec(Box::new([ #(#it),*])))) }} } }) } } +#[cfg(feature = "datalog-macro")] +struct MapEntry<'a> { + key: &'a MapKey, + term: &'a Term, +} + +#[cfg(feature = "datalog-macro")] +impl<'a> ToTokens for MapEntry<'a> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let term = self.term; + tokens.extend(match self.key { + MapKey::Parameter(p) => { + quote! { (::biscuit_auth::builder::MapKey::Parameter(#p.to_string()) , #term )} + } + MapKey::Integer(i) => { + quote! { (::biscuit_auth::builder::MapKey::Integer(#i) , #term )} + } + MapKey::Str(s) => { + quote! { (::biscuit_auth::builder::MapKey::Str(#s.to_string()) , #term )} + } + }); + } +} + #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub enum Scope { Authority, @@ -589,6 +629,11 @@ pub fn array(a: Vec) -> Term { Term::Array(a) } +/// creates a map +pub fn map(m: BTreeMap) -> Term { + Term::Map(m) +} + /// creates a parameter pub fn parameter(p: &str) -> Term { Term::Parameter(p.to_string()) diff --git a/biscuit-parser/src/parser.rs b/biscuit-parser/src/parser.rs index c10e5715..d846fa1a 100644 --- a/biscuit-parser/src/parser.rs +++ b/biscuit-parser/src/parser.rs @@ -9,10 +9,13 @@ use nom::{ combinator::{consumed, cut, eof, map, map_res, opt, recognize, value}, error::{ErrorKind, FromExternalError, ParseError}, multi::{many0, separated_list0, separated_list1}, - sequence::{delimited, pair, preceded, terminated, tuple}, + sequence::{delimited, pair, preceded, separated_pair, terminated, tuple}, IResult, Offset, }; -use std::{collections::BTreeSet, convert::TryInto}; +use std::{ + collections::{BTreeMap, BTreeSet}, + convert::TryInto, +}; use thiserror::Error; /// parse a Datalog fact @@ -812,6 +815,7 @@ fn set(i: &str) -> IResult<&str, builder::Term, Error> { builder::Term::Parameter(_) => 7, builder::Term::Null => 8, builder::Term::Array(_) => 9, + builder::Term::Map(_) => 10, }; if let Some(k) = kind { @@ -835,9 +839,8 @@ fn set(i: &str) -> IResult<&str, builder::Term, Error> { } fn array(i: &str) -> IResult<&str, builder::Term, Error> { - //println!("set:\t{}", i); let (i, _) = preceded(space0, char('['))(i)?; - let (i, mut list) = cut(separated_list0(preceded(space0, char(',')), term_in_set))(i)?; + let (i, mut list) = cut(separated_list0(preceded(space0, char(',')), term_in_fact))(i)?; let mut array = Vec::new(); @@ -860,6 +863,7 @@ fn array(i: &str) -> IResult<&str, builder::Term, Error> { builder::Term::Parameter(_) => 8, builder::Term::Null => 9, builder::Term::Array(_) => 10, + builder::Term::Map(_) => 11, }; if let Some(k) = kind { @@ -882,11 +886,42 @@ fn array(i: &str) -> IResult<&str, builder::Term, Error> { Ok((i, builder::array(array))) } +fn parse_map(i: &str) -> IResult<&str, builder::Term, Error> { + let (i, _) = preceded(space0, char('{'))(i)?; + let (i, mut list) = cut(separated_list0( + preceded(space0, char(',')), + separated_pair(map_key, preceded(space0, char(':')), term_in_fact), + ))(i)?; + + let mut map = BTreeMap::new(); + + for (key, term) in list.drain(..) { + map.insert(key, term); + } + + let (i, _) = preceded(space0, char('}'))(i)?; + + Ok((i, builder::map(map))) +} + +fn map_key(i: &str) -> IResult<&str, builder::MapKey, Error> { + preceded( + space0, + alt(( + map(delimited(char('{'), parameter_name, char('}')), |s| { + builder::MapKey::Parameter(s.to_string()) + }), + map(parse_string, |s| builder::MapKey::Str(s.to_string())), + map(parse_integer, builder::MapKey::Integer), + )), + )(i) +} + fn term(i: &str) -> IResult<&str, builder::Term, Error> { preceded( space0, alt(( - parameter, string, date, variable, integer, bytes, boolean, null, set, array, + parameter, string, date, variable, integer, bytes, boolean, null, set, array, parse_map, )), )(i) } @@ -896,7 +931,7 @@ fn term_in_fact(i: &str) -> IResult<&str, builder::Term, Error> { space0, error( alt(( - parameter, string, date, integer, bytes, boolean, null, set, array, + parameter, string, date, integer, bytes, boolean, null, set, array, parse_map, )), |input| match input.chars().next() { None | Some(',') | Some(')') => "missing term".to_string(), @@ -912,7 +947,9 @@ fn term_in_set(i: &str) -> IResult<&str, builder::Term, Error> { preceded( space0, error( - alt((parameter, string, date, integer, bytes, boolean, null)), + alt(( + parameter, string, date, integer, bytes, boolean, null, parse_map, + )), |input| match input.chars().next() { None | Some(',') | Some(']') => "missing term".to_string(), Some('$') => "variables are not allowed in sets".to_string(), From 6d6df6d89e09e6275c40352a8c69b3b2c8ed4b45 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Thu, 23 May 2024 23:41:49 +0200 Subject: [PATCH 05/27] fix parameter extraction --- biscuit-auth/tests/macros.rs | 1 + biscuit-parser/src/builder.rs | 45 ++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/biscuit-auth/tests/macros.rs b/biscuit-auth/tests/macros.rs index beb61459..70518f4a 100644 --- a/biscuit-auth/tests/macros.rs +++ b/biscuit-auth/tests/macros.rs @@ -11,6 +11,7 @@ fn block_macro() { term_set.insert(builder::int(0i64)); let my_key = "my_value"; let mapkey = "hello"; + let mut b = block!( r#"fact("test", hex:aabbcc, [true], {my_key}, {term_set}, {"a": 1, 2 : "abcd", {mapkey}: 0 }); rule($0, true) <- fact($0, $1, $2, {my_key}); diff --git a/biscuit-parser/src/builder.rs b/biscuit-parser/src/builder.rs index 3539d513..fd69b2da 100644 --- a/biscuit-parser/src/builder.rs +++ b/biscuit-parser/src/builder.rs @@ -23,6 +23,35 @@ pub enum Term { Map(BTreeMap), } +impl Term { + fn extract_parameters(&self, parameters: &mut HashMap>) { + match self { + Term::Parameter(name) => { + parameters.insert(name.to_string(), None); + } + Term::Set(s) => { + for term in s { + term.extract_parameters(parameters); + } + } + Term::Array(a) => { + for term in a { + term.extract_parameters(parameters); + } + } + Term::Map(m) => { + for (key, term) in m { + if let MapKey::Parameter(name) = key { + parameters.insert(name.to_string(), None); + } + term.extract_parameters(parameters); + } + } + _ => {} + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum MapKey { Parameter(String), @@ -183,9 +212,7 @@ impl Fact { let terms: Vec = terms.into(); for term in &terms { - if let Term::Parameter(name) = &term { - parameters.insert(name.to_string(), None); - } + term.extract_parameters(&mut parameters); } Fact { predicate: Predicate::new(name, terms), @@ -341,23 +368,19 @@ impl Rule { let mut scope_parameters = HashMap::new(); for term in &head.terms { - if let Term::Parameter(name) = &term { - parameters.insert(name.to_string(), None); - } + term.extract_parameters(&mut parameters); } for predicate in &body { for term in &predicate.terms { - if let Term::Parameter(name) = &term { - parameters.insert(name.to_string(), None); - } + term.extract_parameters(&mut parameters); } } for expression in &expressions { for op in &expression.ops { - if let Op::Value(Term::Parameter(name)) = &op { - parameters.insert(name.to_string(), None); + if let Op::Value(term) = &op { + term.extract_parameters(&mut parameters); } } } From ae8af8da3f599b4ee4e984e8fe40047efc2dbbae Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 25 May 2024 15:03:39 +0200 Subject: [PATCH 06/27] fix --- biscuit-parser/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/biscuit-parser/src/builder.rs b/biscuit-parser/src/builder.rs index 72c8f5e8..276ea516 100644 --- a/biscuit-parser/src/builder.rs +++ b/biscuit-parser/src/builder.rs @@ -1,6 +1,6 @@ //! helper functions and structure to create tokens and blocks use std::{ - collections::{BTreeMap, BTreeSet, HashMap}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, time::{SystemTime, UNIX_EPOCH}, }; From 2424fe26870e682d075f7a4487787ae2bb2dc381 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 25 May 2024 16:33:38 +0200 Subject: [PATCH 07/27] fix the parser --- biscuit-parser/src/parser.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/biscuit-parser/src/parser.rs b/biscuit-parser/src/parser.rs index 3e5aa29f..05a5f619 100644 --- a/biscuit-parser/src/parser.rs +++ b/biscuit-parser/src/parser.rs @@ -736,7 +736,9 @@ fn integer(i: &str) -> IResult<&str, builder::Term, Error> { fn parse_date(i: &str) -> IResult<&str, u64, Error> { map_res( map_res( - take_while1(|c: char| c != ',' && c != ' ' && c != ')' && c != ']' && c != ';'), + take_while1(|c: char| { + c != ',' && c != ' ' && c != ')' && c != ']' && c != ';' && c != '}' + }), |s| time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339), ), |t| t.unix_timestamp().try_into(), @@ -955,11 +957,11 @@ fn term_in_set(i: &str) -> IResult<&str, builder::Term, Error> { parameter, string, date, integer, bytes, boolean, null, parse_map, )), |input| match input.chars().next() { - None | Some(',') | Some(']') => "missing term".to_string(), + None | Some(',') | Some('}') => "missing term".to_string(), Some('$') => "variables are not allowed in sets".to_string(), _ => "expected a valid term".to_string(), }, - " ,]\n;", + " ,}\n;", ), )(i) } @@ -1556,6 +1558,25 @@ mod tests { )) ); + let h = [ + builder::Term::Date(1575452801), + builder::Term::Date(1607075201), + ] + .iter() + .cloned() + .collect::>(); + assert_eq!( + super::expr("{2020-12-04T09:46:41+00:00, 2019-12-04T09:46:41+00:00}.contains(2020-12-04T09:46:41+00:00)").map(|(i, o)| (i, o.opcodes())), + Ok(( + "", + vec![ + Op::Value(set(h)), + Op::Value(builder::Term::Date(1607075201)), + Op::Binary(Binary::Contains), + ], + )) + ); + assert_eq!( super::expr("$0 == \"abc\"").map(|(i, o)| (i, o.opcodes())), Ok(( From 7ba138d6495030ff15d7661d6e50126fe8a435fc Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 25 May 2024 16:37:59 +0200 Subject: [PATCH 08/27] update samples --- biscuit-auth/examples/testcases.rs | 22 +++---- biscuit-auth/samples/README.md | 58 +++++++++--------- biscuit-auth/samples/samples.json | 36 +++++------ biscuit-auth/samples/test013_block_rules.bc | Bin 490 -> 490 bytes biscuit-auth/samples/test025_check_all.bc | Bin 258 -> 258 bytes .../samples/test028_expressions_v4.bc | Bin 388 -> 388 bytes 6 files changed, 58 insertions(+), 58 deletions(-) diff --git a/biscuit-auth/examples/testcases.rs b/biscuit-auth/examples/testcases.rs index 0c875ef3..a730d262 100644 --- a/biscuit-auth/examples/testcases.rs +++ b/biscuit-auth/examples/testcases.rs @@ -1328,22 +1328,22 @@ fn expressions(target: &str, root: &KeyPair, test: bool) -> TestResult { check if hex:12ab == hex:12ab; // set contains - check if [1, 2].contains(2); - check if [2020-12-04T09:46:41+00:00, 2019-12-04T09:46:41+00:00].contains(2020-12-04T09:46:41+00:00); - check if [true, false, true].contains(true); - check if ["abc", "def"].contains("abc"); - check if [hex:12ab, hex:34de].contains(hex:34de); - check if [1, 2].contains([2]); + check if {1, 2}.contains(2); + check if { 2020-12-04T09:46:41+00:00, 2019-12-04T09:46:41+00:00}.contains(2020-12-04T09:46:41+00:00); + check if {true, false, true}.contains(true); + check if {"abc", "def"}.contains("abc"); + check if {hex:12ab, hex:34de}.contains(hex:34de); + check if {1, 2}.contains({2}); // set equal - check if [1, 2] == [1, 2]; + check if {1, 2} == {1, 2}; // set intersection - check if [1, 2].intersection([2, 3]) == [2]; + check if {1, 2}.intersection({2, 3}) == {2}; // set union - check if [1, 2].union([2, 3]) == [1, 2, 3]; + check if {1, 2}.union({2, 3}) == {1, 2, 3}; // chained method calls - check if [1, 2, 3].intersection([1, 2]).contains(1); + check if {1, 2, 3}.intersection({1, 2}).contains(1); // chained method calls with unary method - check if [1, 2, 3].intersection([1, 2]).length() == 2; + check if {1, 2, 3}.intersection({1, 2}).length() == 2; "#) .build_with_rng(&root, SymbolTable::default(), &mut rng) .unwrap(); diff --git a/biscuit-auth/samples/README.md b/biscuit-auth/samples/README.md index 029ead92..6bfa449d 100644 --- a/biscuit-auth/samples/README.md +++ b/biscuit-auth/samples/README.md @@ -841,7 +841,7 @@ allow if true; revocation ids: - `c46d071ff3f33434223c8305fdad529f62bf78bb5d9cbfc2a345d4bca6bf314014840e18ba353f86fdb9073d58b12b8c872ac1f8e593c2e9064b90f6c2ede006` -- `a0c4c163a0b3ca406df4ece3d1371356190df04208eccef72f77e875ed0531b5d37e243d6f388b1967776a5dfd16ef228f19c5bdd6d2820f145c5ed3c3dcdc00` +- `da16dfc6d0db04e3378dedce4f0250792646e53408a9116e6d5e1651a4ed692d257e1f7b107cdc40fe6e47257d9c189b0d66a83991d67459608ea1807a9a9b04` authorizer world: ``` @@ -919,7 +919,7 @@ allow if true; revocation ids: - `c46d071ff3f33434223c8305fdad529f62bf78bb5d9cbfc2a345d4bca6bf314014840e18ba353f86fdb9073d58b12b8c872ac1f8e593c2e9064b90f6c2ede006` -- `a0c4c163a0b3ca406df4ece3d1371356190df04208eccef72f77e875ed0531b5d37e243d6f388b1967776a5dfd16ef228f19c5bdd6d2820f145c5ed3c3dcdc00` +- `da16dfc6d0db04e3378dedce4f0250792646e53408a9116e6d5e1651a4ed692d257e1f7b107cdc40fe6e47257d9c189b0d66a83991d67459608ea1807a9a9b04` authorizer world: ``` @@ -1249,17 +1249,17 @@ check if 2020-12-04T09:46:41Z >= 2019-12-04T09:46:41Z; check if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z; check if 2020-12-04T09:46:41Z == 2020-12-04T09:46:41Z; check if hex:12ab == hex:12ab; -check if [1, 2].contains(2); -check if [2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z].contains(2020-12-04T09:46:41Z); -check if [false, true].contains(true); -check if ["abc", "def"].contains("abc"); -check if [hex:12ab, hex:34de].contains(hex:34de); -check if [1, 2].contains([2]); -check if [1, 2] == [1, 2]; -check if [1, 2].intersection([2, 3]) == [2]; -check if [1, 2].union([2, 3]) == [1, 2, 3]; -check if [1, 2, 3].intersection([1, 2]).contains(1); -check if [1, 2, 3].intersection([1, 2]).length() == 2; +check if {1, 2}.contains(2); +check if {2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z}.contains(2020-12-04T09:46:41Z); +check if {false, true}.contains(true); +check if {"abc", "def"}.contains("abc"); +check if {hex:12ab, hex:34de}.contains(hex:34de); +check if {1, 2}.contains({2}); +check if {1, 2} == {1, 2}; +check if {1, 2}.intersection({2, 3}) == [2]; +check if {1, 2}.union({2, 3}) == [1, 2, 3]; +check if {1, 2, 3}.intersection({1, 2}).contains(1); +check if {1, 2, 3}.intersection({1, 2}).length() == 2; ``` ### validation @@ -1270,7 +1270,7 @@ allow if true; ``` revocation ids: -- `3d5b23b502b3dd920bfb68b9039164d1563bb8927210166fa5c17f41b76b31bb957bc2ed3318452958f658baa2d398fe4cf25c58a27e6c8bc42c9702c8aa1b0c` +- `d85272eaf1bccf26edc08148aa90ddd63f55e5a44c8d5d7d6f17d9378ed19929da3c7353d7780ffe6672cc370f8df3b07f8e8636e48b5f74c1581d1cc1288a01` authorizer world: ``` @@ -1308,22 +1308,22 @@ World { "check if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z", "check if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z", "check if 3 == 3", - "check if [\"abc\", \"def\"].contains(\"abc\")", - "check if [1, 2, 3].intersection([1, 2]).contains(1)", - "check if [1, 2, 3].intersection([1, 2]).length() == 2", - "check if [1, 2] == [1, 2]", - "check if [1, 2].contains(2)", - "check if [1, 2].contains([2])", - "check if [1, 2].intersection([2, 3]) == [2]", - "check if [1, 2].union([2, 3]) == [1, 2, 3]", - "check if [2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z].contains(2020-12-04T09:46:41Z)", - "check if [false, true].contains(true)", - "check if [hex:12ab, hex:34de].contains(hex:34de)", "check if false == false", "check if false || true", "check if hex:12ab == hex:12ab", "check if true", "check if true == true", + "check if {\"abc\", \"def\"}.contains(\"abc\")", + "check if {1, 2, 3}.intersection({1, 2}).contains(1)", + "check if {1, 2, 3}.intersection({1, 2}).length() == 2", + "check if {1, 2} == {1, 2}", + "check if {1, 2}.contains(2)", + "check if {1, 2}.contains({2})", + "check if {1, 2}.intersection({2, 3}) == [2]", + "check if {1, 2}.union({2, 3}) == [1, 2, 3]", + "check if {2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z}.contains(2020-12-04T09:46:41Z)", + "check if {false, true}.contains(true)", + "check if {hex:12ab, hex:34de}.contains(hex:34de)", ], }, ] @@ -1333,7 +1333,7 @@ World { } ``` -result: `Ok(0)` +result: `Err(Execution(InvalidType))` ------------------------------ @@ -1922,7 +1922,7 @@ allow if true; ``` revocation ids: -- `c456817012e1d523c6d145b6d6a3475d9f7dd4383c535454ff3f745ecf4234984ce09b9dec0551f3d783abe850f826ce43b12f1fd91999a4753a56ecf4c56d0d` +- `899e1fa26d72b860fa6a6e6d58e71cc873230260dcb41d3390e0703c6e134d955defbd0741c23272ac6e6abb2066a23cff2fe815dc5e5bfd712d177cf74ee108` authorizer world: ``` @@ -1977,7 +1977,7 @@ allow if true; ``` revocation ids: -- `c456817012e1d523c6d145b6d6a3475d9f7dd4383c535454ff3f745ecf4234984ce09b9dec0551f3d783abe850f826ce43b12f1fd91999a4753a56ecf4c56d0d` +- `899e1fa26d72b860fa6a6e6d58e71cc873230260dcb41d3390e0703c6e134d955defbd0741c23272ac6e6abb2066a23cff2fe815dc5e5bfd712d177cf74ee108` authorizer world: ``` @@ -2331,7 +2331,7 @@ allow if true; ``` revocation ids: -- `117fa653744c859561555e6a6f5990e3a8e7817f91b87aa6991b6d64297158b4e884c92d10f49f74c96069df722aa676839b72751ca9d1fe83a7025b591de00b` +- `04f9b08f5cf677aa890fd830a4acc2a0ec7d4c9e2657d65ac691ae6512b549184fd7c6deaf17c446f12324a1c454fe373290fe8981bae69cc6054de7312da00f` authorizer world: ``` diff --git a/biscuit-auth/samples/samples.json b/biscuit-auth/samples/samples.json index 54a1ee9e..0800bb49 100644 --- a/biscuit-auth/samples/samples.json +++ b/biscuit-auth/samples/samples.json @@ -924,7 +924,7 @@ "authorizer_code": "resource(\"file1\");\ntime(2020-12-21T09:23:12Z);\n\nallow if true;\n", "revocation_ids": [ "c46d071ff3f33434223c8305fdad529f62bf78bb5d9cbfc2a345d4bca6bf314014840e18ba353f86fdb9073d58b12b8c872ac1f8e593c2e9064b90f6c2ede006", - "a0c4c163a0b3ca406df4ece3d1371356190df04208eccef72f77e875ed0531b5d37e243d6f388b1967776a5dfd16ef228f19c5bdd6d2820f145c5ed3c3dcdc00" + "da16dfc6d0db04e3378dedce4f0250792646e53408a9116e6d5e1651a4ed692d257e1f7b107cdc40fe6e47257d9c189b0d66a83991d67459608ea1807a9a9b04" ] }, "file2": { @@ -993,7 +993,7 @@ "authorizer_code": "resource(\"file2\");\ntime(2020-12-21T09:23:12Z);\n\nallow if true;\n", "revocation_ids": [ "c46d071ff3f33434223c8305fdad529f62bf78bb5d9cbfc2a345d4bca6bf314014840e18ba353f86fdb9073d58b12b8c872ac1f8e593c2e9064b90f6c2ede006", - "a0c4c163a0b3ca406df4ece3d1371356190df04208eccef72f77e875ed0531b5d37e243d6f388b1967776a5dfd16ef228f19c5bdd6d2820f145c5ed3c3dcdc00" + "da16dfc6d0db04e3378dedce4f0250792646e53408a9116e6d5e1651a4ed692d257e1f7b107cdc40fe6e47257d9c189b0d66a83991d67459608ea1807a9a9b04" ] } } @@ -1245,7 +1245,7 @@ ], "public_keys": [], "external_key": null, - "code": "check if true;\ncheck if !false;\ncheck if !false && true;\ncheck if false || true;\ncheck if (true || false) && true;\ncheck if true == true;\ncheck if false == false;\ncheck if 1 < 2;\ncheck if 2 > 1;\ncheck if 1 <= 2;\ncheck if 1 <= 1;\ncheck if 2 >= 1;\ncheck if 2 >= 2;\ncheck if 3 == 3;\ncheck if 1 + 2 * 3 - 4 / 2 == 5;\ncheck if \"hello world\".starts_with(\"hello\") && \"hello world\".ends_with(\"world\");\ncheck if \"aaabde\".matches(\"a*c?.e\");\ncheck if \"aaabde\".contains(\"abd\");\ncheck if \"aaabde\" == \"aaa\" + \"b\" + \"de\";\ncheck if \"abcD12\" == \"abcD12\";\ncheck if \"abcD12\".length() == 6;\ncheck if \"Ă©\".length() == 2;\ncheck if 2019-12-04T09:46:41Z < 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z > 2019-12-04T09:46:41Z;\ncheck if 2019-12-04T09:46:41Z <= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2019-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z == 2020-12-04T09:46:41Z;\ncheck if hex:12ab == hex:12ab;\ncheck if [1, 2].contains(2);\ncheck if [2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z].contains(2020-12-04T09:46:41Z);\ncheck if [false, true].contains(true);\ncheck if [\"abc\", \"def\"].contains(\"abc\");\ncheck if [hex:12ab, hex:34de].contains(hex:34de);\ncheck if [1, 2].contains([2]);\ncheck if [1, 2] == [1, 2];\ncheck if [1, 2].intersection([2, 3]) == [2];\ncheck if [1, 2].union([2, 3]) == [1, 2, 3];\ncheck if [1, 2, 3].intersection([1, 2]).contains(1);\ncheck if [1, 2, 3].intersection([1, 2]).length() == 2;\n" + "code": "check if true;\ncheck if !false;\ncheck if !false && true;\ncheck if false || true;\ncheck if (true || false) && true;\ncheck if true == true;\ncheck if false == false;\ncheck if 1 < 2;\ncheck if 2 > 1;\ncheck if 1 <= 2;\ncheck if 1 <= 1;\ncheck if 2 >= 1;\ncheck if 2 >= 2;\ncheck if 3 == 3;\ncheck if 1 + 2 * 3 - 4 / 2 == 5;\ncheck if \"hello world\".starts_with(\"hello\") && \"hello world\".ends_with(\"world\");\ncheck if \"aaabde\".matches(\"a*c?.e\");\ncheck if \"aaabde\".contains(\"abd\");\ncheck if \"aaabde\" == \"aaa\" + \"b\" + \"de\";\ncheck if \"abcD12\" == \"abcD12\";\ncheck if \"abcD12\".length() == 6;\ncheck if \"Ă©\".length() == 2;\ncheck if 2019-12-04T09:46:41Z < 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z > 2019-12-04T09:46:41Z;\ncheck if 2019-12-04T09:46:41Z <= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2019-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z == 2020-12-04T09:46:41Z;\ncheck if hex:12ab == hex:12ab;\ncheck if {1, 2}.contains(2);\ncheck if {2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z}.contains(2020-12-04T09:46:41Z);\ncheck if {false, true}.contains(true);\ncheck if {\"abc\", \"def\"}.contains(\"abc\");\ncheck if {hex:12ab, hex:34de}.contains(hex:34de);\ncheck if {1, 2}.contains({2});\ncheck if {1, 2} == {1, 2};\ncheck if {1, 2}.intersection({2, 3}) == {2};\ncheck if {1, 2}.union({2, 3}) == {1, 2, 3};\ncheck if {1, 2, 3}.intersection({1, 2}).contains(1);\ncheck if {1, 2, 3}.intersection({1, 2}).length() == 2;\n" } ], "validations": { @@ -1282,22 +1282,22 @@ "check if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z", "check if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z", "check if 3 == 3", - "check if [\"abc\", \"def\"].contains(\"abc\")", - "check if [1, 2, 3].intersection([1, 2]).contains(1)", - "check if [1, 2, 3].intersection([1, 2]).length() == 2", - "check if [1, 2] == [1, 2]", - "check if [1, 2].contains(2)", - "check if [1, 2].contains([2])", - "check if [1, 2].intersection([2, 3]) == [2]", - "check if [1, 2].union([2, 3]) == [1, 2, 3]", - "check if [2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z].contains(2020-12-04T09:46:41Z)", - "check if [false, true].contains(true)", - "check if [hex:12ab, hex:34de].contains(hex:34de)", "check if false == false", "check if false || true", "check if hex:12ab == hex:12ab", "check if true", - "check if true == true" + "check if true == true", + "check if {\"abc\", \"def\"}.contains(\"abc\")", + "check if {1, 2, 3}.intersection({1, 2}).contains(1)", + "check if {1, 2, 3}.intersection({1, 2}).length() == 2", + "check if {1, 2} == {1, 2}", + "check if {1, 2}.contains(2)", + "check if {1, 2}.contains({2})", + "check if {1, 2}.intersection({2, 3}) == {2}", + "check if {1, 2}.union({2, 3}) == {1, 2, 3}", + "check if {2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z}.contains(2020-12-04T09:46:41Z)", + "check if {false, true}.contains(true)", + "check if {hex:12ab, hex:34de}.contains(hex:34de)" ] } ], @@ -1860,7 +1860,7 @@ }, "authorizer_code": "operation(\"A\");\noperation(\"B\");\n\nallow if true;\n", "revocation_ids": [ - "c456817012e1d523c6d145b6d6a3475d9f7dd4383c535454ff3f745ecf4234984ce09b9dec0551f3d783abe850f826ce43b12f1fd91999a4753a56ecf4c56d0d" + "899e1fa26d72b860fa6a6e6d58e71cc873230260dcb41d3390e0703c6e134d955defbd0741c23272ac6e6abb2066a23cff2fe815dc5e5bfd712d177cf74ee108" ] }, "A, invalid": { @@ -1919,7 +1919,7 @@ }, "authorizer_code": "operation(\"A\");\noperation(\"invalid\");\n\nallow if true;\n", "revocation_ids": [ - "c456817012e1d523c6d145b6d6a3475d9f7dd4383c535454ff3f745ecf4234984ce09b9dec0551f3d783abe850f826ce43b12f1fd91999a4753a56ecf4c56d0d" + "899e1fa26d72b860fa6a6e6d58e71cc873230260dcb41d3390e0703c6e134d955defbd0741c23272ac6e6abb2066a23cff2fe815dc5e5bfd712d177cf74ee108" ] } } @@ -2173,7 +2173,7 @@ }, "authorizer_code": "allow if true;\n", "revocation_ids": [ - "117fa653744c859561555e6a6f5990e3a8e7817f91b87aa6991b6d64297158b4e884c92d10f49f74c96069df722aa676839b72751ca9d1fe83a7025b591de00b" + "04f9b08f5cf677aa890fd830a4acc2a0ec7d4c9e2657d65ac691ae6512b549184fd7c6deaf17c446f12324a1c454fe373290fe8981bae69cc6054de7312da00f" ] } } diff --git a/biscuit-auth/samples/test013_block_rules.bc b/biscuit-auth/samples/test013_block_rules.bc index 149b4ee813b07486eed803628efcf7616e08b9ce..2f2d4d99c0aac0317dcc37430ff22cd4b7316d16 100644 GIT binary patch delta 82 zcmV-Y0ImP(1L^~?DFFdWlPUpRK-w1H#?adY<2Q}%&QAhRc_v2XGzh5?Zf#x`QKao@ oEhT;*dk}owK>lt=C4HP2n+;~DIg!?MSzwN#fO?vn1e3-AB!|c%c>n+a delta 82 zcmV-Y0ImP(1L^~?DFFdGlPUpRK%m6IW1zFjKyCEwgp0)hgNHvvkKIA%bJo*$xZa=2jnYHn><=N!m$BLZODv>h{$;BY){6HS#} l@4W{>!ZLEKZfd(AW}-a*FX$EAUR(WfEf;+EPT>fV-Xt?lBP##^ delta 79 zcmV-V0I>gp0)hgNHvu}4IA%b^R)KI5;ngF?(M7h_qeoqzebhKSQ&d#{KXhKtLNu66 l;G3Q71yS?YgRAIJ_$JOnu`eIl8JVPYI#%rT#cd6d-Xx-$CRG3c diff --git a/biscuit-auth/samples/test028_expressions_v4.bc b/biscuit-auth/samples/test028_expressions_v4.bc index c34d7a103fcfe7a1ada52e0be46ec70f544137e0..bb28a285bd9b42ce6d0a5c8ef24e9b42b96aa696 100644 GIT binary patch delta 94 zcmV-k0HObc1B3&R+7C(y3IY%T3IY%W3JeMgN|EU|Km_@)k6iY5s)-NSFr=))pzM82 zo+ek;TE>yCWfHYX7*E&6-me$LM)4ygp~O`FH!_g^iGjN2oW=!B=P@my50hX4B%^I7 A`2YX_ delta 94 zcmV-k0HObc1B3&R+7CJi3IY%T3IY%W3JeMgI+5u&KoNhYQ*=y)m0?w0YHwMP zf04L)rkNXUWGQi2wCIG%EfDmdbje_8-*PIZc7vO8bsVYD{)49iTUj083zJ|0Brzl< A)&Kwi From 8e81d9eb5558f012f8819f1e6a6cf6ee7fd9fbef Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 25 May 2024 19:31:54 +0200 Subject: [PATCH 09/27] update samples --- biscuit-auth/samples/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/biscuit-auth/samples/README.md b/biscuit-auth/samples/README.md index 6bfa449d..4b1d1349 100644 --- a/biscuit-auth/samples/README.md +++ b/biscuit-auth/samples/README.md @@ -1256,8 +1256,8 @@ check if {"abc", "def"}.contains("abc"); check if {hex:12ab, hex:34de}.contains(hex:34de); check if {1, 2}.contains({2}); check if {1, 2} == {1, 2}; -check if {1, 2}.intersection({2, 3}) == [2]; -check if {1, 2}.union({2, 3}) == [1, 2, 3]; +check if {1, 2}.intersection({2, 3}) == {2}; +check if {1, 2}.union({2, 3}) == {1, 2, 3}; check if {1, 2, 3}.intersection({1, 2}).contains(1); check if {1, 2, 3}.intersection({1, 2}).length() == 2; ``` @@ -1270,7 +1270,7 @@ allow if true; ``` revocation ids: -- `d85272eaf1bccf26edc08148aa90ddd63f55e5a44c8d5d7d6f17d9378ed19929da3c7353d7780ffe6672cc370f8df3b07f8e8636e48b5f74c1581d1cc1288a01` +- `3d5b23b502b3dd920bfb68b9039164d1563bb8927210166fa5c17f41b76b31bb957bc2ed3318452958f658baa2d398fe4cf25c58a27e6c8bc42c9702c8aa1b0c` authorizer world: ``` @@ -1319,8 +1319,8 @@ World { "check if {1, 2} == {1, 2}", "check if {1, 2}.contains(2)", "check if {1, 2}.contains({2})", - "check if {1, 2}.intersection({2, 3}) == [2]", - "check if {1, 2}.union({2, 3}) == [1, 2, 3]", + "check if {1, 2}.intersection({2, 3}) == {2}", + "check if {1, 2}.union({2, 3}) == {1, 2, 3}", "check if {2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z}.contains(2020-12-04T09:46:41Z)", "check if {false, true}.contains(true)", "check if {hex:12ab, hex:34de}.contains(hex:34de)", @@ -1333,7 +1333,7 @@ World { } ``` -result: `Err(Execution(InvalidType))` +result: `Ok(0)` ------------------------------ From 7e3e0d162d7d8bc10b7f6e4995c85c8db5851a37 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 25 May 2024 19:43:52 +0200 Subject: [PATCH 10/27] add samples for arrays --- biscuit-auth/examples/testcases.rs | 58 ++++++++++++++++++ biscuit-auth/samples/README.md | 52 ++++++++++++++++ biscuit-auth/samples/samples.json | 49 +++++++++++++++ .../samples/test031_expressions_v5.bc | Bin 0 -> 497 bytes 4 files changed, 159 insertions(+) create mode 100644 biscuit-auth/samples/test031_expressions_v5.bc diff --git a/biscuit-auth/examples/testcases.rs b/biscuit-auth/examples/testcases.rs index a730d262..1db1c2c6 100644 --- a/biscuit-auth/examples/testcases.rs +++ b/biscuit-auth/examples/testcases.rs @@ -146,6 +146,8 @@ fn main() { add_test_result(&mut results, null(&target, &root, test)); + add_test_result(&mut results, expressions_v5(&target, &root, test)); + if json { let s = serde_json::to_string_pretty(&TestCases { root_private_key: hex::encode(root.private().to_bytes()), @@ -2033,6 +2035,62 @@ fn null(target: &str, root: &KeyPair, test: bool) -> TestResult { } } +fn expressions_v5(target: &str, root: &KeyPair, test: bool) -> TestResult { + let mut rng: StdRng = SeedableRng::seed_from_u64(1234); + let title = "test expression syntax and all available operations (v5 blocks)".to_string(); + let filename = "test031_expressions_v5".to_string(); + let token; + + let biscuit = biscuit!( + r#" + //boolean and + check if !false && true; + //boolean or + check if false || true; + //boolean parens + check if (true || false) && true; + // boolean and laziness + //check if !(false && "x".intersection("x")); + // boolean or laziness + //check if true || "x".intersection("x"); + //all + //check if [1,2,3].all($p -> $p > 0); + //all + //check if ![1,2,3].all($p -> $p == 2); + //any + //check if [1,2,3].any($p -> $p > 2); + //any + //check if ![1,2,3].any($p -> $p > 3); + // nested closures + //check if [1,2,3].any($p -> $p > 1 && [3,4,5].any($q -> $p == $q)); + // array + check if [1, 2, 1].length() == 3; + check if ["a", "b"] != [1, 2, 3]; + check if ["a", "b", "c"].contains("c"); + check if [1, 2, 3].starts_with([1, 2]); + check if [4, 5, 6 ].ends_with([6]); + "# + ) + .build_with_rng(&root, SymbolTable::default(), &mut rng) + .unwrap(); + token = print_blocks(&biscuit); + + let data = write_or_load_testcase(target, &filename, root, &biscuit, test); + + let mut validations = BTreeMap::new(); + validations.insert( + "".to_string(), + validate_token(root, &data[..], "allow if true"), + ); + + TestResult { + title, + filename, + token, + validations, + } +} + fn print_blocks(token: &Biscuit) -> Vec { let mut v = Vec::new(); diff --git a/biscuit-auth/samples/README.md b/biscuit-auth/samples/README.md index 4b1d1349..95ceb42e 100644 --- a/biscuit-auth/samples/README.md +++ b/biscuit-auth/samples/README.md @@ -2656,3 +2656,55 @@ World { result: `Err(FailedLogic(Unauthorized { policy: Allow(0), checks: [Block(FailedBlockCheck { block_id: 0, check_id: 0, rule: "check if fact(null, $value), $value == null" }), Block(FailedBlockCheck { block_id: 0, check_id: 1, rule: "reject if fact(null, $value), $value != null" })] }))` + +------------------------------ + +## test expression syntax and all available operations (v5 blocks): test031_expressions_v5.bc +### token + +authority: +symbols: [] + +public keys: [] + +``` +check if !false && true; +check if false || true; +check if (true || false) && true; +``` + +### validation + +authorizer code: +``` +allow if true; +``` + +revocation ids: +- `bd0b89d109b3f450421bf432ecf52c21053ec55f7422115d5fec6324145406b1a7521281ad609b248830d4cea1c78a5b51d16b2922e5918d6d85fe14d24e7c01` + +authorizer world: +``` +World { + facts: [] + rules: [] + checks: [ + Checks { + origin: Some( + 0, + ), + checks: [ + "check if !false && true", + "check if (true || false) && true", + "check if false || true", + ], + }, +] + policies: [ + "allow if true", +] +} +``` + +result: `Ok(0)` + diff --git a/biscuit-auth/samples/samples.json b/biscuit-auth/samples/samples.json index 0800bb49..ee142013 100644 --- a/biscuit-auth/samples/samples.json +++ b/biscuit-auth/samples/samples.json @@ -2501,6 +2501,55 @@ ] } } + }, + { + "title": "test expression syntax and all available operations (v5 blocks)", + "filename": "test031_expressions_v5.bc", + "token": [ + { + "symbols": [ + "a", + "b", + "c" + ], + "public_keys": [], + "external_key": null, + "code": "check if !false && true;\ncheck if false || true;\ncheck if (true || false) && true;\ncheck if [1, 2, 1].length() == 3;\ncheck if [\"a\", \"b\"] != [1, 2, 3];\ncheck if [\"a\", \"b\", \"c\"].contains(\"c\");\ncheck if [1, 2, 3].starts_with([1, 2]);\ncheck if [4, 5, 6].ends_with([6]);\n" + } + ], + "validations": { + "": { + "world": { + "facts": [], + "rules": [], + "checks": [ + { + "origin": 0, + "checks": [ + "check if !false && true", + "check if (true || false) && true", + "check if [\"a\", \"b\", \"c\"].contains(\"c\")", + "check if [\"a\", \"b\"] != [1, 2, 3]", + "check if [1, 2, 1].length() == 3", + "check if [1, 2, 3].starts_with([1, 2])", + "check if [4, 5, 6].ends_with([6])", + "check if false || true" + ] + } + ], + "policies": [ + "allow if true" + ] + }, + "result": { + "Ok": 0 + }, + "authorizer_code": "allow if true;\n", + "revocation_ids": [ + "092bc84e06e25bd984d31361f11fd1f3f70989be15a40a7bec7f989f5c5a03c8a01de3670b31705a3bb3f5729cd3702ff89783548a8dfcb89672eafd24a35a07" + ] + } + } } ] } diff --git a/biscuit-auth/samples/test031_expressions_v5.bc b/biscuit-auth/samples/test031_expressions_v5.bc new file mode 100644 index 0000000000000000000000000000000000000000..7d0af91b3bf275e6c53b9a7eb441336b5141c599 GIT binary patch literal 497 zcmWeS#mse|iHk9ji!q6dF^Vl~p^(gCYg!(ti_7nc_Y+;t%JY)EFqYzMiO6^PlmIJnrnKs+|E+3Z3p z91KDV0-?tPHZBZ^G+!@jeoSqbQS!_f<7Ms Date: Sun, 26 May 2024 11:33:30 +0200 Subject: [PATCH 11/27] basic operations for map --- biscuit-auth/src/datalog/expression.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/biscuit-auth/src/datalog/expression.rs b/biscuit-auth/src/datalog/expression.rs index b69da6b5..7b8ed51f 100644 --- a/biscuit-auth/src/datalog/expression.rs +++ b/biscuit-auth/src/datalog/expression.rs @@ -41,6 +41,8 @@ impl Unary { (Unary::Length, Term::Bytes(s)) => Ok(Term::Integer(s.len() as i64)), (Unary::Length, Term::Set(s)) => Ok(Term::Integer(s.len() as i64)), (Unary::Length, Term::Array(a)) => Ok(Term::Integer(a.len() as i64)), + (Unary::Length, Term::Map(m)) => Ok(Term::Integer(m.len() as i64)), + _ => { //println!("unexpected value type on the stack"); Err(error::Expression::InvalidType) @@ -226,6 +228,16 @@ impl Binary { (Binary::Prefix, Term::Array(i), Term::Array(j)) => Ok(Term::Bool(i.starts_with(&j))), (Binary::Suffix, Term::Array(i), Term::Array(j)) => Ok(Term::Bool(i.ends_with(&j))), + // map + (Binary::Equal, Term::Map(i), Term::Map(j)) => Ok(Term::Bool(i == j)), + (Binary::NotEqual, Term::Map(i), Term::Map(j)) => Ok(Term::Bool(i != j)), + (Binary::Contains, Term::Map(i), j) => { + Ok(Term::Bool(i.iter().any(|elem| match (elem.0, &j) { + (super::MapKey::Integer(k), Term::Integer(l)) => k == l, + (super::MapKey::Str(k), Term::Str(l)) => k == l, + _ => false, + }))) + } _ => { //println!("unexpected value type on the stack"); Err(error::Expression::InvalidType) From 7554559aa1a2ccacbce036ae6776debacc6595d9 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sun, 26 May 2024 11:40:00 +0200 Subject: [PATCH 12/27] add checks in samples --- biscuit-auth/examples/testcases.rs | 6 ++++++ biscuit-auth/samples/README.md | 24 ++++++++++++++++++++++-- biscuit-auth/samples/samples.json | 14 ++++++++++---- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/biscuit-auth/examples/testcases.rs b/biscuit-auth/examples/testcases.rs index 1db1c2c6..40aea970 100644 --- a/biscuit-auth/examples/testcases.rs +++ b/biscuit-auth/examples/testcases.rs @@ -2066,9 +2066,15 @@ fn expressions_v5(target: &str, root: &KeyPair, test: bool) -> TestResult { // array check if [1, 2, 1].length() == 3; check if ["a", "b"] != [1, 2, 3]; + check if ["a", "b"] == ["a", "b"]; check if ["a", "b", "c"].contains("c"); check if [1, 2, 3].starts_with([1, 2]); check if [4, 5, 6 ].ends_with([6]); + // map + check if { "a": 1 , "b": 2, "c": 3, "d": 4}.length() == 4; + check if { 1: "a" , 2: "b"} != { "a": 1 , "b": 2}; + check if { 1: "a" , 2: "b"} == { 2: "b", 1: "a" }; + check if { "a": 1 , "b": 2, "c": 3, "d": 4}.contains("d"); "# ) .build_with_rng(&root, SymbolTable::default(), &mut rng) diff --git a/biscuit-auth/samples/README.md b/biscuit-auth/samples/README.md index 95ceb42e..4c479a53 100644 --- a/biscuit-auth/samples/README.md +++ b/biscuit-auth/samples/README.md @@ -2663,7 +2663,7 @@ result: `Err(FailedLogic(Unauthorized { policy: Allow(0), checks: [Block(FailedB ### token authority: -symbols: [] +symbols: ["a", "b", "c", "d"] public keys: [] @@ -2671,6 +2671,16 @@ public keys: [] check if !false && true; check if false || true; check if (true || false) && true; +check if [1, 2, 1].length() == 3; +check if ["a", "b"] != [1, 2, 3]; +check if ["a", "b"] == ["a", "b"]; +check if ["a", "b", "c"].contains("c"); +check if [1, 2, 3].starts_with([1, 2]); +check if [4, 5, 6].ends_with([6]); +check if {"a": 1, "b": 2, "c": 3, "d": 4}.length() == 4; +check if {1: "a", 2: "b"} != {"a": 1, "b": 2}; +check if {1: "a", 2: "b"} == {1: "a", 2: "b"}; +check if {"a": 1, "b": 2, "c": 3, "d": 4}.contains("d"); ``` ### validation @@ -2681,7 +2691,7 @@ allow if true; ``` revocation ids: -- `bd0b89d109b3f450421bf432ecf52c21053ec55f7422115d5fec6324145406b1a7521281ad609b248830d4cea1c78a5b51d16b2922e5918d6d85fe14d24e7c01` +- `3e0e00592b30db8fa505dc00f0f6ae020713dfa3175ab100828c9c33d116f651f365c34be594484befbd97e7039c5ed03570961385791ccbaf017ad715f2f801` authorizer world: ``` @@ -2696,7 +2706,17 @@ World { checks: [ "check if !false && true", "check if (true || false) && true", + "check if [\"a\", \"b\", \"c\"].contains(\"c\")", + "check if [\"a\", \"b\"] != [1, 2, 3]", + "check if [\"a\", \"b\"] == [\"a\", \"b\"]", + "check if [1, 2, 1].length() == 3", + "check if [1, 2, 3].starts_with([1, 2])", + "check if [4, 5, 6].ends_with([6])", "check if false || true", + "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\")", + "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4", + "check if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2}", + "check if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"}", ], }, ] diff --git a/biscuit-auth/samples/samples.json b/biscuit-auth/samples/samples.json index ee142013..b6462b3b 100644 --- a/biscuit-auth/samples/samples.json +++ b/biscuit-auth/samples/samples.json @@ -2510,11 +2510,12 @@ "symbols": [ "a", "b", - "c" + "c", + "d" ], "public_keys": [], "external_key": null, - "code": "check if !false && true;\ncheck if false || true;\ncheck if (true || false) && true;\ncheck if [1, 2, 1].length() == 3;\ncheck if [\"a\", \"b\"] != [1, 2, 3];\ncheck if [\"a\", \"b\", \"c\"].contains(\"c\");\ncheck if [1, 2, 3].starts_with([1, 2]);\ncheck if [4, 5, 6].ends_with([6]);\n" + "code": "check if !false && true;\ncheck if false || true;\ncheck if (true || false) && true;\ncheck if [1, 2, 1].length() == 3;\ncheck if [\"a\", \"b\"] != [1, 2, 3];\ncheck if [\"a\", \"b\"] == [\"a\", \"b\"];\ncheck if [\"a\", \"b\", \"c\"].contains(\"c\");\ncheck if [1, 2, 3].starts_with([1, 2]);\ncheck if [4, 5, 6].ends_with([6]);\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4;\ncheck if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2};\ncheck if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"};\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\");\n" } ], "validations": { @@ -2530,10 +2531,15 @@ "check if (true || false) && true", "check if [\"a\", \"b\", \"c\"].contains(\"c\")", "check if [\"a\", \"b\"] != [1, 2, 3]", + "check if [\"a\", \"b\"] == [\"a\", \"b\"]", "check if [1, 2, 1].length() == 3", "check if [1, 2, 3].starts_with([1, 2])", "check if [4, 5, 6].ends_with([6])", - "check if false || true" + "check if false || true", + "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\")", + "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4", + "check if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2}", + "check if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"}" ] } ], @@ -2546,7 +2552,7 @@ }, "authorizer_code": "allow if true;\n", "revocation_ids": [ - "092bc84e06e25bd984d31361f11fd1f3f70989be15a40a7bec7f989f5c5a03c8a01de3670b31705a3bb3f5729cd3702ff89783548a8dfcb89672eafd24a35a07" + "3e0e00592b30db8fa505dc00f0f6ae020713dfa3175ab100828c9c33d116f651f365c34be594484befbd97e7039c5ed03570961385791ccbaf017ad715f2f801" ] } } From dbf7dc3e1bddbbd32f3ecce9c9bae8c8e9434299 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sun, 26 May 2024 11:40:44 +0200 Subject: [PATCH 13/27] missing sample --- biscuit-auth/samples/test031_expressions_v5.bc | Bin 497 -> 843 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/biscuit-auth/samples/test031_expressions_v5.bc b/biscuit-auth/samples/test031_expressions_v5.bc index 7d0af91b3bf275e6c53b9a7eb441336b5141c599..83a55bd7add46067e7bb1f0c2a9d8c227ae3f02a 100644 GIT binary patch delta 448 zcmey!e40&2XbBtFPF60)L@vf8F2-ao#*~Tt=k@itbhwx}q@~oj__%nyxVV@l8aRMN zBL@bbMT&`oW#ZkAdLth$FR&gLE(0#TARR7FE@puS4k0E1MliDx#AE_9n?OuvFtZuN zWZ_~FV&Y)pV&MX_K_(ixak+p^v*VKDk_Zw5DrVwf6au>g#AE_8fv$ncF$>gVS_N{N z2#E$^vjpr#BX=%Wu!HQedruRu|5$;6*UZ5MGKqC^Dx-#j9Unuaw!!WGrL1=tK73op z#4db)v3S%*hNhl5#uvrD1%6IF?EQ3#hxhxv)1NcXiMwE0Fip6%Qs(q}#;WV0pMEe- IKFg>A0QfRVH2?qr delta 100 zcmV-q0Gt2I2Jr(A63PP#-vSB&VG03a3ISu063>$n0f&>n0w_QUE67d;;#=8-(-UFw zAJOyo35mWHqzZfNf0&qQtv-NVE({L~NmxEM_jr_Qla_apgqgn@( GCj%sn$|{Bc From cf026b8c3e397835539f94e3aeadeb844e723e20 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 8 Jun 2024 15:44:32 +0200 Subject: [PATCH 14/27] fix sample --- biscuit-auth/examples/testcases.rs | 2 +- biscuit-auth/samples/README.md | 6 +++--- biscuit-auth/samples/samples.json | 6 +++--- biscuit-auth/samples/test017_expressions.bc | Bin 1695 -> 1687 bytes 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/biscuit-auth/examples/testcases.rs b/biscuit-auth/examples/testcases.rs index a8672221..053acba9 100644 --- a/biscuit-auth/examples/testcases.rs +++ b/biscuit-auth/examples/testcases.rs @@ -1301,7 +1301,7 @@ fn expressions(target: &str, root: &KeyPair, test: bool) -> TestResult { check if 1 + 2 * 3 - 4 /2 === 5; // string prefix and suffix - check if "hello world".starts_with("hello") && "hello world".ends_with("world"); + check if "hello world".starts_with("hello"), "hello world".ends_with("world"); // string regex check if "aaabde".matches("a*c?.e"); // string contains diff --git a/biscuit-auth/samples/README.md b/biscuit-auth/samples/README.md index 81dff1e4..e7ce526c 100644 --- a/biscuit-auth/samples/README.md +++ b/biscuit-auth/samples/README.md @@ -1231,7 +1231,7 @@ check if 2 >= 1; check if 2 >= 2; check if 3 === 3; check if 1 + 2 * 3 - 4 / 2 === 5; -check if "hello world".starts_with("hello") && "hello world".ends_with("world"); +check if "hello world".starts_with("hello"), "hello world".ends_with("world"); check if "aaabde".matches("a*c?.e"); check if "aaabde".contains("abd"); check if "aaabde" === "aaa" + "b" + "de"; @@ -1267,7 +1267,7 @@ allow if true; ``` revocation ids: -- `a2640c5f1c1e86302e77422ccda6f7f02a21f63d88d1c34b5b01956227fa0228e49d76482be9d2f251a23c38c425a49ce957b001edd3c18504f37cbd9a341c0b` +- `d0420227266e3583a42dfaa0e38550d99f681d150dd18856f3af9a697bc9c5c8bf06b4b0fe5b9df0377d1b963574e2fd210a0a76a8b0756a65f640c602bebd07` authorizer world: ``` @@ -1286,7 +1286,7 @@ World { "check if \"aaabde\".matches(\"a*c?.e\")", "check if \"abcD12\" === \"abcD12\"", "check if \"abcD12\".length() === 6", - "check if \"hello world\".starts_with(\"hello\") && \"hello world\".ends_with(\"world\")", + "check if \"hello world\".starts_with(\"hello\"), \"hello world\".ends_with(\"world\")", "check if \"Ă©\".length() === 2", "check if 1 + 2 * 3 - 4 / 2 === 5", "check if 1 < 2", diff --git a/biscuit-auth/samples/samples.json b/biscuit-auth/samples/samples.json index f74aec19..8cdee2ca 100644 --- a/biscuit-auth/samples/samples.json +++ b/biscuit-auth/samples/samples.json @@ -1245,7 +1245,7 @@ ], "public_keys": [], "external_key": null, - "code": "check if true;\ncheck if !false;\ncheck if true === true;\ncheck if false === false;\ncheck if 1 < 2;\ncheck if 2 > 1;\ncheck if 1 <= 2;\ncheck if 1 <= 1;\ncheck if 2 >= 1;\ncheck if 2 >= 2;\ncheck if 3 === 3;\ncheck if 1 + 2 * 3 - 4 / 2 === 5;\ncheck if \"hello world\".starts_with(\"hello\") && \"hello world\".ends_with(\"world\");\ncheck if \"aaabde\".matches(\"a*c?.e\");\ncheck if \"aaabde\".contains(\"abd\");\ncheck if \"aaabde\" === \"aaa\" + \"b\" + \"de\";\ncheck if \"abcD12\" === \"abcD12\";\ncheck if \"abcD12\".length() === 6;\ncheck if \"Ă©\".length() === 2;\ncheck if 2019-12-04T09:46:41Z < 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z > 2019-12-04T09:46:41Z;\ncheck if 2019-12-04T09:46:41Z <= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2019-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z === 2020-12-04T09:46:41Z;\ncheck if hex:12ab === hex:12ab;\ncheck if {1, 2}.contains(2);\ncheck if {2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z}.contains(2020-12-04T09:46:41Z);\ncheck if {false, true}.contains(true);\ncheck if {\"abc\", \"def\"}.contains(\"abc\");\ncheck if {hex:12ab, hex:34de}.contains(hex:34de);\ncheck if {1, 2}.contains({2});\ncheck if {1, 2} === {1, 2};\ncheck if {1, 2}.intersection({2, 3}) === {2};\ncheck if {1, 2}.union({2, 3}) === {1, 2, 3};\ncheck if {1, 2, 3}.intersection({1, 2}).contains(1);\ncheck if {1, 2, 3}.intersection({1, 2}).length() === 2;\n" + "code": "check if true;\ncheck if !false;\ncheck if true === true;\ncheck if false === false;\ncheck if 1 < 2;\ncheck if 2 > 1;\ncheck if 1 <= 2;\ncheck if 1 <= 1;\ncheck if 2 >= 1;\ncheck if 2 >= 2;\ncheck if 3 === 3;\ncheck if 1 + 2 * 3 - 4 / 2 === 5;\ncheck if \"hello world\".starts_with(\"hello\"), \"hello world\".ends_with(\"world\");\ncheck if \"aaabde\".matches(\"a*c?.e\");\ncheck if \"aaabde\".contains(\"abd\");\ncheck if \"aaabde\" === \"aaa\" + \"b\" + \"de\";\ncheck if \"abcD12\" === \"abcD12\";\ncheck if \"abcD12\".length() === 6;\ncheck if \"Ă©\".length() === 2;\ncheck if 2019-12-04T09:46:41Z < 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z > 2019-12-04T09:46:41Z;\ncheck if 2019-12-04T09:46:41Z <= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2019-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z >= 2020-12-04T09:46:41Z;\ncheck if 2020-12-04T09:46:41Z === 2020-12-04T09:46:41Z;\ncheck if hex:12ab === hex:12ab;\ncheck if {1, 2}.contains(2);\ncheck if {2019-12-04T09:46:41Z, 2020-12-04T09:46:41Z}.contains(2020-12-04T09:46:41Z);\ncheck if {false, true}.contains(true);\ncheck if {\"abc\", \"def\"}.contains(\"abc\");\ncheck if {hex:12ab, hex:34de}.contains(hex:34de);\ncheck if {1, 2}.contains({2});\ncheck if {1, 2} === {1, 2};\ncheck if {1, 2}.intersection({2, 3}) === {2};\ncheck if {1, 2}.union({2, 3}) === {1, 2, 3};\ncheck if {1, 2, 3}.intersection({1, 2}).contains(1);\ncheck if {1, 2, 3}.intersection({1, 2}).length() === 2;\n" } ], "validations": { @@ -1263,7 +1263,7 @@ "check if \"aaabde\".matches(\"a*c?.e\")", "check if \"abcD12\" === \"abcD12\"", "check if \"abcD12\".length() === 6", - "check if \"hello world\".starts_with(\"hello\") && \"hello world\".ends_with(\"world\")", + "check if \"hello world\".starts_with(\"hello\"), \"hello world\".ends_with(\"world\")", "check if \"Ă©\".length() === 2", "check if 1 + 2 * 3 - 4 / 2 === 5", "check if 1 < 2", @@ -1307,7 +1307,7 @@ }, "authorizer_code": "allow if true;\n", "revocation_ids": [ - "a2640c5f1c1e86302e77422ccda6f7f02a21f63d88d1c34b5b01956227fa0228e49d76482be9d2f251a23c38c425a49ce957b001edd3c18504f37cbd9a341c0b" + "d0420227266e3583a42dfaa0e38550d99f681d150dd18856f3af9a697bc9c5c8bf06b4b0fe5b9df0377d1b963574e2fd210a0a76a8b0756a65f640c602bebd07" ] } } diff --git a/biscuit-auth/samples/test017_expressions.bc b/biscuit-auth/samples/test017_expressions.bc index e5a9c84344891ddac68836887354ee12760a1463..7be7a4fda92f3feecff08e8c3a68d1c53bea2100 100644 GIT binary patch delta 145 zcmbQwJDrzR=mQT|>qOQNM&^xC`HaR!Tn1cB9MV!ET&!Ho5)B++s*!_>MT&`o4OO%W zBFes*g~^=7;er#Bx>}xT^Ag=(3m&%y+?<~wE6RJZBkc3~S(()*kDl1iwq?V==(!)v hYo(`|mOT2a$i-E*VnbNkgDO#Ak-Pp)QD0RVhMT&`ojY~{P zLWg^2pxtL7_013G)DgXcg From bd698818cec408bb9f223006681b1a3ad680f2a2 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 8 Jun 2024 17:02:11 +0200 Subject: [PATCH 15/27] get operation for arrays --- biscuit-auth/src/datalog/expression.rs | 39 ++++++++++++++++++++++++++ biscuit-auth/src/format/convert.rs | 2 ++ biscuit-auth/src/format/schema.proto | 1 + biscuit-auth/src/format/schema.rs | 1 + biscuit-auth/src/token/builder.rs | 1 + biscuit-parser/src/builder.rs | 2 ++ biscuit-parser/src/parser.rs | 1 + 7 files changed, 47 insertions(+) diff --git a/biscuit-auth/src/datalog/expression.rs b/biscuit-auth/src/datalog/expression.rs index 3983c088..4722504d 100644 --- a/biscuit-auth/src/datalog/expression.rs +++ b/biscuit-auth/src/datalog/expression.rs @@ -4,6 +4,7 @@ use super::Term; use super::{SymbolTable, TemporarySymbolTable}; use regex::Regex; use std::collections::{HashMap, HashSet}; +use std::convert::TryFrom; #[derive(Debug, Clone, PartialEq, Hash, Eq)] pub struct Expression { @@ -90,6 +91,7 @@ pub enum Binary { LazyOr, All, Any, + Get, } impl Binary { @@ -317,6 +319,10 @@ impl Binary { } (Binary::Prefix, Term::Array(i), Term::Array(j)) => Ok(Term::Bool(i.starts_with(&j))), (Binary::Suffix, Term::Array(i), Term::Array(j)) => Ok(Term::Bool(i.ends_with(&j))), + (Binary::Get, Term::Array(i), Term::Integer(index)) => Ok(TryFrom::try_from(index) + .ok() + .and_then(|index: usize| i.get(index).cloned()) + .unwrap_or(Term::Null)), // map (Binary::Equal, Term::Map(i), Term::Map(j)) => Ok(Term::Bool(i == j)), @@ -364,6 +370,7 @@ impl Binary { Binary::LazyOr => format!("{left} || {right}"), Binary::All => format!("{left}.all({right})"), Binary::Any => format!("{left}.any({right})"), + Binary::Get => format!("{left}.get({right})"), } } } @@ -1154,5 +1161,37 @@ mod tests { let e = Expression { ops }; let res = e.evaluate(&values, &mut tmp_symbols); assert_eq!(res, Ok(Term::Bool(false))); + + // array get + let ops = vec![ + Op::Value(Term::Array(vec![ + Term::Integer(0), + Term::Integer(1), + Term::Integer(2), + ])), + Op::Value(Term::Integer(1)), + Op::Binary(Binary::Get), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Integer(1))); + + // array get out of bounds + let ops = vec![ + Op::Value(Term::Array(vec![ + Term::Integer(0), + Term::Integer(1), + Term::Integer(2), + ])), + Op::Value(Term::Integer(3)), + Op::Binary(Binary::Get), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Null)); } } diff --git a/biscuit-auth/src/format/convert.rs b/biscuit-auth/src/format/convert.rs index 979febb5..891cbff1 100644 --- a/biscuit-auth/src/format/convert.rs +++ b/biscuit-auth/src/format/convert.rs @@ -729,6 +729,7 @@ pub mod v2 { Binary::LazyOr => Kind::LazyOr, Binary::All => Kind::All, Binary::Any => Kind::Any, + Binary::Get => Kind::Get, } as i32, }) } @@ -793,6 +794,7 @@ pub mod v2 { Some(op_binary::Kind::LazyOr) => Op::Binary(Binary::LazyOr), Some(op_binary::Kind::All) => Op::Binary(Binary::All), Some(op_binary::Kind::Any) => Op::Binary(Binary::Any), + Some(op_binary::Kind::Get) => Op::Binary(Binary::Get), None => { return Err(error::Format::DeserializationError( "deserialization error: binary operation is empty".to_string(), diff --git a/biscuit-auth/src/format/schema.proto b/biscuit-auth/src/format/schema.proto index 4b9fe2bd..dc7fa0b9 100644 --- a/biscuit-auth/src/format/schema.proto +++ b/biscuit-auth/src/format/schema.proto @@ -180,6 +180,7 @@ message OpBinary { LazyOr = 24; All = 25; Any = 26; + Get = 27; } required Kind kind = 1; diff --git a/biscuit-auth/src/format/schema.rs b/biscuit-auth/src/format/schema.rs index 5a2d820f..8ad23ff5 100644 --- a/biscuit-auth/src/format/schema.rs +++ b/biscuit-auth/src/format/schema.rs @@ -281,6 +281,7 @@ pub mod op_binary { LazyOr = 24, All = 25, Any = 26, + Get = 27, } } #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/biscuit-auth/src/token/builder.rs b/biscuit-auth/src/token/builder.rs index b6d05f07..2d3fe542 100644 --- a/biscuit-auth/src/token/builder.rs +++ b/biscuit-auth/src/token/builder.rs @@ -1143,6 +1143,7 @@ impl From for Binary { biscuit_parser::builder::Binary::LazyOr => Binary::LazyOr, biscuit_parser::builder::Binary::All => Binary::All, biscuit_parser::builder::Binary::Any => Binary::Any, + biscuit_parser::builder::Binary::Get => Binary::Get, } } } diff --git a/biscuit-parser/src/builder.rs b/biscuit-parser/src/builder.rs index 4c3b8f78..ea18e95f 100644 --- a/biscuit-parser/src/builder.rs +++ b/biscuit-parser/src/builder.rs @@ -298,6 +298,7 @@ pub enum Binary { LazyOr, All, Any, + Get, } #[cfg(feature = "datalog-macro")] @@ -363,6 +364,7 @@ impl ToTokens for Binary { Binary::LazyOr => quote! { ::biscuit_auth::datalog::Binary::LazyOr }, Binary::All => quote! { ::biscuit_auth::datalog::Binary::All }, Binary::Any => quote! { ::biscuit_auth::datalog::Binary::Any }, + Binary::Get => quote! { ::biscuit_auth::datalog::Binary::Get }, }); } } diff --git a/biscuit-parser/src/parser.rs b/biscuit-parser/src/parser.rs index 4a0957a1..8e64e311 100644 --- a/biscuit-parser/src/parser.rs +++ b/biscuit-parser/src/parser.rs @@ -509,6 +509,7 @@ fn binary_op_8(i: &str) -> IResult<&str, builder::Binary, Error> { value(Binary::Union, tag("union")), value(Binary::All, tag("all")), value(Binary::Any, tag("any")), + value(Binary::Get, tag("get")), ))(i) } From 1f0afa1a1d1d73b53b57d522ef1521e558e74905 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 8 Jun 2024 17:08:46 +0200 Subject: [PATCH 16/27] allow heterogeneous arrays --- biscuit-auth/src/format/convert.rs | 47 ++++-------------------------- biscuit-parser/src/parser.rs | 42 +------------------------- 2 files changed, 6 insertions(+), 83 deletions(-) diff --git a/biscuit-auth/src/format/convert.rs b/biscuit-auth/src/format/convert.rs index 891cbff1..3798fc45 100644 --- a/biscuit-auth/src/format/convert.rs +++ b/biscuit-auth/src/format/convert.rs @@ -616,48 +616,11 @@ pub mod v2 { } Some(Content::Null(_)) => Ok(Term::Null), Some(Content::Array(a)) => { - let mut kind: Option = None; - let mut array = Vec::new(); - - for term in a.array.iter() { - // FIXME: this is not a recursive type check: an array of arrays or array - // of sets could have different types one level down - let index = match term.content { - Some(Content::Variable(_)) => { - return Err(error::Format::DeserializationError( - "deserialization error: arrays cannot contain variables" - .to_string(), - )); - } - Some(Content::Integer(_)) => 2, - Some(Content::String(_)) => 3, - Some(Content::Date(_)) => 4, - Some(Content::Bytes(_)) => 5, - Some(Content::Bool(_)) => 6, - Some(Content::Set(_)) => 7, - Some(Content::Null(_)) => 8, - Some(Content::Array(_)) => 9, - Some(Content::Map(_)) => 10, - None => { - return Err(error::Format::DeserializationError( - "deserialization error: ID content enum is empty".to_string(), - )) - } - }; - - if let Some(k) = kind.as_ref() { - if *k != index { - return Err(error::Format::DeserializationError( - "deserialization error: array elements must have the same type" - .to_string(), - )); - } - } else { - kind = Some(index); - } - - array.push(proto_id_to_token_term(term)?); - } + let array = a + .array + .iter() + .map(proto_id_to_token_term) + .collect::>()?; Ok(Term::Array(array)) } diff --git a/biscuit-parser/src/parser.rs b/biscuit-parser/src/parser.rs index 8e64e311..20fd9130 100644 --- a/biscuit-parser/src/parser.rs +++ b/biscuit-parser/src/parser.rs @@ -892,47 +892,7 @@ fn set(i: &str) -> IResult<&str, builder::Term, Error> { fn array(i: &str) -> IResult<&str, builder::Term, Error> { let (i, _) = preceded(space0, char('['))(i)?; - let (i, mut list) = cut(separated_list0(preceded(space0, char(',')), term_in_fact))(i)?; - - let mut array = Vec::new(); - - let mut kind: Option = None; - for term in list.drain(..) { - let index = match term { - builder::Term::Variable(_) => { - return Err(nom::Err::Failure(Error { - input: i, - code: ErrorKind::Fail, - message: Some("variables are not permitted in arrays".to_string()), - })) - } - builder::Term::Integer(_) => 2, - builder::Term::Str(_) => 3, - builder::Term::Date(_) => 4, - builder::Term::Bytes(_) => 5, - builder::Term::Bool(_) => 6, - builder::Term::Set(_) => 7, - builder::Term::Parameter(_) => 8, - builder::Term::Null => 9, - builder::Term::Array(_) => 10, - builder::Term::Map(_) => 11, - }; - - if let Some(k) = kind { - if k != index { - return Err(nom::Err::Failure(Error { - input: i, - code: ErrorKind::Fail, - message: Some("array elements must have the same type".to_string()), - })); - } - } else { - kind = Some(index); - } - - array.push(term); - } - + let (i, array) = cut(separated_list0(preceded(space0, char(',')), term_in_fact))(i)?; let (i, _) = preceded(space0, char(']'))(i)?; Ok((i, builder::array(array))) From e627761e6e058c6d61d1e6b16355b86a5836139b Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 8 Jun 2024 17:24:34 +0200 Subject: [PATCH 17/27] samples for array get --- biscuit-auth/examples/testcases.rs | 8 +++++--- biscuit-auth/samples/README.md | 16 +++++++-------- biscuit-auth/samples/samples.json | 14 +++++-------- biscuit-auth/samples/test033_array_map.bc | Bin 924 -> 837 bytes biscuit-auth/src/datalog/expression.rs | 23 +++++++++++++++------- 5 files changed, 33 insertions(+), 28 deletions(-) diff --git a/biscuit-auth/examples/testcases.rs b/biscuit-auth/examples/testcases.rs index 053acba9..0219d205 100644 --- a/biscuit-auth/examples/testcases.rs +++ b/biscuit-auth/examples/testcases.rs @@ -2148,11 +2148,13 @@ fn array_map(target: &str, root: &KeyPair, test: bool) -> TestResult { check if ["a", "b", "c"].contains("c"); check if [1, 2, 3].starts_with([1, 2]); check if [4, 5, 6 ].ends_with([6]); - check if [1,2,3].all($p -> $p > 0); + //check if [1,2,3].all($p -> $p > 0); + check if [1,2, "a"].get(2) == "a"; + check if [1, 2].get(3) == null; // all - check if ![1,2,3].all($p -> $p == 2); + //check if ![1,2,3].all($p -> $p == 2); // any - check if [1,2,3].any($p -> $p > 2); + //check if [1,2,3].any($p -> $p > 2); // map check if { "a": 1 , "b": 2, "c": 3, "d": 4}.length() == 4; check if { 1: "a" , 2: "b"} != { "a": 1 , "b": 2}; diff --git a/biscuit-auth/samples/README.md b/biscuit-auth/samples/README.md index e7ce526c..a7d48123 100644 --- a/biscuit-auth/samples/README.md +++ b/biscuit-auth/samples/README.md @@ -2873,7 +2873,7 @@ result: `Err(Execution(InvalidType))` ### token authority: -symbols: ["a", "b", "c", "p", "d"] +symbols: ["a", "b", "c", "d"] public keys: [] @@ -2884,9 +2884,8 @@ check if ["a", "b"] == ["a", "b"]; check if ["a", "b", "c"].contains("c"); check if [1, 2, 3].starts_with([1, 2]); check if [4, 5, 6].ends_with([6]); -check if [1, 2, 3].all($p -> $p > 0); -check if ![1, 2, 3].all($p -> $p == 2); -check if [1, 2, 3].any($p -> $p > 2); +check if [1, 2, "a"].get(2) == "a"; +check if [1, 2].get(3) == null; check if {"a": 1, "b": 2, "c": 3, "d": 4}.length() == 4; check if {1: "a", 2: "b"} != {"a": 1, "b": 2}; check if {1: "a", 2: "b"} == {1: "a", 2: "b"}; @@ -2901,7 +2900,7 @@ allow if true; ``` revocation ids: -- `f027c1e0b86bd3d53bbfffead4e61d71e907b58e0b0ec339c0bc2bdcab21aea4fb4c45643934816608533bc818c9ed14dc31d058021cbe4cec8dde5e519f0e07` +- `7ef81713cb8486b0a89f7dcfad4bd3d632eb42a67c9d8f6383c1882ce7808ba570fddf09132e81c499d2869bf6283b65a610dd0fb435f918ae6e08743ebd8604` authorizer world: ``` @@ -2914,14 +2913,13 @@ World { 0, ), checks: [ - "check if ![1, 2, 3].all($p -> $p == 2)", "check if [\"a\", \"b\", \"c\"].contains(\"c\")", "check if [\"a\", \"b\"] != [1, 2, 3]", "check if [\"a\", \"b\"] == [\"a\", \"b\"]", + "check if [1, 2, \"a\"].get(2) == \"a\"", "check if [1, 2, 1].length() == 3", - "check if [1, 2, 3].all($p -> $p > 0)", - "check if [1, 2, 3].any($p -> $p > 2)", "check if [1, 2, 3].starts_with([1, 2])", + "check if [1, 2].get(3) == null", "check if [4, 5, 6].ends_with([6])", "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\")", "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4", @@ -2936,5 +2934,5 @@ World { } ``` -result: `Err(Execution(InvalidType))` +result: `Ok(0)` diff --git a/biscuit-auth/samples/samples.json b/biscuit-auth/samples/samples.json index 8cdee2ca..28a67dda 100644 --- a/biscuit-auth/samples/samples.json +++ b/biscuit-auth/samples/samples.json @@ -2699,12 +2699,11 @@ "a", "b", "c", - "p", "d" ], "public_keys": [], "external_key": null, - "code": "check if [1, 2, 1].length() == 3;\ncheck if [\"a\", \"b\"] != [1, 2, 3];\ncheck if [\"a\", \"b\"] == [\"a\", \"b\"];\ncheck if [\"a\", \"b\", \"c\"].contains(\"c\");\ncheck if [1, 2, 3].starts_with([1, 2]);\ncheck if [4, 5, 6].ends_with([6]);\ncheck if [1, 2, 3].all($p -> $p > 0);\ncheck if ![1, 2, 3].all($p -> $p == 2);\ncheck if [1, 2, 3].any($p -> $p > 2);\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4;\ncheck if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2};\ncheck if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"};\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\");\n" + "code": "check if [1, 2, 1].length() == 3;\ncheck if [\"a\", \"b\"] != [1, 2, 3];\ncheck if [\"a\", \"b\"] == [\"a\", \"b\"];\ncheck if [\"a\", \"b\", \"c\"].contains(\"c\");\ncheck if [1, 2, 3].starts_with([1, 2]);\ncheck if [4, 5, 6].ends_with([6]);\ncheck if [1, 2, \"a\"].get(2) == \"a\";\ncheck if [1, 2].get(3) == null;\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4;\ncheck if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2};\ncheck if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"};\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\");\n" } ], "validations": { @@ -2716,14 +2715,13 @@ { "origin": 0, "checks": [ - "check if ![1, 2, 3].all($p -> $p == 2)", "check if [\"a\", \"b\", \"c\"].contains(\"c\")", "check if [\"a\", \"b\"] != [1, 2, 3]", "check if [\"a\", \"b\"] == [\"a\", \"b\"]", + "check if [1, 2, \"a\"].get(2) == \"a\"", "check if [1, 2, 1].length() == 3", - "check if [1, 2, 3].all($p -> $p > 0)", - "check if [1, 2, 3].any($p -> $p > 2)", "check if [1, 2, 3].starts_with([1, 2])", + "check if [1, 2].get(3) == null", "check if [4, 5, 6].ends_with([6])", "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\")", "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4", @@ -2737,13 +2735,11 @@ ] }, "result": { - "Err": { - "Execution": "InvalidType" - } + "Ok": 0 }, "authorizer_code": "allow if true;\n", "revocation_ids": [ - "f027c1e0b86bd3d53bbfffead4e61d71e907b58e0b0ec339c0bc2bdcab21aea4fb4c45643934816608533bc818c9ed14dc31d058021cbe4cec8dde5e519f0e07" + "7ef81713cb8486b0a89f7dcfad4bd3d632eb42a67c9d8f6383c1882ce7808ba570fddf09132e81c499d2869bf6283b65a610dd0fb435f918ae6e08743ebd8604" ] } } diff --git a/biscuit-auth/samples/test033_array_map.bc b/biscuit-auth/samples/test033_array_map.bc index 9e15ee0c47643a499528ae002aae40b97a468c34..24b8652cbb6e985c5ee03402af33d1e8c698ffcf 100644 GIT binary patch delta 222 zcmbQkew2-0XdWBaW>zl7L@vf8F2>}ILUN3$CR|2bOdQfuI$VNW{9e3VOahER%*4ek z(ZIpQ0%o#E0TpnuLWO`FQ6oJr9k4DnE*>r}FAlgakOpQhhz20ri2=+Oo&1u~jInvL z0@GcdW)2}H0T!@n&67Eq^&RSdhzp-?Y1^=3e(m|S-j}Z#y>?nwGq*pv`Cy06^M>xF q1%K~z3hOllKCQ Ok(Term::Bool(true)), (Binary::HeterogeneousNotEqual, _, Term::Null) => Ok(Term::Bool(true)), - (Binary::HeterogeneousEqual, _, _) => Ok(Term::Bool(false)), - (Binary::HeterogeneousNotEqual, _, _) => Ok(Term::Bool(true)), - // array - (Binary::Equal, Term::Array(i), Term::Array(j)) => Ok(Term::Bool(i == j)), - (Binary::NotEqual, Term::Array(i), Term::Array(j)) => Ok(Term::Bool(i != j)), + (Binary::Equal | Binary::HeterogeneousEqual, Term::Array(i), Term::Array(j)) => { + Ok(Term::Bool(i == j)) + } + (Binary::NotEqual | Binary::HeterogeneousNotEqual, Term::Array(i), Term::Array(j)) => { + Ok(Term::Bool(i != j)) + } (Binary::Contains, Term::Array(i), j) => { Ok(Term::Bool(i.iter().any(|elem| elem == &j))) } @@ -325,8 +326,12 @@ impl Binary { .unwrap_or(Term::Null)), // map - (Binary::Equal, Term::Map(i), Term::Map(j)) => Ok(Term::Bool(i == j)), - (Binary::NotEqual, Term::Map(i), Term::Map(j)) => Ok(Term::Bool(i != j)), + (Binary::Equal | Binary::HeterogeneousEqual, Term::Map(i), Term::Map(j)) => { + Ok(Term::Bool(i == j)) + } + (Binary::NotEqual | Binary::HeterogeneousNotEqual, Term::Map(i), Term::Map(j)) => { + Ok(Term::Bool(i != j)) + } (Binary::Contains, Term::Map(i), j) => { Ok(Term::Bool(i.iter().any(|elem| match (elem.0, &j) { (super::MapKey::Integer(k), Term::Integer(l)) => k == l, @@ -334,6 +339,10 @@ impl Binary { _ => false, }))) } + + (Binary::HeterogeneousEqual, _, _) => Ok(Term::Bool(false)), + (Binary::HeterogeneousNotEqual, _, _) => Ok(Term::Bool(true)), + _ => { //println!("unexpected value type on the stack"); Err(error::Expression::InvalidType) From 71a336a5fd5c73252f367281e8f0a51893c52a70 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 8 Jun 2024 17:36:57 +0200 Subject: [PATCH 18/27] array any and all operations --- biscuit-auth/examples/testcases.rs | 7 ++---- biscuit-auth/samples/README.md | 10 +++++--- biscuit-auth/samples/samples.json | 10 +++++--- biscuit-auth/src/datalog/expression.rs | 33 ++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/biscuit-auth/examples/testcases.rs b/biscuit-auth/examples/testcases.rs index 0219d205..d7411621 100644 --- a/biscuit-auth/examples/testcases.rs +++ b/biscuit-auth/examples/testcases.rs @@ -2148,13 +2148,10 @@ fn array_map(target: &str, root: &KeyPair, test: bool) -> TestResult { check if ["a", "b", "c"].contains("c"); check if [1, 2, 3].starts_with([1, 2]); check if [4, 5, 6 ].ends_with([6]); - //check if [1,2,3].all($p -> $p > 0); check if [1,2, "a"].get(2) == "a"; check if [1, 2].get(3) == null; - // all - //check if ![1,2,3].all($p -> $p == 2); - // any - //check if [1,2,3].any($p -> $p > 2); + check if [1,2,3].all($p -> $p > 0); + check if [1,2,3].any($p -> $p > 2); // map check if { "a": 1 , "b": 2, "c": 3, "d": 4}.length() == 4; check if { 1: "a" , 2: "b"} != { "a": 1 , "b": 2}; diff --git a/biscuit-auth/samples/README.md b/biscuit-auth/samples/README.md index a7d48123..b20ac14e 100644 --- a/biscuit-auth/samples/README.md +++ b/biscuit-auth/samples/README.md @@ -2864,7 +2864,7 @@ World { } ``` -result: `Err(Execution(InvalidType))` +result: `Err(Execution(ShadowedVariable))` ------------------------------ @@ -2873,7 +2873,7 @@ result: `Err(Execution(InvalidType))` ### token authority: -symbols: ["a", "b", "c", "d"] +symbols: ["a", "b", "c", "p", "d"] public keys: [] @@ -2886,6 +2886,8 @@ check if [1, 2, 3].starts_with([1, 2]); check if [4, 5, 6].ends_with([6]); check if [1, 2, "a"].get(2) == "a"; check if [1, 2].get(3) == null; +check if [1, 2, 3].all($p -> $p > 0); +check if [1, 2, 3].any($p -> $p > 2); check if {"a": 1, "b": 2, "c": 3, "d": 4}.length() == 4; check if {1: "a", 2: "b"} != {"a": 1, "b": 2}; check if {1: "a", 2: "b"} == {1: "a", 2: "b"}; @@ -2900,7 +2902,7 @@ allow if true; ``` revocation ids: -- `7ef81713cb8486b0a89f7dcfad4bd3d632eb42a67c9d8f6383c1882ce7808ba570fddf09132e81c499d2869bf6283b65a610dd0fb435f918ae6e08743ebd8604` +- `ff26323a584a8ec8af2f81b80fefc2d6ea84148f463a4a007386f48eb2a0125f54812ab9da60a82642f46d832939ae8de5bfed20dfd683044f76880092598609` authorizer world: ``` @@ -2918,6 +2920,8 @@ World { "check if [\"a\", \"b\"] == [\"a\", \"b\"]", "check if [1, 2, \"a\"].get(2) == \"a\"", "check if [1, 2, 1].length() == 3", + "check if [1, 2, 3].all($p -> $p > 0)", + "check if [1, 2, 3].any($p -> $p > 2)", "check if [1, 2, 3].starts_with([1, 2])", "check if [1, 2].get(3) == null", "check if [4, 5, 6].ends_with([6])", diff --git a/biscuit-auth/samples/samples.json b/biscuit-auth/samples/samples.json index 28a67dda..ad28b63c 100644 --- a/biscuit-auth/samples/samples.json +++ b/biscuit-auth/samples/samples.json @@ -2680,7 +2680,7 @@ }, "result": { "Err": { - "Execution": "InvalidType" + "Execution": "ShadowedVariable" } }, "authorizer_code": "allow if [true].any($p -> [true].all($p -> $p));\n", @@ -2699,11 +2699,12 @@ "a", "b", "c", + "p", "d" ], "public_keys": [], "external_key": null, - "code": "check if [1, 2, 1].length() == 3;\ncheck if [\"a\", \"b\"] != [1, 2, 3];\ncheck if [\"a\", \"b\"] == [\"a\", \"b\"];\ncheck if [\"a\", \"b\", \"c\"].contains(\"c\");\ncheck if [1, 2, 3].starts_with([1, 2]);\ncheck if [4, 5, 6].ends_with([6]);\ncheck if [1, 2, \"a\"].get(2) == \"a\";\ncheck if [1, 2].get(3) == null;\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4;\ncheck if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2};\ncheck if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"};\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\");\n" + "code": "check if [1, 2, 1].length() == 3;\ncheck if [\"a\", \"b\"] != [1, 2, 3];\ncheck if [\"a\", \"b\"] == [\"a\", \"b\"];\ncheck if [\"a\", \"b\", \"c\"].contains(\"c\");\ncheck if [1, 2, 3].starts_with([1, 2]);\ncheck if [4, 5, 6].ends_with([6]);\ncheck if [1, 2, 3].all($p -> $p > 0);\ncheck if [1, 2, \"a\"].get(2) == \"a\";\ncheck if [1, 2].get(3) == null;\ncheck if [1, 2, 3].all($p -> $p > 0);\ncheck if [1, 2, 3].any($p -> $p > 2);\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4;\ncheck if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2};\ncheck if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"};\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\");\n" } ], "validations": { @@ -2720,6 +2721,9 @@ "check if [\"a\", \"b\"] == [\"a\", \"b\"]", "check if [1, 2, \"a\"].get(2) == \"a\"", "check if [1, 2, 1].length() == 3", + "check if [1, 2, 3].all($p -> $p > 0)", + "check if [1, 2, 3].all($p -> $p > 0)", + "check if [1, 2, 3].any($p -> $p > 2)", "check if [1, 2, 3].starts_with([1, 2])", "check if [1, 2].get(3) == null", "check if [4, 5, 6].ends_with([6])", @@ -2739,7 +2743,7 @@ }, "authorizer_code": "allow if true;\n", "revocation_ids": [ - "7ef81713cb8486b0a89f7dcfad4bd3d632eb42a67c9d8f6383c1882ce7808ba570fddf09132e81c499d2869bf6283b65a610dd0fb435f918ae6e08743ebd8604" + "00efb9846891354b420fd165e9162e65398af9ec7361b584d312964879b3418e049ff1a39cde0e90dd8f8f9ff24b689806139b40e5a3a44f1ccee1115a64f108" ] } } diff --git a/biscuit-auth/src/datalog/expression.rs b/biscuit-auth/src/datalog/expression.rs index ad6548d5..bd050e0d 100644 --- a/biscuit-auth/src/datalog/expression.rs +++ b/biscuit-auth/src/datalog/expression.rs @@ -104,6 +104,7 @@ impl Binary { symbols: &mut TemporarySymbolTable, ) -> Result { match (self, left, params) { + // boolean (Binary::LazyOr, Term::Bool(true), []) => Ok(Term::Bool(true)), (Binary::LazyOr, Term::Bool(false), []) => { let e = Expression { ops: right.clone() }; @@ -114,6 +115,8 @@ impl Binary { let e = Expression { ops: right.clone() }; e.evaluate(values, symbols) } + + // set (Binary::All, Term::Set(set_values), [param]) => { for value in set_values.iter() { values.insert(*param, value.clone()); @@ -142,6 +145,36 @@ impl Binary { } Ok(Term::Bool(false)) } + + // array + (Binary::All, Term::Array(set_values), [param]) => { + for value in set_values.iter() { + values.insert(*param, value.clone()); + let e = Expression { ops: right.clone() }; + let result = e.evaluate(values, symbols); + values.remove(param); + match result? { + Term::Bool(true) => {} + Term::Bool(false) => return Ok(Term::Bool(false)), + _ => return Err(error::Expression::InvalidType), + }; + } + Ok(Term::Bool(true)) + } + (Binary::Any, Term::Array(set_values), [param]) => { + for value in set_values.iter() { + values.insert(*param, value.clone()); + let e = Expression { ops: right.clone() }; + let result = e.evaluate(values, symbols); + values.remove(param); + match result? { + Term::Bool(false) => {} + Term::Bool(true) => return Ok(Term::Bool(true)), + _ => return Err(error::Expression::InvalidType), + }; + } + Ok(Term::Bool(false)) + } (_, _, _) => Err(error::Expression::InvalidType), } } From 72161458a10211b568095a9ad746b3e9efd0bff1 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 8 Jun 2024 17:37:58 +0200 Subject: [PATCH 19/27] more array tests --- biscuit-auth/src/datalog/expression.rs | 55 ++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/biscuit-auth/src/datalog/expression.rs b/biscuit-auth/src/datalog/expression.rs index bd050e0d..ff98a4d0 100644 --- a/biscuit-auth/src/datalog/expression.rs +++ b/biscuit-auth/src/datalog/expression.rs @@ -1235,5 +1235,60 @@ mod tests { let e = Expression { ops }; let res = e.evaluate(&values, &mut tmp_symbols); assert_eq!(res, Ok(Term::Null)); + + // array get out of bounds + let ops = vec![ + Op::Value(Term::Array(vec![ + Term::Integer(0), + Term::Integer(1), + Term::Integer(2), + ])), + Op::Value(Term::Integer(3)), + Op::Binary(Binary::Get), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Null)); + + // array all + let p = tmp_symbols.insert("param") as u32; + let ops1 = vec![ + Op::Value(Term::Array([Term::Integer(1), Term::Integer(2)].into())), + Op::Closure( + vec![p], + vec![ + Op::Value(Term::Variable(p)), + Op::Value(Term::Integer(0)), + Op::Binary(Binary::GreaterThan), + ], + ), + Op::Binary(Binary::All), + ]; + let e1 = Expression { ops: ops1 }; + println!("{:?}", e1.print(&symbols)); + + let res1 = e1.evaluate(&HashMap::new(), &mut tmp_symbols).unwrap(); + assert_eq!(res1, Term::Bool(true)); + + // array any + let ops1 = vec![ + Op::Value(Term::Array([Term::Integer(1), Term::Integer(2)].into())), + Op::Closure( + vec![p], + vec![ + Op::Value(Term::Variable(p)), + Op::Value(Term::Integer(0)), + Op::Binary(Binary::Equal), + ], + ), + Op::Binary(Binary::Any), + ]; + let e1 = Expression { ops: ops1 }; + println!("{:?}", e1.print(&symbols)); + + let res1 = e1.evaluate(&HashMap::new(), &mut tmp_symbols).unwrap(); + assert_eq!(res1, Term::Bool(false)); } } From 6e69b3ea065dfe0cc9e1911d2c7d692ee88f5758 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 8 Jun 2024 17:52:18 +0200 Subject: [PATCH 20/27] tests for maps --- biscuit-auth/src/datalog/expression.rs | 209 +++++++++++++++++++++++-- 1 file changed, 193 insertions(+), 16 deletions(-) diff --git a/biscuit-auth/src/datalog/expression.rs b/biscuit-auth/src/datalog/expression.rs index ff98a4d0..cfa8d2cb 100644 --- a/biscuit-auth/src/datalog/expression.rs +++ b/biscuit-auth/src/datalog/expression.rs @@ -548,7 +548,7 @@ mod tests { use std::collections::BTreeSet; use super::*; - use crate::datalog::{SymbolTable, TemporarySymbolTable}; + use crate::datalog::{MapKey, SymbolTable, TemporarySymbolTable}; #[test] fn negate() { @@ -1204,7 +1204,7 @@ mod tests { let res = e.evaluate(&values, &mut tmp_symbols); assert_eq!(res, Ok(Term::Bool(false))); - // array get + // get let ops = vec![ Op::Value(Term::Array(vec![ Term::Integer(0), @@ -1220,7 +1220,7 @@ mod tests { let res = e.evaluate(&values, &mut tmp_symbols); assert_eq!(res, Ok(Term::Integer(1))); - // array get out of bounds + // get out of bounds let ops = vec![ Op::Value(Term::Array(vec![ Term::Integer(0), @@ -1236,14 +1236,171 @@ mod tests { let res = e.evaluate(&values, &mut tmp_symbols); assert_eq!(res, Ok(Term::Null)); - // array get out of bounds + // all + let p = tmp_symbols.insert("param") as u32; + let ops1 = vec![ + Op::Value(Term::Array([Term::Integer(1), Term::Integer(2)].into())), + Op::Closure( + vec![p], + vec![ + Op::Value(Term::Variable(p)), + Op::Value(Term::Integer(0)), + Op::Binary(Binary::GreaterThan), + ], + ), + Op::Binary(Binary::All), + ]; + let e1 = Expression { ops: ops1 }; + println!("{:?}", e1.print(&symbols)); + + let res1 = e1.evaluate(&HashMap::new(), &mut tmp_symbols).unwrap(); + assert_eq!(res1, Term::Bool(true)); + + // any + let ops1 = vec![ + Op::Value(Term::Array([Term::Integer(1), Term::Integer(2)].into())), + Op::Closure( + vec![p], + vec![ + Op::Value(Term::Variable(p)), + Op::Value(Term::Integer(0)), + Op::Binary(Binary::Equal), + ], + ), + Op::Binary(Binary::Any), + ]; + let e1 = Expression { ops: ops1 }; + println!("{:?}", e1.print(&symbols)); + + let res1 = e1.evaluate(&HashMap::new(), &mut tmp_symbols).unwrap(); + assert_eq!(res1, Term::Bool(false)); + } + + #[test] + fn map() { + let symbols = SymbolTable::new(); + let mut tmp_symbols = TemporarySymbolTable::new(&symbols); let ops = vec![ - Op::Value(Term::Array(vec![ - Term::Integer(0), - Term::Integer(1), - Term::Integer(2), - ])), - Op::Value(Term::Integer(3)), + Op::Value(Term::Map( + [ + (MapKey::Str(1), Term::Integer(0)), + (MapKey::Str(2), Term::Integer(1)), + ] + .iter() + .cloned() + .collect(), + )), + Op::Value(Term::Map( + [ + (MapKey::Str(2), Term::Integer(1)), + (MapKey::Str(1), Term::Integer(0)), + ] + .iter() + .cloned() + .collect(), + )), + Op::Binary(Binary::Equal), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(true))); + + let ops = vec![ + Op::Value(Term::Map( + [ + (MapKey::Str(1), Term::Integer(0)), + (MapKey::Str(2), Term::Integer(1)), + ] + .iter() + .cloned() + .collect(), + )), + Op::Value(Term::Map( + [(MapKey::Str(1), Term::Integer(0))] + .iter() + .cloned() + .collect(), + )), + Op::Binary(Binary::Equal), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(false))); + + let ops = vec![ + Op::Value(Term::Map( + [ + (MapKey::Str(1), Term::Integer(0)), + (MapKey::Str(2), Term::Integer(1)), + ] + .iter() + .cloned() + .collect(), + )), + Op::Value(Term::Str(1)), + Op::Binary(Binary::Contains), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(true))); + + let ops = vec![ + Op::Value(Term::Map( + [ + (MapKey::Str(1), Term::Integer(0)), + (MapKey::Str(2), Term::Integer(1)), + ] + .iter() + .cloned() + .collect(), + )), + Op::Value(Term::Integer(0)), + Op::Binary(Binary::Contains), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Bool(false))); + + // get + let ops = vec![ + Op::Value(Term::Map( + [ + (MapKey::Str(1), Term::Integer(0)), + (MapKey::Str(2), Term::Integer(1)), + ] + .iter() + .cloned() + .collect(), + )), + Op::Value(Term::Str(2)), + Op::Binary(Binary::Get), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Integer(1))); + + // get non existing key + let ops = vec![ + Op::Value(Term::Map( + [ + (MapKey::Str(1), Term::Integer(0)), + (MapKey::Str(2), Term::Integer(1)), + ] + .iter() + .cloned() + .collect(), + )), + Op::Value(Term::Integer(0)), Op::Binary(Binary::Get), ]; @@ -1252,16 +1409,26 @@ mod tests { let res = e.evaluate(&values, &mut tmp_symbols); assert_eq!(res, Ok(Term::Null)); - // array all + // all let p = tmp_symbols.insert("param") as u32; let ops1 = vec![ - Op::Value(Term::Array([Term::Integer(1), Term::Integer(2)].into())), + Op::Value(Term::Map( + [ + (MapKey::Str(1), Term::Integer(0)), + (MapKey::Str(2), Term::Integer(1)), + ] + .iter() + .cloned() + .collect(), + )), Op::Closure( vec![p], vec![ Op::Value(Term::Variable(p)), - Op::Value(Term::Integer(0)), - Op::Binary(Binary::GreaterThan), + Op::Value(Term::Integer(1)), + Op::Binary(Binary::Get), + Op::Value(Term::Integer(2)), + Op::Binary(Binary::LessThan), ], ), Op::Binary(Binary::All), @@ -1272,14 +1439,24 @@ mod tests { let res1 = e1.evaluate(&HashMap::new(), &mut tmp_symbols).unwrap(); assert_eq!(res1, Term::Bool(true)); - // array any + // any let ops1 = vec![ - Op::Value(Term::Array([Term::Integer(1), Term::Integer(2)].into())), + Op::Value(Term::Map( + [ + (MapKey::Str(1), Term::Integer(0)), + (MapKey::Str(2), Term::Integer(1)), + ] + .iter() + .cloned() + .collect(), + )), Op::Closure( vec![p], vec![ Op::Value(Term::Variable(p)), Op::Value(Term::Integer(0)), + Op::Binary(Binary::Get), + Op::Value(Term::Str(1)), Op::Binary(Binary::Equal), ], ), From ca1636db726f955d7a6b8a318417fa47d42383b2 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 8 Jun 2024 18:37:40 +0200 Subject: [PATCH 21/27] get, all and any operations for maps --- biscuit-auth/examples/testcases.rs | 6 ++ biscuit-auth/samples/README.md | 16 +++- biscuit-auth/samples/samples.json | 15 ++- biscuit-auth/samples/test033_array_map.bc | Bin 837 -> 1523 bytes biscuit-auth/src/datalog/expression.rs | 107 ++++++++++++++++++++-- 5 files changed, 128 insertions(+), 16 deletions(-) diff --git a/biscuit-auth/examples/testcases.rs b/biscuit-auth/examples/testcases.rs index d7411621..af4d1537 100644 --- a/biscuit-auth/examples/testcases.rs +++ b/biscuit-auth/examples/testcases.rs @@ -2157,6 +2157,12 @@ fn array_map(target: &str, root: &KeyPair, test: bool) -> TestResult { check if { 1: "a" , 2: "b"} != { "a": 1 , "b": 2}; check if { 1: "a" , 2: "b"} == { 2: "b", 1: "a" }; check if { "a": 1 , "b": 2, "c": 3, "d": 4}.contains("d"); + check if { "a": 1 , "b": 2, 1: "A" }.get("a") == 1; + check if { "a": 1 , "b": 2, 1: "A" }.get(1) == "A"; + check if { "a": 1 , "b": 2, 1: "A" }.get("c") == null; + check if { "a": 1 , "b": 2, 1: "A" }.get(2) == null; + check if { "a": 1 , "b": 2 }.all($kv -> $kv.get(0) != "c" && $kv.get(1) < 3 ); + check if { "a": 1 , "b": 2, 1: "A" }.any($kv -> $kv.get(0) == 1 && $kv.get(1) == "A" ); "# ) .build_with_rng(&root, SymbolTable::default(), &mut rng) diff --git a/biscuit-auth/samples/README.md b/biscuit-auth/samples/README.md index b20ac14e..ce77ddfa 100644 --- a/biscuit-auth/samples/README.md +++ b/biscuit-auth/samples/README.md @@ -2873,7 +2873,7 @@ result: `Err(Execution(ShadowedVariable))` ### token authority: -symbols: ["a", "b", "c", "p", "d"] +symbols: ["a", "b", "c", "p", "d", "A", "kv"] public keys: [] @@ -2892,6 +2892,12 @@ check if {"a": 1, "b": 2, "c": 3, "d": 4}.length() == 4; check if {1: "a", 2: "b"} != {"a": 1, "b": 2}; check if {1: "a", 2: "b"} == {1: "a", 2: "b"}; check if {"a": 1, "b": 2, "c": 3, "d": 4}.contains("d"); +check if {1: "A", "a": 1, "b": 2}.get("a") == 1; +check if {1: "A", "a": 1, "b": 2}.get(1) == "A"; +check if {1: "A", "a": 1, "b": 2}.get("c") == null; +check if {1: "A", "a": 1, "b": 2}.get(2) == null; +check if {"a": 1, "b": 2}.all($kv -> $kv.get(0) != "c" && $kv.get(1) < 3); +check if {1: "A", "a": 1, "b": 2}.any($kv -> $kv.get(0) == 1 && $kv.get(1) == "A"); ``` ### validation @@ -2902,7 +2908,7 @@ allow if true; ``` revocation ids: -- `ff26323a584a8ec8af2f81b80fefc2d6ea84148f463a4a007386f48eb2a0125f54812ab9da60a82642f46d832939ae8de5bfed20dfd683044f76880092598609` +- `724dd2068fa72d515cbc29894b81e8e64878b45f35755a52fd77bbf0bd2df3bc14b88033ee6b8255e3e79dc253947a6621d3ca7e6427e3f8f29888588b0a6907` authorizer world: ``` @@ -2927,6 +2933,12 @@ World { "check if [4, 5, 6].ends_with([6])", "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\")", "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4", + "check if {\"a\": 1, \"b\": 2}.all($kv -> $kv.get(0) != \"c\" && $kv.get(1) < 3)", + "check if {1: \"A\", \"a\": 1, \"b\": 2}.any($kv -> $kv.get(0) == 1 && $kv.get(1) == \"A\")", + "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"a\") == 1", + "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"c\") == null", + "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(1) == \"A\"", + "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(2) == null", "check if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2}", "check if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"}", ], diff --git a/biscuit-auth/samples/samples.json b/biscuit-auth/samples/samples.json index ad28b63c..344650c4 100644 --- a/biscuit-auth/samples/samples.json +++ b/biscuit-auth/samples/samples.json @@ -2700,11 +2700,13 @@ "b", "c", "p", - "d" + "d", + "A", + "kv" ], "public_keys": [], "external_key": null, - "code": "check if [1, 2, 1].length() == 3;\ncheck if [\"a\", \"b\"] != [1, 2, 3];\ncheck if [\"a\", \"b\"] == [\"a\", \"b\"];\ncheck if [\"a\", \"b\", \"c\"].contains(\"c\");\ncheck if [1, 2, 3].starts_with([1, 2]);\ncheck if [4, 5, 6].ends_with([6]);\ncheck if [1, 2, 3].all($p -> $p > 0);\ncheck if [1, 2, \"a\"].get(2) == \"a\";\ncheck if [1, 2].get(3) == null;\ncheck if [1, 2, 3].all($p -> $p > 0);\ncheck if [1, 2, 3].any($p -> $p > 2);\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4;\ncheck if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2};\ncheck if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"};\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\");\n" + "code": "check if [1, 2, 1].length() == 3;\ncheck if [\"a\", \"b\"] != [1, 2, 3];\ncheck if [\"a\", \"b\"] == [\"a\", \"b\"];\ncheck if [\"a\", \"b\", \"c\"].contains(\"c\");\ncheck if [1, 2, 3].starts_with([1, 2]);\ncheck if [4, 5, 6].ends_with([6]);\ncheck if [1, 2, \"a\"].get(2) == \"a\";\ncheck if [1, 2].get(3) == null;\ncheck if [1, 2, 3].all($p -> $p > 0);\ncheck if [1, 2, 3].any($p -> $p > 2);\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4;\ncheck if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2};\ncheck if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"};\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\");\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"a\") == 1;\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(1) == \"A\";\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"c\") == null;\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(2) == null;\ncheck if {\"a\": 1, \"b\": 2}.all($kv -> $kv.get(0) != \"c\" && $kv.get(1) < 3);\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.any($kv -> $kv.get(0) == 1 && $kv.get(1) == \"A\");\n" } ], "validations": { @@ -2722,13 +2724,18 @@ "check if [1, 2, \"a\"].get(2) == \"a\"", "check if [1, 2, 1].length() == 3", "check if [1, 2, 3].all($p -> $p > 0)", - "check if [1, 2, 3].all($p -> $p > 0)", "check if [1, 2, 3].any($p -> $p > 2)", "check if [1, 2, 3].starts_with([1, 2])", "check if [1, 2].get(3) == null", "check if [4, 5, 6].ends_with([6])", "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\")", "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4", + "check if {\"a\": 1, \"b\": 2}.all($kv -> $kv.get(0) != \"c\" && $kv.get(1) < 3)", + "check if {1: \"A\", \"a\": 1, \"b\": 2}.any($kv -> $kv.get(0) == 1 && $kv.get(1) == \"A\")", + "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"a\") == 1", + "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"c\") == null", + "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(1) == \"A\"", + "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(2) == null", "check if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2}", "check if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"}" ] @@ -2743,7 +2750,7 @@ }, "authorizer_code": "allow if true;\n", "revocation_ids": [ - "00efb9846891354b420fd165e9162e65398af9ec7361b584d312964879b3418e049ff1a39cde0e90dd8f8f9ff24b689806139b40e5a3a44f1ccee1115a64f108" + "724dd2068fa72d515cbc29894b81e8e64878b45f35755a52fd77bbf0bd2df3bc14b88033ee6b8255e3e79dc253947a6621d3ca7e6427e3f8f29888588b0a6907" ] } } diff --git a/biscuit-auth/samples/test033_array_map.bc b/biscuit-auth/samples/test033_array_map.bc index 24b8652cbb6e985c5ee03402af33d1e8c698ffcf..06138f38914ec90dd6c6b23266bdd583e9c1d99c 100644 GIT binary patch literal 1523 zcmbVM&r1|>6nAEJ*KeOp*RkEUSx9#@HCrXd!st*IrZQRBim;UsrUW5G2$d*>yZ)dk zh@e9rJk+6}iwFi$dht*Yo$3#W63UZAUAjcQ@B8krbPM$``~A%9d!P4t?|o*>cU$oZ z(0ZX=fYt}?3bcM`N1+c~3wfO=>Y$6TRRuqa-6H6IjV_%}kEshC9>NAZmM#L$J~VK2 zEs9a(0!C<5&`(0F$yOObrvXvUs9uw?nn6{lLoLVKC_|}Rl2gG5B?Y|nfDRe!&Ea0H z-9QoGicB{$-sfbw)A6!5FI_$e_*{ACW9~v{KOEK(MJY<$Z5bx+SO(Hyh&0TKXb4Ju zw%b+pDM3vnEa!QwAQgWI2f2!qXMQ0&D8__I9m_+OEHGsijdOQ88_U&maxJF=F4t`V zZ8R3e78w3fVanx8n2=Csn3T|9I4+?FSyvDyJprc`EzEWp7DBNg1(QmXCncdykQ|c! zFJC3e6WkzAz8XqT8;-M{Bb(?>8f-jmuy(XEQ8RX#@= z4KvAot7K&{D!ECfoLA;=^JT}h&hDh9wCxFJTs({+9@F4|cYMn35R=N!bVx4c=diRm zz0_>bpc%2t+4HFwjUR_O-e9br+b0ssU)qP}&L6mXHumfKt8WYSKjyc;7;Rh`NOmuMonGvEFnlrc;qA@-nx&uL vAKr@J!KFgmM#a?fNb&u6*RGZ4#_vG&<-L`Qk2^=o?oNlx>*nGg>wEtIdm*u0 delta 130 zcmV-|0Db@S3&jQ!5}pPMvjqwPVG03a3ISsZ0c5cpfdP{h0yP1HlS=~K6N3m60uTfW z1quTgg9r))8UhFflV=4eKz{fa6U&5#u&AGX&#g<-)-vltrhJ``V}rqnEa!lWrEvY< k2@@`X#F^5DoAxL>Wu_3_541J;7_M#zbUwX?1e4DNBn?h3{Qv*} diff --git a/biscuit-auth/src/datalog/expression.rs b/biscuit-auth/src/datalog/expression.rs index cfa8d2cb..51b73071 100644 --- a/biscuit-auth/src/datalog/expression.rs +++ b/biscuit-auth/src/datalog/expression.rs @@ -1,6 +1,6 @@ use crate::error; -use super::Term; +use super::{MapKey, Term}; use super::{SymbolTable, TemporarySymbolTable}; use regex::Regex; use std::collections::{HashMap, HashSet}; @@ -147,8 +147,8 @@ impl Binary { } // array - (Binary::All, Term::Array(set_values), [param]) => { - for value in set_values.iter() { + (Binary::All, Term::Array(array), [param]) => { + for value in array.iter() { values.insert(*param, value.clone()); let e = Expression { ops: right.clone() }; let result = e.evaluate(values, symbols); @@ -161,8 +161,8 @@ impl Binary { } Ok(Term::Bool(true)) } - (Binary::Any, Term::Array(set_values), [param]) => { - for value in set_values.iter() { + (Binary::Any, Term::Array(array), [param]) => { + for value in array.iter() { values.insert(*param, value.clone()); let e = Expression { ops: right.clone() }; let result = e.evaluate(values, symbols); @@ -175,6 +175,46 @@ impl Binary { } Ok(Term::Bool(false)) } + + //map + (Binary::All, Term::Map(map), [param]) => { + for (key, value) in map.iter() { + let key = match key { + MapKey::Integer(i) => Term::Integer(*i), + MapKey::Str(i) => Term::Str(*i), + }; + values.insert(*param, Term::Array(vec![key, value.clone()])); + + let e = Expression { ops: right.clone() }; + let result = e.evaluate(values, symbols); + values.remove(param); + match result? { + Term::Bool(true) => {} + Term::Bool(false) => return Ok(Term::Bool(false)), + _ => return Err(error::Expression::InvalidType), + }; + } + Ok(Term::Bool(true)) + } + (Binary::Any, Term::Map(map), [param]) => { + for (key, value) in map.iter() { + let key = match key { + MapKey::Integer(i) => Term::Integer(*i), + MapKey::Str(i) => Term::Str(*i), + }; + values.insert(*param, Term::Array(vec![key, value.clone()])); + + let e = Expression { ops: right.clone() }; + let result = e.evaluate(values, symbols); + values.remove(param); + match result? { + Term::Bool(false) => {} + Term::Bool(true) => return Ok(Term::Bool(true)), + _ => return Err(error::Expression::InvalidType), + }; + } + Ok(Term::Bool(false)) + } (_, _, _) => Err(error::Expression::InvalidType), } } @@ -372,6 +412,14 @@ impl Binary { _ => false, }))) } + (Binary::Get, Term::Map(m), Term::Integer(i)) => match m.get(&MapKey::Integer(i)) { + Some(term) => Ok(term.clone()), + None => Ok(Term::Null), + }, + (Binary::Get, Term::Map(m), Term::Str(i)) => match m.get(&MapKey::Str(i)) { + Some(term) => Ok(term.clone()), + None => Ok(Term::Null), + }, (Binary::HeterogeneousEqual, _, _) => Ok(Term::Bool(false)), (Binary::HeterogeneousNotEqual, _, _) => Ok(Term::Bool(true)), @@ -1278,8 +1326,10 @@ mod tests { #[test] fn map() { - let symbols = SymbolTable::new(); + let mut symbols = SymbolTable::new(); + let p = symbols.insert("param") as u32; let mut tmp_symbols = TemporarySymbolTable::new(&symbols); + let ops = vec![ Op::Value(Term::Map( [ @@ -1374,13 +1424,32 @@ mod tests { Op::Value(Term::Map( [ (MapKey::Str(1), Term::Integer(0)), - (MapKey::Str(2), Term::Integer(1)), + (MapKey::Integer(2), Term::Integer(1)), ] .iter() .cloned() .collect(), )), - Op::Value(Term::Str(2)), + Op::Value(Term::Str(1)), + Op::Binary(Binary::Get), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Integer(0))); + + let ops = vec![ + Op::Value(Term::Map( + [ + (MapKey::Str(1), Term::Integer(0)), + (MapKey::Integer(2), Term::Integer(1)), + ] + .iter() + .cloned() + .collect(), + )), + Op::Value(Term::Integer(2)), Op::Binary(Binary::Get), ]; @@ -1409,8 +1478,26 @@ mod tests { let res = e.evaluate(&values, &mut tmp_symbols); assert_eq!(res, Ok(Term::Null)); + let ops = vec![ + Op::Value(Term::Map( + [ + (MapKey::Str(1), Term::Integer(0)), + (MapKey::Str(2), Term::Integer(1)), + ] + .iter() + .cloned() + .collect(), + )), + Op::Value(Term::Str(3)), + Op::Binary(Binary::Get), + ]; + + let values = HashMap::new(); + let e = Expression { ops }; + let res = e.evaluate(&values, &mut tmp_symbols); + assert_eq!(res, Ok(Term::Null)); + // all - let p = tmp_symbols.insert("param") as u32; let ops1 = vec![ Op::Value(Term::Map( [ @@ -1466,6 +1553,6 @@ mod tests { println!("{:?}", e1.print(&symbols)); let res1 = e1.evaluate(&HashMap::new(), &mut tmp_symbols).unwrap(); - assert_eq!(res1, Term::Bool(false)); + assert_eq!(res1, Term::Bool(true)); } } From 150d5dbac328eec7035175514f737c500fa97abb Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 8 Jun 2024 18:49:16 +0200 Subject: [PATCH 22/27] add an example of nested maps and arrays --- biscuit-auth/examples/testcases.rs | 2 ++ biscuit-auth/samples/README.md | 6 ++++-- biscuit-auth/samples/samples.json | 9 ++++++--- biscuit-auth/samples/test033_array_map.bc | Bin 1523 -> 1623 bytes 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/biscuit-auth/examples/testcases.rs b/biscuit-auth/examples/testcases.rs index af4d1537..aec0992a 100644 --- a/biscuit-auth/examples/testcases.rs +++ b/biscuit-auth/examples/testcases.rs @@ -2163,6 +2163,8 @@ fn array_map(target: &str, root: &KeyPair, test: bool) -> TestResult { check if { "a": 1 , "b": 2, 1: "A" }.get(2) == null; check if { "a": 1 , "b": 2 }.all($kv -> $kv.get(0) != "c" && $kv.get(1) < 3 ); check if { "a": 1 , "b": 2, 1: "A" }.any($kv -> $kv.get(0) == 1 && $kv.get(1) == "A" ); + // nesting + check if { "user": { "id": 1, "roles": ["admin"] } }.get("user").get("roles").contains("admin"); "# ) .build_with_rng(&root, SymbolTable::default(), &mut rng) diff --git a/biscuit-auth/samples/README.md b/biscuit-auth/samples/README.md index ce77ddfa..27a8027d 100644 --- a/biscuit-auth/samples/README.md +++ b/biscuit-auth/samples/README.md @@ -2873,7 +2873,7 @@ result: `Err(Execution(ShadowedVariable))` ### token authority: -symbols: ["a", "b", "c", "p", "d", "A", "kv"] +symbols: ["a", "b", "c", "p", "d", "A", "kv", "id", "roles"] public keys: [] @@ -2898,6 +2898,7 @@ check if {1: "A", "a": 1, "b": 2}.get("c") == null; check if {1: "A", "a": 1, "b": 2}.get(2) == null; check if {"a": 1, "b": 2}.all($kv -> $kv.get(0) != "c" && $kv.get(1) < 3); check if {1: "A", "a": 1, "b": 2}.any($kv -> $kv.get(0) == 1 && $kv.get(1) == "A"); +check if {"user": {"id": 1, "roles": ["admin"]}}.get("user").get("roles").contains("admin"); ``` ### validation @@ -2908,7 +2909,7 @@ allow if true; ``` revocation ids: -- `724dd2068fa72d515cbc29894b81e8e64878b45f35755a52fd77bbf0bd2df3bc14b88033ee6b8255e3e79dc253947a6621d3ca7e6427e3f8f29888588b0a6907` +- `7096e2ad9ad5dcae778cea1cee800ffc38017196e56aed693810d0933bcecc804a723768c3b494fa23d99be59ca3588bfa806e3fe2dac29d0ca9e452b69ead09` authorizer world: ``` @@ -2934,6 +2935,7 @@ World { "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\")", "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4", "check if {\"a\": 1, \"b\": 2}.all($kv -> $kv.get(0) != \"c\" && $kv.get(1) < 3)", + "check if {\"user\": {\"id\": 1, \"roles\": [\"admin\"]}}.get(\"user\").get(\"roles\").contains(\"admin\")", "check if {1: \"A\", \"a\": 1, \"b\": 2}.any($kv -> $kv.get(0) == 1 && $kv.get(1) == \"A\")", "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"a\") == 1", "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"c\") == null", diff --git a/biscuit-auth/samples/samples.json b/biscuit-auth/samples/samples.json index 344650c4..3fb9fa30 100644 --- a/biscuit-auth/samples/samples.json +++ b/biscuit-auth/samples/samples.json @@ -2702,11 +2702,13 @@ "p", "d", "A", - "kv" + "kv", + "id", + "roles" ], "public_keys": [], "external_key": null, - "code": "check if [1, 2, 1].length() == 3;\ncheck if [\"a\", \"b\"] != [1, 2, 3];\ncheck if [\"a\", \"b\"] == [\"a\", \"b\"];\ncheck if [\"a\", \"b\", \"c\"].contains(\"c\");\ncheck if [1, 2, 3].starts_with([1, 2]);\ncheck if [4, 5, 6].ends_with([6]);\ncheck if [1, 2, \"a\"].get(2) == \"a\";\ncheck if [1, 2].get(3) == null;\ncheck if [1, 2, 3].all($p -> $p > 0);\ncheck if [1, 2, 3].any($p -> $p > 2);\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4;\ncheck if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2};\ncheck if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"};\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\");\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"a\") == 1;\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(1) == \"A\";\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"c\") == null;\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(2) == null;\ncheck if {\"a\": 1, \"b\": 2}.all($kv -> $kv.get(0) != \"c\" && $kv.get(1) < 3);\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.any($kv -> $kv.get(0) == 1 && $kv.get(1) == \"A\");\n" + "code": "check if [1, 2, 1].length() == 3;\ncheck if [\"a\", \"b\"] != [1, 2, 3];\ncheck if [\"a\", \"b\"] == [\"a\", \"b\"];\ncheck if [\"a\", \"b\", \"c\"].contains(\"c\");\ncheck if [1, 2, 3].starts_with([1, 2]);\ncheck if [4, 5, 6].ends_with([6]);\ncheck if [1, 2, \"a\"].get(2) == \"a\";\ncheck if [1, 2].get(3) == null;\ncheck if [1, 2, 3].all($p -> $p > 0);\ncheck if [1, 2, 3].any($p -> $p > 2);\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4;\ncheck if {1: \"a\", 2: \"b\"} != {\"a\": 1, \"b\": 2};\ncheck if {1: \"a\", 2: \"b\"} == {1: \"a\", 2: \"b\"};\ncheck if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\");\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"a\") == 1;\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(1) == \"A\";\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"c\") == null;\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.get(2) == null;\ncheck if {\"a\": 1, \"b\": 2}.all($kv -> $kv.get(0) != \"c\" && $kv.get(1) < 3);\ncheck if {1: \"A\", \"a\": 1, \"b\": 2}.any($kv -> $kv.get(0) == 1 && $kv.get(1) == \"A\");\ncheck if {\"user\": {\"id\": 1, \"roles\": [\"admin\"]}}.get(\"user\").get(\"roles\").contains(\"admin\");\n" } ], "validations": { @@ -2731,6 +2733,7 @@ "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.contains(\"d\")", "check if {\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4}.length() == 4", "check if {\"a\": 1, \"b\": 2}.all($kv -> $kv.get(0) != \"c\" && $kv.get(1) < 3)", + "check if {\"user\": {\"id\": 1, \"roles\": [\"admin\"]}}.get(\"user\").get(\"roles\").contains(\"admin\")", "check if {1: \"A\", \"a\": 1, \"b\": 2}.any($kv -> $kv.get(0) == 1 && $kv.get(1) == \"A\")", "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"a\") == 1", "check if {1: \"A\", \"a\": 1, \"b\": 2}.get(\"c\") == null", @@ -2750,7 +2753,7 @@ }, "authorizer_code": "allow if true;\n", "revocation_ids": [ - "724dd2068fa72d515cbc29894b81e8e64878b45f35755a52fd77bbf0bd2df3bc14b88033ee6b8255e3e79dc253947a6621d3ca7e6427e3f8f29888588b0a6907" + "7096e2ad9ad5dcae778cea1cee800ffc38017196e56aed693810d0933bcecc804a723768c3b494fa23d99be59ca3588bfa806e3fe2dac29d0ca9e452b69ead09" ] } } diff --git a/biscuit-auth/samples/test033_array_map.bc b/biscuit-auth/samples/test033_array_map.bc index 06138f38914ec90dd6c6b23266bdd583e9c1d99c..b4cd49836b5d0260780b7ec7db8dc62dff6b80dd 100644 GIT binary patch delta 211 zcmey&eVs>LXaf(|QEo2AL@vf8F2-ao#sV(J6fQcglLRjp zh~(m8kpk-DV&!6%=-_~Ipwd7I*2(Ft8V&{19<7~q_0GETo>wyO8uvLxsyo$^-4sV(AOZn#Pr*jrZbpL9|vww8!&|IFCPlC41Tgy55JgW)-)TcU4 delta 110 zcmV-!0FnRK4D$;d63hz<;R*@?VG03a3ISsZ0dNWdWC{U63Ic0(u_sFflgkAtKyppe z29KvLQCz$!iA#a#=16$7Uo~}FQvG+k@VzbbycD>AGwy4GRpaNK!c&xbW+Btceq<-( Q`0|*DSc?j22a_`fBuKI>X#fBK From 0399b2fb94a3cef3cf9be710ee01c1c900965895 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 8 Jun 2024 18:59:39 +0200 Subject: [PATCH 23/27] test array parameters --- biscuit-auth/tests/macros.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/biscuit-auth/tests/macros.rs b/biscuit-auth/tests/macros.rs index 66ecae4e..f7a6aa5f 100644 --- a/biscuit-auth/tests/macros.rs +++ b/biscuit-auth/tests/macros.rs @@ -10,10 +10,11 @@ fn block_macro() { let mut term_set = BTreeSet::new(); term_set.insert(builder::int(0i64)); let my_key = "my_value"; + let array_param = 2; let mapkey = "hello"; let mut b = block!( - r#"fact("test", hex:aabbcc, [true], {my_key}, {term_set}, {"a": 1, 2 : "abcd", {mapkey}: 0 }); + r#"fact("test", hex:aabbcc, [1, {array_param}], {my_key}, {term_set}, {"a": 1, 2 : "abcd", {mapkey}: 0 }); rule($0, true) <- fact($0, $1, $2, {my_key}), true || false; check if {my_key}.starts_with("my"); check if {true,false}.any($p -> true); @@ -25,7 +26,7 @@ fn block_macro() { assert_eq!( b.to_string(), - r#"fact("test", hex:aabbcc, [true], "my_value", {0}, {2: "abcd", "a": 1, "hello": 0}); + r#"fact("test", hex:aabbcc, [1, 2], "my_value", {0}, {2: "abcd", "a": 1, "hello": 0}); appended(true); rule($0, true) <- fact($0, $1, $2, "my_value"), true || false; check if "my_value".starts_with("my"); From 38f81ac4f03fcf80d212840cc532e99aa71d6fde Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 8 Jun 2024 19:41:42 +0200 Subject: [PATCH 24/27] JSON support --- biscuit-auth/Cargo.toml | 1 + biscuit-auth/src/token/builder.rs | 38 +++++++++++++++++++++++++++++++ biscuit-auth/tests/macros.rs | 26 +++++++++++++++++++-- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/biscuit-auth/Cargo.toml b/biscuit-auth/Cargo.toml index 4e100841..1b6caa58 100644 --- a/biscuit-auth/Cargo.toml +++ b/biscuit-auth/Cargo.toml @@ -49,6 +49,7 @@ uuid = { version = "1", optional = true } biscuit-parser = { version = "0.1.2", path = "../biscuit-parser" } biscuit-quote = { version = "0.2.2", optional = true, path = "../biscuit-quote" } chrono = { version = "0.4.26", optional = true, default-features = false, features = ["serde"] } +serde_json = "1.0.117" [dev-dependencies] diff --git a/biscuit-auth/src/token/builder.rs b/biscuit-auth/src/token/builder.rs index 2d3fe542..44042121 100644 --- a/biscuit-auth/src/token/builder.rs +++ b/biscuit-auth/src/token/builder.rs @@ -2048,6 +2048,13 @@ pub trait ToAnyParam { fn to_any_param(&self) -> AnyParam; } +#[cfg(feature = "datalog-macro")] +impl ToAnyParam for Term { + fn to_any_param(&self) -> AnyParam { + AnyParam::Term(self.clone()) + } +} + impl From for Term { fn from(i: i64) -> Self { Term::Integer(i) @@ -2238,6 +2245,37 @@ impl> TryFrom for BTreeSet } } +// TODO: From and ToAnyParam for arrays and maps +impl TryFrom for Term { + type Error = &'static str; + + fn try_from(value: serde_json::Value) -> Result { + match value { + serde_json::Value::Null => Ok(Term::Null), + serde_json::Value::Bool(b) => Ok(Term::Bool(b)), + serde_json::Value::Number(i) => match i.as_i64() { + Some(i) => Ok(Term::Integer(i)), + None => Err("Biscuit values do not support floating point numbers"), + }, + serde_json::Value::String(s) => Ok(Term::Str(s)), + serde_json::Value::Array(array) => Ok(Term::Array( + array + .into_iter() + .map(|v| v.try_into()) + .collect::>()?, + )), + serde_json::Value::Object(o) => Ok(Term::Map( + o.into_iter() + .map(|(key, value)| { + let value: Term = value.try_into()?; + Ok::<_, &'static str>((MapKey::Str(key), value)) + }) + .collect::>()?, + )), + } + } +} + macro_rules! tuple_try_from( ($ty1:ident, $ty2:ident, $($ty:ident),*) => ( tuple_try_from!(__impl $ty1, $ty2; $($ty),*); diff --git a/biscuit-auth/tests/macros.rs b/biscuit-auth/tests/macros.rs index f7a6aa5f..88a464b7 100644 --- a/biscuit-auth/tests/macros.rs +++ b/biscuit-auth/tests/macros.rs @@ -1,9 +1,9 @@ -use biscuit_auth::builder; +use biscuit_auth::{builder, KeyPair}; use biscuit_quote::{ authorizer, authorizer_merge, biscuit, biscuit_merge, block, block_merge, check, fact, policy, rule, }; -use std::collections::BTreeSet; +use std::{collections::BTreeSet, convert::TryInto}; #[test] fn block_macro() { @@ -222,3 +222,25 @@ fn policy_macro() { r#"allow if fact("my_value", {0}) trusting ed25519/6e9e6d5a75cf0c0e87ec1256b4dfed0ca3ba452912d213fcc70f8516583db9db"#, ); } + +#[test] +fn json() { + let key_pair = KeyPair::new(); + let biscuit = biscuit!(r#"user(123)"#).build(&key_pair).unwrap(); + + let value: serde_json::Value = + serde_json::from_str(r#"{ "id": 123, "roles": ["admin"] }"#).unwrap(); + let json_value: biscuit_auth::builder::Term = value.try_into().unwrap(); + + let authorizer = authorizer!( + r#" + user_roles({json_value}); + allow if + user($id), + user_roles($value), + $value.get("id") == $id, + $value.get("roles").contains("admin");"# + ); + + assert!(biscuit.authorize(&authorizer).is_ok()); +} From d5003f33bce156de1282f0e5eebcfd25a9dafb72 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sat, 15 Jun 2024 12:08:13 +0200 Subject: [PATCH 25/27] use the json! macro --- biscuit-auth/tests/macros.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/biscuit-auth/tests/macros.rs b/biscuit-auth/tests/macros.rs index 88a464b7..6af98924 100644 --- a/biscuit-auth/tests/macros.rs +++ b/biscuit-auth/tests/macros.rs @@ -3,6 +3,7 @@ use biscuit_quote::{ authorizer, authorizer_merge, biscuit, biscuit_merge, block, block_merge, check, fact, policy, rule, }; +use serde_json::json; use std::{collections::BTreeSet, convert::TryInto}; #[test] @@ -228,8 +229,12 @@ fn json() { let key_pair = KeyPair::new(); let biscuit = biscuit!(r#"user(123)"#).build(&key_pair).unwrap(); - let value: serde_json::Value = - serde_json::from_str(r#"{ "id": 123, "roles": ["admin"] }"#).unwrap(); + let value: serde_json::Value = json!( + r#"{ + "id": 123, + "roles": ["admin"] + }"# + ); let json_value: biscuit_auth::builder::Term = value.try_into().unwrap(); let authorizer = authorizer!( From ab861d1a2c412dec86760aed7ecc098711417ae4 Mon Sep 17 00:00:00 2001 From: Clement Delafargue Date: Sat, 5 Oct 2024 14:07:20 +0200 Subject: [PATCH 26/27] fix json test the json macro takes a json-like syntax, not a string --- biscuit-auth/tests/macros.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/biscuit-auth/tests/macros.rs b/biscuit-auth/tests/macros.rs index 6af98924..b2146294 100644 --- a/biscuit-auth/tests/macros.rs +++ b/biscuit-auth/tests/macros.rs @@ -230,10 +230,10 @@ fn json() { let biscuit = biscuit!(r#"user(123)"#).build(&key_pair).unwrap(); let value: serde_json::Value = json!( - r#"{ + { "id": 123, "roles": ["admin"] - }"# + } ); let json_value: biscuit_auth::builder::Term = value.try_into().unwrap(); From 6d3b426beb124df4eea5f025bb8b12ff81e41131 Mon Sep 17 00:00:00 2001 From: Geoffroy Couprie Date: Sun, 20 Oct 2024 16:04:44 +0200 Subject: [PATCH 27/27] fix regression on 1-letter parameter names --- biscuit-parser/src/parser.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/biscuit-parser/src/parser.rs b/biscuit-parser/src/parser.rs index 20fd9130..fda67dab 100644 --- a/biscuit-parser/src/parser.rs +++ b/biscuit-parser/src/parser.rs @@ -735,11 +735,14 @@ fn name(i: &str) -> IResult<&str, &str, Error> { fn parameter_name(i: &str) -> IResult<&str, &str, Error> { let is_name_char = |c: char| is_alphanumeric(c as u8) || c == '_' || c == ':'; - reduce( + error( recognize(preceded( satisfy(|c: char| is_alphabetic(c as u8)), - take_while1(is_name_char), + take_while(is_name_char), )), + |_| { + "invalid parameter name: it must start with an alphabetic character, followed by alphanumeric characters, underscores or colons".to_string() + }, " ,:(\n;", )(i) } @@ -1348,6 +1351,17 @@ mod tests { super::parameter("{param}"), Ok(("", builder::parameter("param"))) ); + + assert_eq!( + super::parameter("{1param}"), + Err(nom::Err::Error(crate::parser::Error { + input: "1param}", + code: nom::error::ErrorKind::Satisfy, + message: Some("invalid parameter name: it must start with an alphabetic character, followed by alphanumeric characters, underscores or colons".to_string()) + })) + ); + + assert_eq!(super::parameter("{p}"), Ok(("", builder::parameter("p")))); } #[test]