use std::fmt;
use std::num::NonZeroU32;
use std::time::Duration;
use regorus::utils::limits::{ExecutionTimerConfig, LimitError};
use regorus::{Engine, Value as RegoValue};
use serde_json::Value as JsonValue;
use sha2::{Digest, Sha256};
use uuid::Uuid;
use vti_common::error::AppError;
use super::model::PolicyPurpose;
const POLICY_EVAL_TIME_LIMIT: Duration = Duration::from_millis(250);
const POLICY_EVAL_CHECK_INTERVAL: u32 = 1000;
const MAX_POLICY_INPUT_BYTES: usize = 256 * 1024;
pub const POLICY_MODULE_PATH: &str = "policy.rego";
pub struct CompiledPolicy {
id: Uuid,
source_sha256: [u8; 32],
engine: Engine,
}
impl fmt::Debug for CompiledPolicy {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CompiledPolicy")
.field("id", &self.id)
.field("source_sha256", &hex::encode(self.source_sha256))
.finish_non_exhaustive()
}
}
impl CompiledPolicy {
pub fn id(&self) -> Uuid {
self.id
}
pub fn source_sha256(&self) -> &[u8; 32] {
&self.source_sha256
}
}
pub fn compile(rego_source: &str, id: Uuid) -> Result<CompiledPolicy, AppError> {
let mut engine = Engine::new();
engine
.add_policy(POLICY_MODULE_PATH.to_string(), rego_source.to_string())
.map_err(|e| AppError::Validation(format!("rego compile failed for policy {id}: {e}")))?;
let source_sha256: [u8; 32] = Sha256::digest(rego_source.as_bytes()).into();
Ok(CompiledPolicy {
id,
source_sha256,
engine,
})
}
pub fn evaluate(
compiled: &CompiledPolicy,
query: &str,
input: JsonValue,
) -> Result<JsonValue, AppError> {
let input_bytes = serde_json::to_vec(&input)?;
if input_bytes.len() > MAX_POLICY_INPUT_BYTES {
return Err(AppError::ResourceExhausted(format!(
"policy input ({} bytes) exceeds the {MAX_POLICY_INPUT_BYTES}-byte cap",
input_bytes.len()
)));
}
let mut engine = compiled.engine.clone();
engine.set_execution_timer_config(ExecutionTimerConfig {
limit: POLICY_EVAL_TIME_LIMIT,
check_interval: NonZeroU32::new(POLICY_EVAL_CHECK_INTERVAL)
.expect("POLICY_EVAL_CHECK_INTERVAL is non-zero"),
});
engine.set_input(RegoValue::from(input));
let results = engine.eval_query(query.to_string(), false).map_err(|e| {
if e.downcast_ref::<LimitError>().is_some() {
AppError::ResourceExhausted(format!(
"policy {} evaluation exceeded its resource budget",
compiled.id
))
} else {
AppError::Internal(format!(
"rego evaluation failed for policy {}: {e}",
compiled.id
))
}
})?;
serde_json::to_value(results).map_err(AppError::from)
}
pub fn validate_purpose_package(
compiled: &CompiledPolicy,
purpose: PolicyPurpose,
) -> Result<(), AppError> {
let Some(pkg) = purpose.expected_package() else {
return Ok(());
};
if yields_decision_or_allow(compiled, pkg) {
return Ok(());
}
Err(AppError::Validation(format!(
"policy declares purpose `{p}` but yields no decision in package `{pkg}` for a \
trivial input — it must define a `decision` rule (or a boolean `allow`) under \
`package {pkg}`. A module in the wrong package compiles cleanly but silently \
denies every `{p}` request.",
p = purpose.as_str(),
)))
}
fn yields_decision_or_allow(compiled: &CompiledPolicy, pkg: &str) -> bool {
let empty = JsonValue::Object(serde_json::Map::new());
if let Ok(r) = evaluate(compiled, &format!("data.{pkg}.decision"), empty.clone())
&& r.pointer("/result/0/expressions/0/value")
.and_then(|v| v.get("effect"))
.and_then(JsonValue::as_str)
.is_some()
{
return true;
}
if let Ok(r) = evaluate(compiled, &format!("data.{pkg}.allow"), empty)
&& r.pointer("/result/0/expressions/0/value")
.and_then(JsonValue::as_bool)
.is_some()
{
return true;
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
const ALLOW_POLICY: &str = "\
package vtc.test
import rego.v1
default allow := false
allow if input.role == \"admin\"
";
const DENY_POLICY: &str = "\
package vtc.test
import rego.v1
default allow := false
allow if {
input.role == \"admin\"
input.context == \"prod\"
}
";
fn test_id() -> Uuid {
Uuid::from_u128(0x0102_0304_0506_0708_0900_0a0b_0c0d_0e0f)
}
#[test]
fn compile_happy_path() {
let id = test_id();
let compiled = compile(ALLOW_POLICY, id).expect("compile should succeed");
assert_eq!(compiled.id(), id);
let expected: [u8; 32] = Sha256::digest(ALLOW_POLICY.as_bytes()).into();
assert_eq!(compiled.source_sha256(), &expected);
}
#[test]
fn compile_surfaces_parse_error() {
let id = test_id();
let err = compile("not valid rego @@@ }}}", id).expect_err("malformed source must fail");
match err {
AppError::Validation(msg) => {
assert!(
msg.contains(&id.to_string()),
"error message should name the policy id: {msg}"
);
assert!(
msg.contains("rego compile failed"),
"error message should be a compile-failure: {msg}"
);
}
other => panic!("expected Validation error, got {other:?}"),
}
}
#[test]
fn evaluate_allow_true() {
let compiled = compile(ALLOW_POLICY, test_id()).unwrap();
let result = evaluate(&compiled, "data.vtc.test.allow", json!({ "role": "admin" }))
.expect("evaluate must succeed");
let value = pluck_expression_value(&result);
assert_eq!(value, &json!(true));
}
#[test]
fn evaluate_allow_false() {
let compiled = compile(DENY_POLICY, test_id()).unwrap();
let result = evaluate(
&compiled,
"data.vtc.test.allow",
json!({ "role": "admin", "context": "staging" }),
)
.expect("evaluate must succeed");
let value = pluck_expression_value(&result);
assert_eq!(value, &json!(false));
}
#[test]
fn evaluate_undefined_returns_empty_and_malformed_query_errors() {
let compiled = compile(ALLOW_POLICY, test_id()).unwrap();
let ok = evaluate(&compiled, "data.vtc.test.does_not_exist", json!({}))
.expect("undefined symbols must not surface as an error");
let value = ok.pointer("/result/0/expressions/0/value");
assert!(
value.is_none() || matches!(value, Some(JsonValue::Object(o)) if o.is_empty()),
"undefined rule should yield no value, got {ok}"
);
let err = evaluate(&compiled, "@@@ not a query @@@", json!({}))
.expect_err("malformed query must fail");
match err {
AppError::Internal(msg) => {
assert!(
msg.contains("rego evaluation failed"),
"error message should be an evaluation failure: {msg}"
);
}
other => panic!("expected Internal error, got {other:?}"),
}
}
#[test]
fn compile_sha_is_deterministic() {
let a = compile(ALLOW_POLICY, Uuid::new_v4()).unwrap();
let b = compile(ALLOW_POLICY, Uuid::new_v4()).unwrap();
assert_eq!(a.source_sha256(), b.source_sha256());
let c = compile(DENY_POLICY, Uuid::new_v4()).unwrap();
assert_ne!(a.source_sha256(), c.source_sha256());
}
#[test]
fn evaluate_rejects_oversized_input() {
let compiled = compile(ALLOW_POLICY, test_id()).unwrap();
let blob = "x".repeat(MAX_POLICY_INPUT_BYTES + 1);
let err = evaluate(&compiled, "data.vtc.test.allow", json!({ "blob": blob }))
.expect_err("oversized input must be rejected");
assert!(
matches!(err, AppError::ResourceExhausted(_)),
"expected ResourceExhausted, got {err:?}"
);
let ok = evaluate(&compiled, "data.vtc.test.allow", json!({ "role": "admin" }))
.expect("normal input must still evaluate");
assert_eq!(pluck_expression_value(&ok), &json!(true));
}
#[test]
fn evaluate_aborts_runaway_policy() {
const RUNAWAY: &str = "\
package vtc.test
import rego.v1
xs := numbers.range(1, 10000)
allow if {
count([1 | some i in xs; some j in xs; i == j]) >= 0
}
";
let compiled = compile(RUNAWAY, test_id()).unwrap();
let err = evaluate(&compiled, "data.vtc.test.allow", json!({}))
.expect_err("runaway policy must abort");
assert!(
matches!(err, AppError::ResourceExhausted(_)),
"expected ResourceExhausted, got {err:?}"
);
}
fn pluck_expression_value(results: &JsonValue) -> &JsonValue {
results
.pointer("/result/0/expressions/0/value")
.expect("regorus QueryResults must carry result[0].expressions[0].value")
}
#[test]
fn validate_purpose_package_accepts_boolean_allow_in_right_package() {
let src = "package vtc.join\nimport rego.v1\ndefault allow := false\n";
let c = compile(src, test_id()).unwrap();
assert!(validate_purpose_package(&c, PolicyPurpose::Join).is_ok());
}
#[test]
fn validate_purpose_package_accepts_decision_rule_in_right_package() {
let src = "package vtc.directory\nimport rego.v1\n\
default decision := {\"effect\": \"deny\"}\n";
let c = compile(src, test_id()).unwrap();
assert!(validate_purpose_package(&c, PolicyPurpose::Directory).is_ok());
}
#[test]
fn validate_purpose_package_rejects_wrong_package() {
let src = "package vtc.removal\nimport rego.v1\ndefault allow := false\n";
let c = compile(src, test_id()).unwrap();
let err = validate_purpose_package(&c, PolicyPurpose::Join).unwrap_err();
match err {
AppError::Validation(msg) => assert!(
msg.contains("vtc.join"),
"error must name the expected package: {msg}"
),
other => panic!("expected Validation, got {other:?}"),
}
}
#[test]
fn validate_purpose_package_rejects_missing_default_rule() {
let src = "package vtc.join\nimport rego.v1\nallow if input.role == \"admin\"\n";
let c = compile(src, test_id()).unwrap();
assert!(validate_purpose_package(&c, PolicyPurpose::Join).is_err());
}
#[test]
fn validate_purpose_package_skips_unpinned_purposes() {
let src = "package whatever\nimport rego.v1\ndefault publish_on_join := true\n";
let c = compile(src, test_id()).unwrap();
assert!(validate_purpose_package(&c, PolicyPurpose::Registry).is_ok());
}
}