From af5071446b474d8b34d3d5cee9784aca2cd20fb8 Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:11:09 -0700 Subject: [PATCH] feat: OPA v0.68.0. Engine::set_rego_v1 (#305) Provide ability in the engine to treat subsequently loaded policies as rego.v1. Signed-off-by: Anand Krishnamoorthi --- README.md | 6 +- examples/regorus.rs | 10 ++ src/engine.rs | 43 +++++- src/parser.rs | 17 ++- tests/opa.passing | 344 +++++++++++++++++++++++++++++--------------- tests/opa.rs | 9 +- 6 files changed, 300 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 4b9afe67..505bd404 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Regorus is also - *cross-platform* - Written in platform-agnostic Rust. - *no_std compatible* - Regorus can be used in `no_std` environments too. Most of the builtins are supported. - *current* - We strive to keep Regorus up to date with latest OPA release. Regorus supports `import rego.v1`. - - *compliant* - Regorus is mostly compliant with the latest [OPA release v0.67.0](https://github.com/open-policy-agent/opa/releases/tag/v0.67.0). See [OPA Conformance](#opa-conformance) for details. Note that while we behaviorally produce the same results, we don't yet support all the builtins. + - *compliant* - Regorus is mostly compliant with the latest [OPA release v0.68.0](https://github.com/open-policy-agent/opa/releases/tag/v0.68.0). See [OPA Conformance](#opa-conformance) for details. Note that while we behaviorally produce the same results, we don't yet support all the builtins. - *extensible* - Extend the Rego language by implementing custom stateful builtins in Rust. See [add_extension](https://github.com/microsoft/regorus/blob/fc68bf9c8bea36427dae9401a7d1f6ada771f7ab/src/engine.rs#L352). Support for extensibility using other languages coming soon. @@ -99,7 +99,7 @@ $ cargo build -r --example regorus --no-default-features; strip target/release/e -rwxr-xr-x 1 anand staff 1.9M May 11 22:04 target/release/examples/regorus* ``` -Regorus passes the [OPA v0.67.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few +Regorus passes the [OPA v0.68.0 test-suite](https://www.openpolicyagent.org/docs/latest/ir/#test-suite) barring a few builtins. See [OPA Conformance](#opa-conformance) below. ## Bindings @@ -276,7 +276,7 @@ Benchmark 1: opa eval -b tests/aci -d tests/aci/data.json -i tests/aci/input.jso ``` ## OPA Conformance -Regorus has been verified to be compliant with [OPA v0.67.0](https://github.com/open-policy-agent/opa/releases/tag/v0.67.0) +Regorus has been verified to be compliant with [OPA v0.68.0](https://github.com/open-policy-agent/opa/releases/tag/v0.68.0) using a [test driver](https://github.com/microsoft/regorus/blob/main/tests/opa.rs) that loads and runs the OPA testsuite using Regorus, and verifies that expected outputs are produced. The test driver can be invoked by running: diff --git a/examples/regorus.rs b/examples/regorus.rs index 204dd56b..b4a46aee 100644 --- a/examples/regorus.rs +++ b/examples/regorus.rs @@ -33,6 +33,7 @@ fn add_policy_from_file(engine: &mut regorus::Engine, path: String) -> Result Result<()> { // Create engine. let mut engine = regorus::Engine::new(); @@ -50,6 +52,8 @@ fn rego_eval( #[cfg(feature = "coverage")] engine.set_enable_coverage(coverage); + engine.set_rego_v1(v1); + // Load files from given bundles. for dir in bundles.iter() { let entries = @@ -233,6 +237,10 @@ enum RegorusCommand { #[cfg(feature = "coverage")] #[arg(long, short)] coverage: bool, + + /// Turn on rego.v1 + #[arg(long)] + v1: bool, }, /// Tokenize a Rego policy. @@ -274,6 +282,7 @@ fn main() -> Result<()> { non_strict, #[cfg(feature = "coverage")] coverage, + v1, } => rego_eval( &bundles, &data, @@ -283,6 +292,7 @@ fn main() -> Result<()> { non_strict, #[cfg(feature = "coverage")] coverage, + v1, ), RegorusCommand::Lex { file, verbose } => rego_lex(file, verbose), RegorusCommand::Parse { file } => rego_parse(file), diff --git a/src/engine.rs b/src/engine.rs index 9550fe69..f3ae13da 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -20,6 +20,7 @@ pub struct Engine { modules: Vec>, interpreter: Interpreter, prepared: bool, + rego_v1: bool, } /// Create a default engine. @@ -36,9 +37,35 @@ impl Engine { modules: vec![], interpreter: Interpreter::new(), prepared: false, + rego_v1: false, } } + /// Turn rego.v1 on/off for subsequently added policies. + /// + /// Explicit import rego.v1 is not needed if set. + /// + /// ``` + /// # use regorus::*; + /// # fn main() -> anyhow::Result<()> { + /// let mut engine = Engine::new(); + /// + /// engine.set_rego_v1(true); + /// engine.add_policy( + /// "test.rego".to_string(), + /// r#" + /// package test + /// allow if true # if keyword is automatically imported + /// "#.to_string())?; + /// + /// # Ok(()) + /// # } + /// ``` + /// + pub fn set_rego_v1(&mut self, rego_v1: bool) { + self.rego_v1 = rego_v1; + } + /// Add a policy. /// /// The policy file will be parsed and converted to AST representation. @@ -67,7 +94,7 @@ impl Engine { /// pub fn add_policy(&mut self, path: String, rego: String) -> Result { let source = Source::from_contents(path, rego)?; - let mut parser = Parser::new(&source)?; + let mut parser = self.make_parser(&source)?; let module = Ref::new(parser.parse()?); self.modules.push(module.clone()); // if policies change, interpreter needs to be prepared again @@ -98,7 +125,7 @@ impl Engine { #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn add_policy_from_file>(&mut self, path: P) -> Result { let source = Source::from_file(path)?; - let mut parser = Parser::new(&source)?; + let mut parser = self.make_parser(&source)?; let module = Ref::new(parser.parse()?); self.modules.push(module.clone()); // if policies change, interpreter needs to be prepared again @@ -428,7 +455,7 @@ impl Engine { // Parse the query. let query_source = Source::from_contents("".to_string(), query)?; - let mut parser = Parser::new(&query_source)?; + let mut parser = self.make_parser(&query_source)?; let query_node = parser.parse_user_query()?; if query_node.span.text() == "data" { self.eval_modules(enable_tracing)?; @@ -545,7 +572,7 @@ impl Engine { // Parse the query. let query_source = Source::from_contents("".to_string(), query)?; - let mut parser = Parser::new(&query_source)?; + let mut parser = self.make_parser(&query_source)?; let query_node = parser.parse_user_query()?; let query_schedule = Analyzer::new().analyze_query_snippet(&self.modules, &query_node)?; self.interpreter.eval_user_query( @@ -870,4 +897,12 @@ impl Engine { serde_json::to_string_pretty(&ast).map_err(anyhow::Error::msg) } + + fn make_parser<'a>(&self, source: &'a Source) -> Result> { + let mut parser = Parser::new(source)?; + if self.rego_v1 { + parser.enable_rego_v1()?; + } + Ok(parser) + } } diff --git a/src/parser.rs b/src/parser.rs index 45d45b8a..1b820958 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -40,6 +40,18 @@ impl<'source> Parser<'source> { }) } + pub fn enable_rego_v1(&mut self) -> Result<()> { + self.turn_on_rego_v1(self.tok.1.clone()) + } + + fn turn_on_rego_v1(&mut self, span: Span) -> Result<()> { + self.rego_v1 = true; + for kw in FUTURE_KEYWORDS { + self.set_future_keyword(kw, &span)?; + } + Ok(()) + } + pub fn token_text(&self) -> &str { match self.tok.0 { TokenKind::Symbol | TokenKind::Number | TokenKind::Ident | TokenKind::Eof => { @@ -1648,10 +1660,7 @@ impl<'source> Parser<'source> { let is_future_kw = if comps.len() == 2 && comps[0].text() == "rego" && comps[1].text() == "v1" { - self.rego_v1 = true; - for kw in FUTURE_KEYWORDS { - self.set_future_keyword(kw, &span)?; - } + self.turn_on_rego_v1(span.clone())?; true } else { self.handle_import_future_keywords(&comps)? diff --git a/tests/opa.passing b/tests/opa.passing index 240dfbaf..26177ffa 100644 --- a/tests/opa.passing +++ b/tests/opa.passing @@ -1,115 +1,229 @@ -aggregates -all -any -arithmetic -array -assignments -base64builtins -base64urlbuiltins -baseandvirtualdocs -bitsand -bitsnegate -bitsor -bitsshiftleft -bitsshiftright -bitsxor -casts -comparisonexpr -completedoc -compositebasedereference -compositereferences -comprehensions -containskeyword -cryptohmacequal -cryptohmacmd5 -cryptohmacsha1 -cryptohmacsha256 -cryptohmacsha512 -cryptomd5 -cryptosha1 -cryptosha256 -dataderef -defaultkeyword -disjunction -elsekeyword -embeddedvirtualdoc -eqexpr -evaltermexpr -every -example -fix1863 -functionerrors -functions -globmatch -globquotemeta -helloworld -hexbuiltins -indexing -indirectreferences -inputvalues -intersection -jsonbuiltins -jsonfilter -jsonfilteridempotent -jsonremove -jsonremoveidempotent -jsonschema -jwtbuiltins -negation -nestedreferences -numbersrange -numbersrangestep -objectfilter -objectfilteridempotent -objectfilternonstringkey -objectget -objectkeys -objectremove -objectremoveidempotent -objectremovenonstringkey -objectunion -objectunionn -partialdocconstants -partialiter -partialobjectdoc -partialsetdoc -planner-ir -rand -reachable -refheads -regexfind -regexfindallstringsubmatch -regexisvalid -regexmatch -regexmatchtemplate -regexreplace -regexsplit -replacen -semvercompare -semverisvalid -sets -sprintf -strings -subset -toarray -topdowndynamicdispatch -toset -time -trim -trimleft -trimprefix -trimright -trimspace -trimsuffix -type -typebuiltin -typenamebuiltin -undos -union -units -urlbuiltins -uuid -varreferences -virtualdocs -walkbuiltin -withkeyword \ No newline at end of file +v0/aggregates +v0/all +v0/any +v0/arithmetic +v0/array +v0/assignments +v0/base64builtins +v0/base64urlbuiltins +v0/baseandvirtualdocs +v0/bitsand +v0/bitsnegate +v0/bitsor +v0/bitsshiftleft +v0/bitsshiftright +v0/bitsxor +v0/casts +v0/comparisonexpr +v0/completedoc +v0/compositebasedereference +v0/compositereferences +v0/comprehensions +v0/containskeyword +v0/cryptohmacequal +v0/cryptohmacmd5 +v0/cryptohmacsha1 +v0/cryptohmacsha256 +v0/cryptohmacsha512 +v0/cryptomd5 +v0/cryptosha1 +v0/cryptosha256 +v0/dataderef +v0/defaultkeyword +v0/disjunction +v0/elsekeyword +v0/embeddedvirtualdoc +v0/eqexpr +v0/evaltermexpr +v0/every +v0/example +v0/fix1863 +v0/functionerrors +v0/functions +v0/globmatch +v0/globquotemeta +v0/helloworld +v0/hexbuiltins +v0/indexing +v0/indirectreferences +v0/inputvalues +v0/intersection +v0/jsonbuiltins +v0/jsonfilter +v0/jsonfilteridempotent +v0/jsonremove +v0/jsonremoveidempotent +v0/jsonschema +v0/jwtbuiltins +v0/negation +v0/nestedreferences +v0/numbersrange +v0/numbersrangestep +v0/objectfilter +v0/objectfilteridempotent +v0/objectfilternonstringkey +v0/objectget +v0/objectkeys +v0/objectremove +v0/objectremoveidempotent +v0/objectremovenonstringkey +v0/objectunion +v0/objectunionn +v0/partialdocconstants +v0/partialiter +v0/partialobjectdoc +v0/partialsetdoc +v0/planner-ir +v0/rand +v0/reachable +v0/refheads +v0/regexfind +v0/regexfindallstringsubmatch +v0/regexisvalid +v0/regexmatch +v0/regexmatchtemplate +v0/regexreplace +v0/regexsplit +v0/replacen +v0/semvercompare +v0/semverisvalid +v0/sets +v0/sprintf +v0/strings +v0/subset +v0/toarray +v0/topdowndynamicdispatch +v0/toset +v0/time +v0/trim +v0/trimleft +v0/trimprefix +v0/trimright +v0/trimspace +v0/trimsuffix +v0/type +v0/typebuiltin +v0/typenamebuiltin +v0/undos +v0/union +v0/units +v0/urlbuiltins +v0/uuid +v0/varreferences +v0/virtualdocs +v0/walkbuiltin +v0/withkeyword +v1/aggregates +v1/all +v1/any +v1/arithmetic +v1/array +v1/assignments +v1/base64builtins +v1/base64urlbuiltins +v1/baseandvirtualdocs +v1/bitsand +v1/bitsnegate +v1/bitsor +v1/bitsshiftleft +v1/bitsshiftright +v1/bitsxor +v1/casts +v1/comparisonexpr +v1/completedoc +v1/compositebasedereference +v1/compositereferences +v1/comprehensions +v1/containskeyword +v1/cryptohmacequal +v1/cryptohmacmd5 +v1/cryptohmacsha1 +v1/cryptohmacsha256 +v1/cryptohmacsha512 +v1/cryptomd5 +v1/cryptosha1 +v1/cryptosha256 +v1/dataderef +v1/defaultkeyword +v1/disjunction +v1/elsekeyword +v1/embeddedvirtualdoc +v1/eqexpr +v1/evaltermexpr +v1/every +v1/example +v1/fix1863 +v1/functionerrors +v1/functions +v1/globmatch +v1/globquotemeta +v1/helloworld +v1/hexbuiltins +v1/indexing +v1/indirectreferences +v1/inputvalues +v1/intersection +v1/jsonbuiltins +v1/jsonfilter +v1/jsonfilteridempotent +v1/jsonremove +v1/jsonremoveidempotent +v1/jsonschema +v1/jwtbuiltins +v1/negation +v1/nestedreferences +v1/numbersrange +v1/numbersrangestep +v1/objectfilter +v1/objectfilteridempotent +v1/objectfilternonstringkey +v1/objectget +v1/objectkeys +v1/objectremove +v1/objectremoveidempotent +v1/objectremovenonstringkey +v1/objectunion +v1/objectunionn +v1/partialdocconstants +v1/partialiter +v1/partialobjectdoc +v1/partialsetdoc +v1/planner-ir +v1/rand +v1/reachable +v1/refheads +v1/regexfind +v1/regexfindallstringsubmatch +v1/regexisvalid +v1/regexmatch +v1/regexmatchtemplate +v1/regexreplace +v1/regexsplit +v1/replacen +v1/semvercompare +v1/semverisvalid +v1/sets +v1/sprintf +v1/strings +v1/subset +v1/toarray +v1/topdowndynamicdispatch +v1/toset +v1/time +v1/trim +v1/trimleft +v1/trimprefix +v1/trimright +v1/trimspace +v1/trimsuffix +v1/type +v1/typebuiltin +v1/typenamebuiltin +v1/undos +v1/union +v1/units +v1/urlbuiltins +v1/uuid +v1/varreferences +v1/virtualdocs +v1/walkbuiltin diff --git a/tests/opa.rs b/tests/opa.rs index 8785a427..b986d225 100644 --- a/tests/opa.rs +++ b/tests/opa.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use walkdir::WalkDir; const OPA_REPO: &str = "https://github.com/open-policy-agent/opa"; -const OPA_BRANCH: &str = "v0.67.0"; +const OPA_BRANCH: &str = "v0.68.0"; #[derive(Serialize, Deserialize, PartialEq, Debug)] #[serde(deny_unknown_fields)] @@ -51,12 +51,14 @@ struct YamlTest { cases: Vec, } -fn eval_test_case(case: &TestCase) -> Result { +fn eval_test_case(case: &TestCase, is_rego_v1_test: bool) -> Result { let mut engine = Engine::new(); #[cfg(feature = "coverage")] engine.set_enable_coverage(true); + engine.set_rego_v1(is_rego_v1_test); + if let Some(data) = &case.data { engine.add_data(data.clone())?; } @@ -172,6 +174,7 @@ fn run_opa_tests(opa_tests_dir: String, folders: &[String]) -> Result<()> { continue; } + let is_rego_v1_test = path_dir_str.starts_with("v1/"); let entry = status.entry(path_dir_str).or_insert((0, 0, 0)); let yaml_str = std::fs::read_to_string(&path_str)?; @@ -216,7 +219,7 @@ fn run_opa_tests(opa_tests_dir: String, folders: &[String]) -> Result<()> { print!("{:4}: {:90}", entry.2, case.note); entry.2 += 1; - match (eval_test_case(&case), &case.want_result) { + match (eval_test_case(&case, is_rego_v1_test), &case.want_result) { (Ok(actual), Some(expected)) if is_json_schema_test && json_schema_tests_check(&actual, &expected) => {