Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions bindings/csharp/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -578,3 +578,14 @@ var result = policy.EvalWithInput(inputJson);
```

The compiled approach provides better performance for repeated evaluations and clearer resource management.

## Effective Package Override

For engine-based loading, `Engine.AddPolicyWithPackage(path, rego, effectivePackage)`
loads the original policy text under a bare dotted package path such as
`tenant.authz` (not `data.tenant.authz`). This changes the policy's effective
package for evaluation and compilation without changing its source. Absolute
`data.*` references and imports are left unchanged; callers must update those
references themselves if they should point to the overridden package.
For RVM compilation, use `Program.CompileFromEngine(engine, new[] { "data.tenant.authz.allow" })`;
`Program.CompileFromModules` has no package-override option.
64 changes: 64 additions & 0 deletions bindings/csharp/Regorus.Tests/RegorusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,70 @@ public void Rule_conflict_preserves_error_status_and_reports_previous_location()
Assert.IsFalse(ex.Message.Contains('"'));
}

[TestMethod]
public void Add_policy_with_package_evaluates_under_effective_package()
{
using var engine = new Engine();
var package = engine.AddPolicyWithPackage(
"source.rego",
"package original\nallow := input.user == \"alice\"",
"tenant.authz");
engine.SetInputJson("{\"user\":\"alice\"}");

Assert.AreEqual("data.tenant.authz", package);
Assert.AreEqual("true", engine.EvalRule("data.tenant.authz.allow"));
}

[TestMethod]
public void Add_policy_with_package_rejects_embedded_nul_without_adding_policy()
{
using var engine = new Engine();
engine.AddPolicy("existing.rego", "package existing\nallow := true");
var packagesBefore = engine.GetPolicyPackageNames();

Assert.ThrowsException<ArgumentException>(
() => engine.AddPolicyWithPackage(
"source.rego",
"package original\nallow := true",
"tenant.authz\0evil"));

Assert.AreEqual(packagesBefore, engine.GetPolicyPackageNames());
}

[TestMethod]
public void Add_policy_with_package_rejects_embedded_nul_in_rego_without_adding_policy()
{
using var engine = new Engine();
engine.AddPolicy("existing.rego", "package existing\nallow := true");
var packagesBefore = engine.GetPolicyPackageNames();

Assert.ThrowsException<ArgumentException>(
() => engine.AddPolicyWithPackage(
"source.rego",
"package original\nallow := true\0\nallow := false",
"tenant.authz"));

Assert.AreEqual(packagesBefore, engine.GetPolicyPackageNames());
Assert.AreEqual("true", engine.EvalRule("data.existing.allow"));
}

[TestMethod]
public void Add_policy_with_package_rejects_embedded_nul_in_path_without_adding_policy()
{
using var engine = new Engine();
engine.AddPolicy("existing.rego", "package existing\nallow := true");
var packagesBefore = engine.GetPolicyPackageNames();

Assert.ThrowsException<ArgumentException>(
() => engine.AddPolicyWithPackage(
"source.rego\0suffix",
"package original\nallow := true",
"tenant.authz"));

Assert.AreEqual(packagesBefore, engine.GetPolicyPackageNames());
Assert.AreEqual("true", engine.EvalRule("data.existing.allow"));
}

[TestMethod]
public void Evaluation_using_file_policies_succeeds()
{
Expand Down
17 changes: 17 additions & 0 deletions bindings/csharp/Regorus.Tests/RvmProgramTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,23 @@ public void Program_compile_from_engine_succeeds()
Assert.AreEqual("true", result, "expected allow=true");
}

[TestMethod]
public void Program_compile_from_engine_uses_effective_package_override()
{
using var engine = new Engine();
engine.AddPolicyWithPackage(
"source.rego",
"package original\nis_admin(user) := user == \"alice\"\nallow if is_admin(input.user)",
"tenant.authz");

var program = Program.CompileFromEngine(engine, new[] { "data.tenant.authz.allow" });
using var vm = new Rvm();
vm.LoadProgram(program);
vm.SetInputJson("{\"user\":\"alice\"}");

Assert.AreEqual("true", vm.Execute(), "expected the overridden package entrypoint to evaluate");
}

[TestMethod]
public void Program_host_await_suspend_and_resume_succeeds()
{
Expand Down
22 changes: 22 additions & 0 deletions bindings/csharp/Regorus/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,28 @@ public void ClearPolicyLengthConfig()
)));
}

/// <summary>
/// Adds a policy using a bare dotted effective package path, such as
/// <c>tenant.authz</c>. The original policy text is preserved. Absolute
/// <c>data.*</c> references are not rewritten.
/// </summary>
public string? AddPolicyWithPackage(string path, string rego, string effectivePackage)
{
Utf8Marshaller.ThrowIfContainsNul(path, nameof(path));
Utf8Marshaller.ThrowIfContainsNul(rego, nameof(rego));
Utf8Marshaller.ThrowIfContainsNul(effectivePackage, nameof(effectivePackage));
return Utf8Marshaller.WithUtf8(path, pathPtr =>
Utf8Marshaller.WithUtf8(rego, regoPtr =>
Utf8Marshaller.WithUtf8(effectivePackage, packagePtr =>
UseHandle(enginePtr =>
CheckAndDropResult(Regorus.Internal.API.regorus_engine_add_policy_with_package(
(Regorus.Internal.RegorusEngine*)enginePtr,
(byte*)pathPtr,
(byte*)regoPtr,
(byte*)packagePtr))
))));
}

public void SetRegoV0(bool enable)
{
UseHandle(enginePtr =>
Expand Down
6 changes: 6 additions & 0 deletions bindings/csharp/Regorus/NativeMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,12 @@ internal static unsafe partial class API
[DllImport(LibraryName, EntryPoint = "regorus_engine_add_policy", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_add_policy(RegorusEngine* engine, byte* path, byte* rego);

/// <summary>
/// Add a policy with an effective package path override.
/// </summary>
[DllImport(LibraryName, EntryPoint = "regorus_engine_add_policy_with_package", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
internal static extern RegorusResult regorus_engine_add_policy_with_package(RegorusEngine* engine, byte* path, byte* rego, byte* package);

/// <summary>
/// Add a policy from file.
/// </summary>
Expand Down
164 changes: 164 additions & 0 deletions bindings/ffi/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,146 @@ impl Clone for RegorusEngine {
}
}

#[cfg(all(test, feature = "std"))]
mod package_override_tests {
#[cfg(feature = "azure_policy")]
use super::regorus_engine_get_policy_package_names;
use super::{
regorus_engine_add_policy, regorus_engine_add_policy_with_package, regorus_engine_drop,
regorus_engine_get_packages, regorus_engine_get_policies, regorus_engine_new,
};
use crate::common::{regorus_result_drop, RegorusDataType, RegorusStatus};
use core::ffi::c_char;
use core::ptr;
use std::ffi::{CStr, CString};

fn result_text(result: &crate::common::RegorusResult) -> String {
assert!(matches!(&result.status, RegorusStatus::Ok));
assert!(matches!(&result.data_type, RegorusDataType::String));
assert!(!result.output.is_null());
unsafe {
CStr::from_ptr(result.output)
.to_str()
.expect("FFI output should be UTF-8")
.to_owned()
}
}

fn assert_rejected(result: crate::common::RegorusResult) {
assert!(matches!(&result.status, RegorusStatus::Error));
assert!(!result.error_message.is_null());
unsafe {
assert!(!CStr::from_ptr(result.error_message)
.to_str()
.expect("FFI error should be UTF-8")
.is_empty());
}
regorus_result_drop(result);
}

#[test]
fn package_override_ffi_reports_effective_package_and_preserves_source() {
let engine = regorus_engine_new();
assert!(!engine.is_null());

let legacy_path = CString::new("legacy.rego").unwrap();
let legacy_policy = CString::new("package legacy\nvalue := 1").unwrap();
let legacy_result =
regorus_engine_add_policy(engine, legacy_path.as_ptr(), legacy_policy.as_ptr());
assert_eq!(
"data.legacy",
result_text(&legacy_result),
"the existing two-argument symbol keeps declared-package behavior"
);
regorus_result_drop(legacy_result);

let path = CString::new("source.rego").unwrap();
let rego = CString::new("package original.authz\nallowed := true").unwrap();
let package = CString::new("tenant.authz").unwrap();
let added = regorus_engine_add_policy_with_package(
engine,
path.as_ptr(),
rego.as_ptr(),
package.as_ptr(),
);
assert_eq!("data.tenant.authz", result_text(&added));
regorus_result_drop(added);

let packages = regorus_engine_get_packages(engine);
let package_json: serde_json::Value =
serde_json::from_str(&result_text(&packages)).expect("valid package JSON");
assert_eq!(
serde_json::json!(["data.legacy", "data.tenant.authz"]),
package_json
);
regorus_result_drop(packages);

let policies = regorus_engine_get_policies(engine);
let policy_json: serde_json::Value =
serde_json::from_str(&result_text(&policies)).expect("valid policy JSON");
assert_eq!("source.rego", policy_json[1]["path"]);
assert_eq!(
"package original.authz\nallowed := true",
policy_json[1]["contents"]
);
regorus_result_drop(policies);

#[cfg(feature = "azure_policy")]
{
let names = regorus_engine_get_policy_package_names(engine);
let name_json: serde_json::Value =
serde_json::from_str(&result_text(&names)).expect("valid package-name JSON");
assert_eq!("tenant.authz", name_json[1]["package_name"]);
regorus_result_drop(names);
}

regorus_engine_drop(engine);
}

#[test]
fn package_override_ffi_rejects_null_and_malformed_without_adding_modules() {
let engine = regorus_engine_new();
assert!(!engine.is_null());

let existing_path = CString::new("existing.rego").unwrap();
let existing_policy = CString::new("package existing\nvalue := true").unwrap();
let existing =
regorus_engine_add_policy(engine, existing_path.as_ptr(), existing_policy.as_ptr());
assert_eq!("data.existing", result_text(&existing));
regorus_result_drop(existing);

let path = CString::new("invalid.rego").unwrap();
let rego = CString::new("package invalid\nvalue := 1").unwrap();
assert_rejected(regorus_engine_add_policy_with_package(
engine,
path.as_ptr(),
rego.as_ptr(),
ptr::null::<c_char>(),
));

let malformed_package = CString::new("tenant .authz").unwrap();
assert_rejected(regorus_engine_add_policy_with_package(
engine,
path.as_ptr(),
rego.as_ptr(),
malformed_package.as_ptr(),
));

let packages = regorus_engine_get_packages(engine);
let package_json: serde_json::Value =
serde_json::from_str(&result_text(&packages)).expect("valid package JSON");
assert_eq!(serde_json::json!(["data.existing"]), package_json);
regorus_result_drop(packages);

let policies = regorus_engine_get_policies(engine);
let policy_json: serde_json::Value =
serde_json::from_str(&result_text(&policies)).expect("valid policy JSON");
assert_eq!(1, policy_json.as_array().expect("array").len());
regorus_result_drop(policies);
regorus_engine_drop(engine);
}
}

#[cfg(all(test, feature = "contention_checks", feature = "std"))]
mod tests {
use super::RegorusEngine;
Expand Down Expand Up @@ -268,6 +408,30 @@ pub extern "C" fn regorus_engine_add_policy(
})
}

/// Add a policy with an effective package path override.
///
/// `package` is a bare dotted Rego package path (for example `tenant.authz`),
/// without the `data.` prefix or `package` keyword.
#[no_mangle]
pub extern "C" fn regorus_engine_add_policy_with_package(
engine: *mut RegorusEngine,
path: *const c_char,
rego: *const c_char,
package: *const c_char,
) -> RegorusResult {
with_unwind_guard(|| {
to_regorus_string_result(|| -> Result<String> {
let engine = to_shared_ref(engine as *const RegorusEngine)?;
let mut guard = engine.try_write()?;
guard.add_policy_with_package(
from_c_str(path)?,
from_c_str(rego)?,
from_c_str(package)?,
)
}())
})
}

#[cfg(feature = "std")]
#[no_mangle]
pub extern "C" fn regorus_engine_add_policy_from_file(
Expand Down
4 changes: 4 additions & 0 deletions src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,10 @@ pub struct Import {
#[cfg_attr(feature = "ast", derive(serde::Serialize))]
pub struct Module {
pub package: Package,
/// Effective package path used for evaluation, when overridden by the caller.
/// The original package AST is retained so source spans and policy text stay intact.
#[cfg_attr(feature = "ast", serde(skip_serializing_if = "Option::is_none"))]
pub effective_package: Option<String>,
pub imports: Vec<Import>,
#[cfg_attr(feature = "ast", serde(rename(serialize = "rules")))]
pub policy: Vec<Ref<Rule>>,
Expand Down
Loading
Loading