diff --git a/effector/default_effector.go b/effector/default_effector.go index fca8912ed..e9c21892f 100644 --- a/effector/default_effector.go +++ b/effector/default_effector.go @@ -40,12 +40,17 @@ 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 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 { @@ -53,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 (priority order: Allow > RateLimit) + 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 + } + } + } + // DenyOverride defaults to Allow if no deny rules matched (matches original behavior) + if result == Indeterminate { + result = Allow + } } case constant.AllowAndDenyEffect: // short-circuit if matched deny rule @@ -71,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 @@ -83,9 +115,15 @@ 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 + // 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 @@ -94,6 +132,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_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/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..d798125d1 100644 --- a/model_test.go +++ b/model_test.go @@ -342,6 +342,25 @@ 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 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")