From 3f0ad80ad636a96e94e36350255db8cd102b4ce5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 01:57:56 +0000 Subject: [PATCH 1/5] Initial plan From ece659d2208b355f014b753877bd567529904521 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 02:05:04 +0000 Subject: [PATCH 2/5] Add RateLimit effect support to Casbin Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- effector/default_effector.go | 13 +++++++++++++ effector/effector.go | 1 + enforcer.go | 4 +++- examples/rate_limit_model.conf | 14 ++++++++++++++ examples/rate_limit_policy.csv | 7 +++++++ model_test.go | 10 ++++++++++ 6 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 examples/rate_limit_model.conf create mode 100644 examples/rate_limit_policy.csv diff --git a/effector/default_effector.go b/effector/default_effector.go index fca8912ed..69fc99fa3 100644 --- a/effector/default_effector.go +++ b/effector/default_effector.go @@ -46,6 +46,11 @@ func (e *DefaultEffector) MergeEffects(expr string, effects []Effect, matches [] explainIndex = policyIndex break } + if effects[policyIndex] == RateLimit { + result = RateLimit + explainIndex = policyIndex + break + } case constant.DenyOverrideEffect: // only check the current policyIndex if matches[policyIndex] != 0 && effects[policyIndex] == Deny { @@ -83,6 +88,12 @@ func (e *DefaultEffector) MergeEffects(expr string, effects []Effect, matches [] explainIndex = i break } + if eft == RateLimit { + result = RateLimit + // set hit rule to first matched rate_limit rule + explainIndex = i + break + } } case constant.PriorityEffect, constant.SubjectPriorityEffect: // reverse merge, short-circuit may be earlier @@ -94,6 +105,8 @@ func (e *DefaultEffector) MergeEffects(expr string, effects []Effect, matches [] if effects[i] != Indeterminate { if effects[i] == Allow { result = Allow + } else if effects[i] == RateLimit { + result = RateLimit } else { result = Deny } diff --git a/effector/effector.go b/effector/effector.go index 49b84c3e1..b84c411e1 100644 --- a/effector/effector.go +++ b/effector/effector.go @@ -22,6 +22,7 @@ const ( Allow Effect = iota Indeterminate Deny + RateLimit ) // Effector is the interface for Casbin effectors. diff --git a/enforcer.go b/enforcer.go index f9bab13c5..3d810cee9 100644 --- a/enforcer.go +++ b/enforcer.go @@ -792,6 +792,8 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac policyEffects[policyIndex] = effector.Allow } else if eft == "deny" { policyEffects[policyIndex] = effector.Deny + } else if eft == "rate_limit" { + policyEffects[policyIndex] = effector.RateLimit } else { policyEffects[policyIndex] = effector.Indeterminate } @@ -855,7 +857,7 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac // effect -> result result := false - if effect == effector.Allow { + if effect == effector.Allow || effect == effector.RateLimit { result = true } e.logger.LogEnforce(expString, rvals, result, logExplains) diff --git a/examples/rate_limit_model.conf b/examples/rate_limit_model.conf new file mode 100644 index 000000000..6aa1ae5a6 --- /dev/null +++ b/examples/rate_limit_model.conf @@ -0,0 +1,14 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act, 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 diff --git a/examples/rate_limit_policy.csv b/examples/rate_limit_policy.csv new file mode 100644 index 000000000..9554bb8e0 --- /dev/null +++ b/examples/rate_limit_policy.csv @@ -0,0 +1,7 @@ +p, alice, data1, read, allow +p, bob, data2, write, rate_limit +p, data2_admin, data2, read, allow +p, data2_admin, data2, write, allow +p, charlie, data3, read, rate_limit + +g, alice, data2_admin diff --git a/model_test.go b/model_test.go index be00f4d9c..fe15a488e 100644 --- a/model_test.go +++ b/model_test.go @@ -342,6 +342,16 @@ func TestRBACModelWithOnlyDeny(t *testing.T) { testEnforce(t, e, "alice", "data2", "write", false) } +func TestRBACModelWithRateLimit(t *testing.T) { + e, _ := NewEnforcer("examples/rate_limit_model.conf", "examples/rate_limit_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) + testEnforce(t, e, "bob", "data2", "write", true) // rate_limit effect should return true + testEnforce(t, e, "charlie", "data3", "read", true) // rate_limit effect should return true + testEnforce(t, e, "alice", "data2", "read", true) + testEnforce(t, e, "alice", "data2", "write", true) +} + func TestRBACModelWithCustomData(t *testing.T) { e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") From 1a7bec4665ea477aa12549dbbc3921a245a26248 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 02:08:19 +0000 Subject: [PATCH 3/5] Improve RateLimit handling in DenyOverrideEffect mode Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- effector/default_effector.go | 31 ++++++++++++++++++-- examples/rate_limit_deny_override_model.conf | 11 +++++++ examples/rate_limit_deny_override_policy.csv | 4 +++ model_test.go | 9 ++++++ 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 examples/rate_limit_deny_override_model.conf create mode 100644 examples/rate_limit_deny_override_policy.csv diff --git a/effector/default_effector.go b/effector/default_effector.go index 69fc99fa3..fa61b2fce 100644 --- a/effector/default_effector.go +++ b/effector/default_effector.go @@ -58,9 +58,36 @@ func (e *DefaultEffector) MergeEffects(expr string, effects []Effect, matches [] explainIndex = policyIndex break } - // if no deny rules are matched at last, then allow + // if no deny rules are matched at last, check for allow or rate_limit if policyIndex == policyLength-1 { - result = Allow + // Check all matched policies for allow first, then rate_limit + for i := range effects { + if matches[i] == 0 { + continue + } + if effects[i] == Allow { + result = Allow + explainIndex = i + break + } + } + // If no allow found, check for rate_limit + if result == Indeterminate { + for i := range effects { + if matches[i] == 0 { + continue + } + if effects[i] == RateLimit { + result = RateLimit + explainIndex = i + break + } + } + } + // If still no match, default to allow + if result == Indeterminate { + result = Allow + } } case constant.AllowAndDenyEffect: // short-circuit if matched deny rule diff --git a/examples/rate_limit_deny_override_model.conf b/examples/rate_limit_deny_override_model.conf new file mode 100644 index 000000000..3dd9d1837 --- /dev/null +++ b/examples/rate_limit_deny_override_model.conf @@ -0,0 +1,11 @@ +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act, eft + +[policy_effect] +e = !some(where (p.eft == deny)) + +[matchers] +m = r.sub == p.sub && r.obj == p.obj && r.act == p.act diff --git a/examples/rate_limit_deny_override_policy.csv b/examples/rate_limit_deny_override_policy.csv new file mode 100644 index 000000000..b612f9990 --- /dev/null +++ b/examples/rate_limit_deny_override_policy.csv @@ -0,0 +1,4 @@ +p, alice, data1, read, allow +p, bob, data2, write, rate_limit +p, charlie, data3, read, deny +p, david, data4, write, rate_limit diff --git a/model_test.go b/model_test.go index fe15a488e..bccadf97d 100644 --- a/model_test.go +++ b/model_test.go @@ -352,6 +352,15 @@ func TestRBACModelWithRateLimit(t *testing.T) { testEnforce(t, e, "alice", "data2", "write", true) } +func TestRateLimitWithDenyOverride(t *testing.T) { + e, _ := NewEnforcer("examples/rate_limit_deny_override_model.conf", "examples/rate_limit_deny_override_policy.csv") + + testEnforce(t, e, "alice", "data1", "read", true) // allow effect + testEnforce(t, e, "bob", "data2", "write", true) // rate_limit effect should return true + testEnforce(t, e, "charlie", "data3", "read", false) // deny effect should return false + testEnforce(t, e, "david", "data4", "write", true) // rate_limit effect should return true +} + func TestRBACModelWithCustomData(t *testing.T) { e, _ := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv") From 805a8f11629a860631152a6bc42097ee1a6c0d81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 02:10:27 +0000 Subject: [PATCH 4/5] Add comments documenting priority order for RateLimit effect Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- effector/default_effector.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/effector/default_effector.go b/effector/default_effector.go index fa61b2fce..e9c21892f 100644 --- a/effector/default_effector.go +++ b/effector/default_effector.go @@ -40,7 +40,7 @@ func (e *DefaultEffector) MergeEffects(expr string, effects []Effect, matches [] if matches[policyIndex] == 0 { break } - // only check the current policyIndex + // only check the current policyIndex (priority order: Allow > RateLimit) if effects[policyIndex] == Allow { result = Allow explainIndex = policyIndex @@ -60,7 +60,7 @@ func (e *DefaultEffector) MergeEffects(expr string, effects []Effect, matches [] } // if no deny rules are matched at last, check for allow or rate_limit if policyIndex == policyLength-1 { - // Check all matched policies for allow first, then rate_limit + // Check all matched policies for allow first, then rate_limit (priority order: Allow > RateLimit) for i := range effects { if matches[i] == 0 { continue @@ -84,7 +84,7 @@ func (e *DefaultEffector) MergeEffects(expr string, effects []Effect, matches [] } } } - // If still no match, default to allow + // DenyOverride defaults to Allow if no deny rules matched (matches original behavior) if result == Indeterminate { result = Allow } @@ -103,7 +103,7 @@ func (e *DefaultEffector) MergeEffects(expr string, effects []Effect, matches [] // choose not to short-circuit return result, explainIndex, nil } - // merge all effects at last + // merge all effects at last (priority order: Allow > RateLimit) for i, eft := range effects { if matches[i] == 0 { continue @@ -123,7 +123,7 @@ func (e *DefaultEffector) MergeEffects(expr string, effects []Effect, matches [] } } case constant.PriorityEffect, constant.SubjectPriorityEffect: - // reverse merge, short-circuit may be earlier + // reverse merge, short-circuit may be earlier (priority order: Allow > RateLimit > Deny) for i := len(effects) - 1; i >= 0; i-- { if matches[i] == 0 { continue From 5a456aaa979867f3021b902ab8a0e376b7e147a0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 02:12:33 +0000 Subject: [PATCH 5/5] Fix code formatting for linter compliance Co-authored-by: hsluoyz <3787410+hsluoyz@users.noreply.github.com> --- model_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/model_test.go b/model_test.go index bccadf97d..d798125d1 100644 --- a/model_test.go +++ b/model_test.go @@ -346,7 +346,7 @@ func TestRBACModelWithRateLimit(t *testing.T) { e, _ := NewEnforcer("examples/rate_limit_model.conf", "examples/rate_limit_policy.csv") testEnforce(t, e, "alice", "data1", "read", true) - testEnforce(t, e, "bob", "data2", "write", true) // rate_limit effect should return true + testEnforce(t, e, "bob", "data2", "write", true) // rate_limit effect should return true testEnforce(t, e, "charlie", "data3", "read", true) // rate_limit effect should return true testEnforce(t, e, "alice", "data2", "read", true) testEnforce(t, e, "alice", "data2", "write", true) @@ -355,10 +355,10 @@ func TestRBACModelWithRateLimit(t *testing.T) { func TestRateLimitWithDenyOverride(t *testing.T) { e, _ := NewEnforcer("examples/rate_limit_deny_override_model.conf", "examples/rate_limit_deny_override_policy.csv") - testEnforce(t, e, "alice", "data1", "read", true) // allow effect - testEnforce(t, e, "bob", "data2", "write", true) // rate_limit effect should return true + testEnforce(t, e, "alice", "data1", "read", true) // allow effect + testEnforce(t, e, "bob", "data2", "write", true) // rate_limit effect should return true testEnforce(t, e, "charlie", "data3", "read", false) // deny effect should return false - testEnforce(t, e, "david", "data4", "write", true) // rate_limit effect should return true + testEnforce(t, e, "david", "data4", "write", true) // rate_limit effect should return true } func TestRBACModelWithCustomData(t *testing.T) {