Rego policies
Open Policy Agent's Rego through regorus: a policy evaluated with input and data, undefined told from false, any query with its bindings, a package's rules listed, policies kept compiled by name, and errors with their line numbers.
Version 1.0.0 · 7 functions · 2 crates
Functions
What use rego.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use rego_eval, rego_query from rego (Quillo 0.6).
rego_eval(policies: any, data: any, input: any, rule: str) → anyor an errorOne rule's value: rego_eval(policy, data, input, "data.shop.allow") gives { defined, value, prints }, defined being false for a rule whose bodies all failed, as Rego has no false by default. prints is what print() wrote. A rule that does not exist is an error, and so is a policy that does not compile, with its line.
rego_query(policies: any, data: any, input: any, query: str) → anyor an errorAny Rego query: { results: [{ values, bindings }], prints }, one result per way of satisfying it: rego_query(p, d, i, "x := data.shop.reasons[_]").
rego_allowed(policies: any, data: any, input: any, rule: str) → boolor an errorWhether a boolean rule holds: true only if the rule is defined and true. Undefined is false, so a deny-by-default rule stays that way.
rego_load(name: str, policies: any, data: any) → listor an errorCompiles policies once and keeps them under a name, with their data, for many decisions. Gives the packages.
rego_decide(name: str, input: any, rule: str) → anyor an errorrego_eval on policies kept by rego_load; each call works on its own copy, so requests do not wait for each other.
rego_rules(policies: any) → listor an errorThe rules of each package: [{ package, file, name, kind, line, default }], kind being rule, set, object or function. Several bodies of one rule are listed once, with bodies counted.
rego_check(policies: any) → anyCompiles without running: { ok, errors }, each error with file, line, column and message.
Get it
$ quillo install rego # rego.qwer into this folder$ quillo install rego --example # and its example, rego.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/rego.qwer$ curl -O https://quillolang.org/plugins/files/rego.qweThen, in your program, all of it or only what you call:
use rego.qweruse rego_eval, rego_query from rego // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
serde_json = { version = "1", features = ["preserve_order"] }regorus = { version = "=0.14.0", default-features = false, features = ["arc", "ast", "full-opa"] }Example
// An access policy in Rego, as Open Policy Agent runs it: decisions with input and data, the reasons// behind them, the package's rules listed, and a policy with a mistake placed by its line.//// quillo run rego.qwe then open http://localhost:8080/decide, /reasons, /rules, /kept and /brokenuse "rego.qwer"let POLICY = `package shop.authzimport rego.v1default allow := falseallow if { input.user in data.admins}allow if { input.method == "GET" startswith(input.path, "/public/")}deny contains msg if { input.user in data.blocked msg := sprintf("%s is blocked", [input.user])}deny contains "writes are off on Sundays" if { input.method != "GET" input.day == "sunday"}limit(tier) := 100 if tier == "free"limit(tier) := 1000 if tier == "paid"quota := limit(input.tier) if print("tier", input.tier)`let DATA = { admins: ["ada"], blocked: ["mallory"] }route GET /decide = { admin: rego_allowed(POLICY, DATA, { user: "ada", method: "POST", path: "/orders" }, "data.shop.authz.allow")?, public: rego_allowed(POLICY, DATA, { user: "bob", method: "GET", path: "/public/a" }, "data.shop.authz.allow")?, refused: rego_allowed(POLICY, DATA, { user: "bob", method: "POST", path: "/orders" }, "data.shop.authz.allow")?, quota: rego_eval(POLICY, DATA, { tier: "paid" }, "data.shop.authz.quota")?, undefined: rego_eval(POLICY, DATA, { tier: "gold" }, "data.shop.authz.quota")?,}route GET /reasons = { deny: rego_eval(POLICY, DATA, { user: "mallory", method: "POST", day: "sunday" }, "data.shop.authz.deny")?.value, each: rego_query(POLICY, DATA, { user: "mallory", method: "POST", day: "sunday" }, "why := data.shop.authz.deny[_]")?.results | map((r) => r.bindings.why),}route GET /rules = rego_rules({ "authz.rego": POLICY })? | map((r) => { name: r.name, kind: r.kind, line: r.line, default: r.default, bodies: r.bodies })// Compiled once at start; each request gets a decision from the kept policies.let LOADED = rego_load("shop", POLICY, DATA)route GET /kept = { packages: LOADED?, allow: rego_decide("shop", { user: "ada", method: "GET", path: "/x" }, "data.shop.authz.allow")?.value, none: rego_decide("nothing", {}, "data.x").error,}route GET /broken = { syntax: rego_check("package a\n\nallow if {\n input.x ==\n}\n"), unsafe_var: rego_check("package a\nimport rego.v1\n\nallow if {\n x == 1\n}\n"), refused: rego_eval("package a\n\nallow := = 1\n", null, {}, "data.a.allow").error, no_rule: rego_eval("package a\nx := 1\n", null, {}, "data.b.y").error, fine: rego_check(POLICY).ok,}$ quillo run rego.qwe$ curl 'localhost:8080/decide'$ curl 'localhost:8080/reasons'$ curl 'localhost:8080/rules'$ curl 'localhost:8080/kept'$ curl 'localhost:8080/broken'Source
//! Open Policy Agent's Rego, through regorus: a policy evaluated with input and data, a package's rules listed, policies kept compiled by name, and errors with their line numbers.//! version: 1.0.0//!//! [dependencies]//! serde_json = { version = "1", features = ["preserve_order"] }//! regorus = { version = "=0.14.0", default-features = false, features = ["arc", "ast", "full-opa"] }use regorus::{Engine, Value as RegoValue};use serde_json::{json, Value};use std::collections::HashMap;use std::sync::Mutex;static NAMED: Mutex<Option<HashMap<String, Engine>>> = Mutex::new(None);/// regorus writes a mistake as `--> file:line:col`, a picture of the line, and `error: message`. Split up.fn placed(e: &str) -> Value { let (mut file, mut line, mut column) = (Value::Null, Value::Null, Value::Null); if let Some(l) = e.lines().find(|l| l.trim_start().starts_with("-->")) { let t = l.trim_start().trim_start_matches("-->").trim(); let bits: Vec<&str> = t.rsplitn(3, ':').collect(); if bits.len() == 3 { column = json!(bits[0].parse::<u32>().ok()); line = json!(bits[1].parse::<u32>().ok()); file = json!(bits[2]); } } let last = e.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("").trim(); let message = last.strip_prefix("error:").unwrap_or(last).trim(); json!({ "file": file, "line": line, "column": column, "message": message })}fn one_line(e: &str) -> String { let p = placed(e); match p["line"].as_u64() { Some(l) => format!("{} line {}, column {}: {}", p["file"].as_str().unwrap_or(""), l, p["column"], p["message"].as_str().unwrap_or("")), None => p["message"].as_str().unwrap_or(e).to_string(), }}fn to_rego(v: &Value) -> Result<RegoValue, String> { RegoValue::from_json_str(&v.to_string()).map_err(|e| e.to_string())}fn to_json(v: &RegoValue) -> Result<Value, String> { let t = v.to_json_str().map_err(|e| e.to_string())?; serde_json::from_str(&t).map_err(|e| e.to_string())}/// An engine with `policies`: one text, or a map of file names to texts, or a list of `{ name, text }`.fn engine(policies: &Value, data: &Value) -> Result<Engine, String> { let mut e = Engine::new(); let mut add = |name: &str, text: &str| e.add_policy(name.to_string(), text.to_string()).map(|_| ()).map_err(|x| one_line(&x.to_string())); match policies { Value::String(t) => add("policy.rego", t)?, Value::Object(m) => { for (k, v) in m { add(k, v.as_str().ok_or(format!("policy {} is not text", k))?)?; } } Value::Array(a) => { for p in a { add(p["name"].as_str().unwrap_or("policy.rego"), p["text"].as_str().ok_or("a policy has no text")?)?; } } _ => return Err("policies should be text, or a map of file names to texts".into()), } if !data.is_null() { if !data.is_object() { return Err("data must be a map".into()); } e.add_data(to_rego(data)?).map_err(|x| x.to_string())?; } e.set_gather_prints(true); Ok(e)}fn run_rule(e: &mut Engine, input: &Value, rule: &str) -> Result<Value, String> { e.set_input(to_rego(input)?); let v = e.eval_rule(rule.to_string()).map_err(|x| format!("`{}`: {}", rule, one_line(&x.to_string())))?; let prints = e.take_prints().unwrap_or_default(); if v == RegoValue::Undefined { return Ok(json!({ "defined": false, "value": null, "prints": prints })); } Ok(json!({ "defined": true, "value": to_json(&v)?, "prints": prints }))}fn run_query(e: &mut Engine, input: &Value, query: &str) -> Result<Value, String> { e.set_input(to_rego(input)?); let r = e.eval_query(query.to_string(), false).map_err(|x| one_line(&x.to_string()))?; let mut rows = vec![]; for row in &r.result { let bindings = to_json(&row.bindings)?; let exprs: Vec<Value> = row.expressions.iter().map(|x| to_json(&x.value)).collect::<Result<_, _>>()?; rows.push(json!({ "values": exprs, "bindings": bindings })); } let prints = e.take_prints().unwrap_or_default(); Ok(json!({ "results": rows, "prints": prints }))}/// One rule's value: `rego_eval(policy, data, input, "data.shop.allow")` gives `{ defined, value, prints }`, `defined` being false for a rule whose bodies all failed, as Rego has no false by default. `prints` is what `print()` wrote. A rule that does not exist is an error, and so is a policy that does not compile, with its line.pub fn rego_eval(policies: serde_json::Value, data: serde_json::Value, input: serde_json::Value, rule: &str) -> Result<Value, String> { let mut e = engine(&policies, &data)?; run_rule(&mut e, &input, rule)}/// Any Rego query: `{ results: [{ values, bindings }], prints }`, one result per way of satisfying it: `rego_query(p, d, i, "x := data.shop.reasons[_]")`.pub fn rego_query(policies: serde_json::Value, data: serde_json::Value, input: serde_json::Value, query: &str) -> Result<Value, String> { let mut e = engine(&policies, &data)?; run_query(&mut e, &input, query)}/// Whether a boolean rule holds: true only if the rule is defined and true. Undefined is false, so a deny-by-default rule stays that way.pub fn rego_allowed(policies: serde_json::Value, data: serde_json::Value, input: serde_json::Value, rule: &str) -> Result<bool, String> { Ok(rego_eval(policies, data, input, rule)?["value"] == json!(true))}/// Compiles policies once and keeps them under a name, with their data, for many decisions. Gives the packages.pub fn rego_load(name: &str, policies: serde_json::Value, data: serde_json::Value) -> Result<Vec<String>, String> { let e = engine(&policies, &data)?; let packages = e.get_packages().map_err(|x| x.to_string())?; NAMED.lock().unwrap().get_or_insert_with(HashMap::new).insert(name.to_string(), e); Ok(packages)}/// `rego_eval` on policies kept by `rego_load`; each call works on its own copy, so requests do not wait for each other.pub fn rego_decide(name: &str, input: serde_json::Value, rule: &str) -> Result<Value, String> { let mut e = NAMED.lock().unwrap().as_ref().and_then(|m| m.get(name).cloned()).ok_or(format!("no policies are loaded as `{}`", name))?; run_rule(&mut e, &input, rule)}fn ref_text(head: &Value) -> Option<String> { let span = head["refr"]["span"].clone(); span.get("text").and_then(Value::as_str).map(str::to_string)}/// The rules of each package: `[{ package, file, name, kind, line, default }]`, `kind` being rule, set, object or function. Several bodies of one rule are listed once, with `bodies` counted.pub fn rego_rules(policies: serde_json::Value) -> Result<Vec<Value>, String> { let e = engine(&policies, &Value::Null)?; let text = e.get_ast_as_json().map_err(|x| x.to_string())?; let ast: Value = serde_json::from_str(&text).map_err(|x| x.to_string())?; let packages = e.get_packages().map_err(|x| x.to_string())?; let mut out: Vec<Value> = vec![]; for (i, m) in ast.as_array().into_iter().flatten().enumerate() { let package = packages.get(i).cloned().unwrap_or_default(); let file = m["source"]["path"].as_str().or(m["source"]["file"].as_str()).unwrap_or("").to_string(); for rule in m["ast"]["rules"].as_array().into_iter().flatten() { let (name, kind, line, default) = if let Some(s) = rule.get("Spec") { let head = &s["head"]; let (h, kind) = if let Some(c) = head.get("Compr") { (c, if c["refr"].get("RefBrack").is_some() { "object" } else { "rule" }) } else if let Some(c) = head.get("Set") { (c, "set") } else { (&head["Func"], "function") }; (rule_name(h), kind, s["span"]["line"].clone(), false) } else if let Some(d) = rule.get("Default") { (rule_name(d), "rule", d["span"]["line"].clone(), true) } else { continue; }; if let Some(x) = out.iter_mut().find(|x| x["package"] == json!(package) && x["name"] == json!(name)) { if default { x["default"] = json!(true); } else { x["bodies"] = json!(x["bodies"].as_i64().unwrap_or(0) + 1); } continue; } out.push(json!({ "package": package, "file": file, "name": name, "kind": kind, "line": line, "default": default, "bodies": if default { 0 } else { 1 } })); } } Ok(out)}fn rule_name(h: &Value) -> String { ref_text(h).unwrap_or_else(|| { // A Var in the AST is [span, name]. let r = &h["refr"]; r["Var"][1].as_str().map(str::to_string).unwrap_or_else(|| first_string(r)) })}fn first_string(v: &Value) -> String { match v { Value::String(s) => s.clone(), Value::Array(a) => a.iter().map(first_string).find(|s| !s.is_empty()).unwrap_or_default(), Value::Object(m) => m.iter().filter(|(k, _)| *k != "span").map(|(_, x)| first_string(x)).find(|s| !s.is_empty()).unwrap_or_default(), _ => String::new(), }}/// Compiles without running: `{ ok, errors }`, each error with `file`, `line`, `column` and `message`.pub fn rego_check(policies: serde_json::Value) -> Value { let mut problems = vec![]; let list: Vec<(String, String)> = match &policies { Value::String(t) => vec![("policy.rego".to_string(), t.clone())], Value::Object(m) => m.iter().map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string())).collect(), _ => vec![], }; for (name, text) in list { let mut e = Engine::new(); if let Err(x) = e.add_policy(name.clone(), text) { problems.push(placed(&x.to_string())); continue; } // Names are resolved when a query first prepares the engine. if let Err(x) = e.eval_query("1 == 1".to_string(), false) { problems.push(placed(&x.to_string())); } } json!({ "ok": problems.is_empty(), "errors": problems })}Related
Thread 0 posts
On Rego policies: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.