From dbce6e8752f6fe1902142f7caea45707152b66e0 Mon Sep 17 00:00:00 2001 From: Victor Yang Date: Thu, 11 Jun 2026 11:36:45 +0800 Subject: [PATCH 1/3] feat: add examples for RBAC combined with ABAC context rules Add model, policy and request example files demonstrating how context-based ABAC rules can be layered on top of RBAC roles in a single combined model. --- examples/rbac_with_abac_rule_model.conf | 14 ++++++++++++++ examples/rbac_with_abac_rule_policy.csv | 14 ++++++++++++++ examples/rbac_with_abac_rule_request_example.csv | 9 +++++++++ 3 files changed, 37 insertions(+) create mode 100644 examples/rbac_with_abac_rule_model.conf create mode 100644 examples/rbac_with_abac_rule_policy.csv create mode 100644 examples/rbac_with_abac_rule_request_example.csv diff --git a/examples/rbac_with_abac_rule_model.conf b/examples/rbac_with_abac_rule_model.conf new file mode 100644 index 00000000..8de900b4 --- /dev/null +++ b/examples/rbac_with_abac_rule_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act, ctx + +[policy_definition] +p = sub, obj, act, ctx_rule, eft + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + +[matchers] +m = g(r.sub, p.sub) && (r.obj == p.obj) && (r.act == p.act || p.act == "*") && (p.ctx_rule == "noRule" || eval(p.ctx_rule)) \ No newline at end of file diff --git a/examples/rbac_with_abac_rule_policy.csv b/examples/rbac_with_abac_rule_policy.csv new file mode 100644 index 00000000..9ec53782 --- /dev/null +++ b/examples/rbac_with_abac_rule_policy.csv @@ -0,0 +1,14 @@ +p, admin, data1, read, noRule, allow +p, admin, data1, read, r.ctx.age < 18 || r.ctx.type == "minor", deny + +p, admin, data2, write, noRule, allow +p, admin, data2, write, r.ctx.network == "http", deny + +p, admin, data3, *, noRule, allow + +p, user, data4, read, noRule, allow +p, user, data4, read, r.ctx.RiskStatus == "high", deny + +g, alice, admin +g, alice, user +g, bob, admin \ No newline at end of file diff --git a/examples/rbac_with_abac_rule_request_example.csv b/examples/rbac_with_abac_rule_request_example.csv new file mode 100644 index 00000000..c9821986 --- /dev/null +++ b/examples/rbac_with_abac_rule_request_example.csv @@ -0,0 +1,9 @@ +alice, data1, read, {} +alice, data1, read, {"age":18,"type":"minor"} +alice, data2, read, {} +alice, data2, write, {} +alice, data2, write, {"network":"http"} +alice, data3, read, {} +alice, data3, write, {} +alice, data4, read, {} +alice, data4, read, {"RiskStatus":"high"} \ No newline at end of file From b8622e5e85788c95f27e58fbb38fa1494c02f06a Mon Sep 17 00:00:00 2001 From: Victor Yang Date: Mon, 22 Jun 2026 14:51:21 +0800 Subject: [PATCH 2/3] test: add tests for RBAC combined with ABAC context rules Add coverage for examples/rbac_with_abac_rule_model.conf in AbacAPIUnitTest#testRBACWithABACRule, verifying that the combined RBAC + ABAC context rule model correctly enforces allow/deny based on request context (minor, network, RiskStatus) alongside role inheritance and wildcard actions. --- .../casbin/jcasbin/main/AbacAPIUnitTest.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/test/java/org/casbin/jcasbin/main/AbacAPIUnitTest.java b/src/test/java/org/casbin/jcasbin/main/AbacAPIUnitTest.java index 4833936a..1685f3a8 100644 --- a/src/test/java/org/casbin/jcasbin/main/AbacAPIUnitTest.java +++ b/src/test/java/org/casbin/jcasbin/main/AbacAPIUnitTest.java @@ -21,6 +21,8 @@ import static org.casbin.jcasbin.main.TestUtil.testDomainEnforce; import static org.casbin.jcasbin.main.TestUtil.testEnforce; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; public class AbacAPIUnitTest { @Test @@ -79,6 +81,55 @@ public void testABACMapRequest() { testEnforce(e, "bob", data2, "write", true); } + @Test + public void testRBACWithABACRule() { + // rbac_with_abac_rule_model combines RBAC (g) with ABAC context rules (p.ctx_rule). + // The matcher evaluates a context rule as a per-request allow/deny filter. + Enforcer e = new Enforcer( + "examples/rbac_with_abac_rule_model.conf", + "examples/rbac_with_abac_rule_policy.csv" + ); + + Map emptyCtx = new HashMap<>(); + + Map minorCtx = new HashMap<>(); + minorCtx.put("age", 18); + minorCtx.put("type", "minor"); + + Map httpCtx = new HashMap<>(); + httpCtx.put("network", "http"); + + Map highRiskCtx = new HashMap<>(); + highRiskCtx.put("RiskStatus", "high"); + + // alice has roles {admin, user}; bob has role {admin}. + + // admin/data1/read: allow under noRule, deny when context matches r.ctx.age < 18 || r.ctx.type == "minor". + assertTrue(e.enforce("alice", "data1", "read", emptyCtx)); + assertFalse(e.enforce("alice", "data1", "read", minorCtx)); + + // admin/data2: no policy for "read" so it is denied; "write" is allowed under noRule + // and denied when r.ctx.network == "http". + assertFalse(e.enforce("alice", "data2", "read", emptyCtx)); + assertTrue(e.enforce("alice", "data2", "write", emptyCtx)); + assertFalse(e.enforce("alice", "data2", "write", httpCtx)); + + // admin/data3/* : wildcard action matches any act, allowed under noRule. + assertTrue(e.enforce("alice", "data3", "read", emptyCtx)); + assertTrue(e.enforce("alice", "data3", "write", emptyCtx)); + + // user/data4/read: allowed under noRule, denied when r.ctx.RiskStatus == "high". + assertTrue(e.enforce("alice", "data4", "read", emptyCtx)); + assertFalse(e.enforce("alice", "data4", "read", highRiskCtx)); + + // bob is admin only, so he can use admin policies but not user policies. + assertTrue(e.enforce("bob", "data1", "read", emptyCtx)); + assertFalse(e.enforce("bob", "data4", "read", emptyCtx)); + + // Unknown resource has no matching policy -> denied. + assertFalse(e.enforce("alice", "data5", "read", emptyCtx)); + } + public static class TestEvalRule { private String name; private int age; From f933f9ce12984ce9f4d56c3959bc0413d048cd5d Mon Sep 17 00:00:00 2001 From: Victor Yang Date: Mon, 22 Jun 2026 15:59:29 +0800 Subject: [PATCH 3/3] test: add RBAC + ABAC context rule tests and sync with Go casbin Add coverage for examples/rbac_with_abac_rule_model.conf in AbacAPIUnitTest#testRBACWithABACRule, ported from casbin Go's rbac_with_abac_rule_test.go so the two suites stay assertion-compatible (13 enforce calls with matching sub/obj/act/ctx/res tuples, plus matching helper names: makeRBACABACCtx and testRBACWithABACRuleEnforce, the latter promoted to TestUtil alongside testEnforce / testEnforceWithContext). Also remove examples/rbac_with_abac_rule_request_example.csv from both repos. The file was unused (zero references in either codebase) and the only request_example of its kind, so it served no documentation or fixture role. --- examples/rbac_with_abac_rule_policy.csv | 8 +-- .../rbac_with_abac_rule_request_example.csv | 9 --- .../casbin/jcasbin/main/AbacAPIUnitTest.java | 57 +++++++++++-------- .../org/casbin/jcasbin/main/TestUtil.java | 4 ++ 4 files changed, 40 insertions(+), 38 deletions(-) delete mode 100644 examples/rbac_with_abac_rule_request_example.csv diff --git a/examples/rbac_with_abac_rule_policy.csv b/examples/rbac_with_abac_rule_policy.csv index 9ec53782..bbf3693e 100644 --- a/examples/rbac_with_abac_rule_policy.csv +++ b/examples/rbac_with_abac_rule_policy.csv @@ -1,14 +1,14 @@ p, admin, data1, read, noRule, allow -p, admin, data1, read, r.ctx.age < 18 || r.ctx.type == "minor", deny +p, admin, data1, read, r.ctx.age < 18 || r.ctx.type == 'minor', deny p, admin, data2, write, noRule, allow -p, admin, data2, write, r.ctx.network == "http", deny +p, admin, data2, write, r.ctx.network == 'http', deny p, admin, data3, *, noRule, allow p, user, data4, read, noRule, allow -p, user, data4, read, r.ctx.RiskStatus == "high", deny +p, user, data4, read, r.ctx.RiskStatus == 'high', deny g, alice, admin g, alice, user -g, bob, admin \ No newline at end of file +g, bob, admin diff --git a/examples/rbac_with_abac_rule_request_example.csv b/examples/rbac_with_abac_rule_request_example.csv deleted file mode 100644 index c9821986..00000000 --- a/examples/rbac_with_abac_rule_request_example.csv +++ /dev/null @@ -1,9 +0,0 @@ -alice, data1, read, {} -alice, data1, read, {"age":18,"type":"minor"} -alice, data2, read, {} -alice, data2, write, {} -alice, data2, write, {"network":"http"} -alice, data3, read, {} -alice, data3, write, {} -alice, data4, read, {} -alice, data4, read, {"RiskStatus":"high"} \ No newline at end of file diff --git a/src/test/java/org/casbin/jcasbin/main/AbacAPIUnitTest.java b/src/test/java/org/casbin/jcasbin/main/AbacAPIUnitTest.java index 1685f3a8..1eb05fb5 100644 --- a/src/test/java/org/casbin/jcasbin/main/AbacAPIUnitTest.java +++ b/src/test/java/org/casbin/jcasbin/main/AbacAPIUnitTest.java @@ -21,8 +21,7 @@ import static org.casbin.jcasbin.main.TestUtil.testDomainEnforce; import static org.casbin.jcasbin.main.TestUtil.testEnforce; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertTrue; +import static org.casbin.jcasbin.main.TestUtil.testRBACWithABACRuleEnforce; public class AbacAPIUnitTest { @Test @@ -85,49 +84,57 @@ public void testABACMapRequest() { public void testRBACWithABACRule() { // rbac_with_abac_rule_model combines RBAC (g) with ABAC context rules (p.ctx_rule). // The matcher evaluates a context rule as a per-request allow/deny filter. + // + // Ported from casbin/rbac_with_abac_rule_test.go (Go). In jcasbin (Aviator 5.9.0) + // `null < 18` evaluates to true, so an empty HashMap would incorrectly trip the + // data1/read deny rule. The Go test sidesteps the same problem in govaluate + // (which throws on missing keys) by always supplying a neutral context; we do + // the same here so the two test suites stay assertion-compatible. Enforcer e = new Enforcer( "examples/rbac_with_abac_rule_model.conf", "examples/rbac_with_abac_rule_policy.csv" ); - Map emptyCtx = new HashMap<>(); - - Map minorCtx = new HashMap<>(); - minorCtx.put("age", 18); - minorCtx.put("type", "minor"); - - Map httpCtx = new HashMap<>(); - httpCtx.put("network", "http"); - - Map highRiskCtx = new HashMap<>(); - highRiskCtx.put("RiskStatus", "high"); + Map neutralCtx = makeRBACABACCtx(100, "adult", "https", "low"); + Map minorCtx = makeRBACABACCtx(18, "minor", "https", "low"); + Map httpCtx = makeRBACABACCtx(100, "adult", "http", "low"); + Map highRiskCtx = makeRBACABACCtx(100, "adult", "https", "high"); // alice has roles {admin, user}; bob has role {admin}. // admin/data1/read: allow under noRule, deny when context matches r.ctx.age < 18 || r.ctx.type == "minor". - assertTrue(e.enforce("alice", "data1", "read", emptyCtx)); - assertFalse(e.enforce("alice", "data1", "read", minorCtx)); + testRBACWithABACRuleEnforce(e, "alice", "data1", "read", neutralCtx, true); + testRBACWithABACRuleEnforce(e, "alice", "data1", "read", minorCtx, false); // admin/data2: no policy for "read" so it is denied; "write" is allowed under noRule // and denied when r.ctx.network == "http". - assertFalse(e.enforce("alice", "data2", "read", emptyCtx)); - assertTrue(e.enforce("alice", "data2", "write", emptyCtx)); - assertFalse(e.enforce("alice", "data2", "write", httpCtx)); + testRBACWithABACRuleEnforce(e, "alice", "data2", "read", neutralCtx, false); + testRBACWithABACRuleEnforce(e, "alice", "data2", "write", neutralCtx, true); + testRBACWithABACRuleEnforce(e, "alice", "data2", "write", httpCtx, false); // admin/data3/* : wildcard action matches any act, allowed under noRule. - assertTrue(e.enforce("alice", "data3", "read", emptyCtx)); - assertTrue(e.enforce("alice", "data3", "write", emptyCtx)); + testRBACWithABACRuleEnforce(e, "alice", "data3", "read", neutralCtx, true); + testRBACWithABACRuleEnforce(e, "alice", "data3", "write", neutralCtx, true); // user/data4/read: allowed under noRule, denied when r.ctx.RiskStatus == "high". - assertTrue(e.enforce("alice", "data4", "read", emptyCtx)); - assertFalse(e.enforce("alice", "data4", "read", highRiskCtx)); + testRBACWithABACRuleEnforce(e, "alice", "data4", "read", neutralCtx, true); + testRBACWithABACRuleEnforce(e, "alice", "data4", "read", highRiskCtx, false); // bob is admin only, so he can use admin policies but not user policies. - assertTrue(e.enforce("bob", "data1", "read", emptyCtx)); - assertFalse(e.enforce("bob", "data4", "read", emptyCtx)); + testRBACWithABACRuleEnforce(e, "bob", "data1", "read", neutralCtx, true); + testRBACWithABACRuleEnforce(e, "bob", "data4", "read", neutralCtx, false); // Unknown resource has no matching policy -> denied. - assertFalse(e.enforce("alice", "data5", "read", emptyCtx)); + testRBACWithABACRuleEnforce(e, "alice", "data5", "read", neutralCtx, false); + } + + private static Map makeRBACABACCtx(int age, String typ, String network, String risk) { + Map ctx = new HashMap<>(); + ctx.put("age", age); + ctx.put("type", typ); + ctx.put("network", network); + ctx.put("RiskStatus", risk); + return ctx; } public static class TestEvalRule { diff --git a/src/test/java/org/casbin/jcasbin/main/TestUtil.java b/src/test/java/org/casbin/jcasbin/main/TestUtil.java index e86d1bea..ee54e8e4 100644 --- a/src/test/java/org/casbin/jcasbin/main/TestUtil.java +++ b/src/test/java/org/casbin/jcasbin/main/TestUtil.java @@ -59,6 +59,10 @@ static void testEnforceWithContext(Enforcer e, EnforceContext enforceContext, Ob assertEquals(res, e.enforce(enforceContext, sub, obj, act)); } + static void testRBACWithABACRuleEnforce(Enforcer e, Object sub, Object obj, String act, Map ctx, boolean res) { + assertEquals(res, e.enforce(sub, obj, act, ctx)); + } + static void testDomainEnforce(Enforcer e, Object sub, Object dom, Object obj, Object act, boolean res) { assertEquals(res, e.enforce(sub, dom, obj, act)); }