This is an automated email from the ASF dual-hosted git repository.
hsluoyz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/casbin.git
The following commit(s) were added to refs/heads/master by this push:
new 94411fb feat: add policy index for partitioned enforcement
94411fb is described below
commit 94411fb66255de2eec199abb382542157fe7bd93
Author: Yang Luo <[email protected]>
AuthorDate: Sun Nov 1 02:39:47 2026 +0800
feat: add policy index for partitioned enforcement
---
enforcer.go | 132 +++++++++++++----
model/assertion.go | 2 +
model/model.go | 2 +
model/policy.go | 8 ++
policy_index.go | 209 +++++++++++++++++++++++++++
policy_index_b_test.go | 65 +++++++++
policy_index_test.go | 373 +++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 764 insertions(+), 27 deletions(-)
diff --git a/enforcer.go b/enforcer.go
index b78b920..220ec6c 100644
--- a/enforcer.go
+++ b/enforcer.go
@@ -25,6 +25,7 @@ import (
"sync"
"sync/atomic"
+ "github.com/casbin/casbin/v3/constant"
"github.com/casbin/casbin/v3/detector"
"github.com/casbin/casbin/v3/effector"
"github.com/casbin/casbin/v3/log"
@@ -47,14 +48,16 @@ type Enforcer struct {
fm model.FunctionMap
eft effector.Effector
- adapter persist.Adapter
- watcher persist.Watcher
- dispatcher persist.Dispatcher
- rmMap map[string]rbac.RoleManager
- condRmMap map[string]rbac.ConditionalRoleManager
- matcherMap sync.Map
- logger log.Logger
- detectors []detector.Detector
+ adapter persist.Adapter
+ watcher persist.Watcher
+ dispatcher persist.Dispatcher
+ rmMap map[string]rbac.RoleManager
+ condRmMap map[string]rbac.ConditionalRoleManager
+ matcherMap sync.Map
+ policyIndexes sync.Map
+ denyCache sync.Map
+ logger log.Logger
+ detectors []detector.Detector
enabled bool
autoSave bool
@@ -179,6 +182,7 @@ func (e *Enforcer) InitWithModelAndAdapter(m model.Model,
adapter persist.Adapte
e.model = m
e.model.PrintModel()
e.fm = model.LoadFunctionMap()
+ e.resetPolicyCaches()
e.initialize()
@@ -242,6 +246,7 @@ func (e *Enforcer) GetModel() model.Model {
func (e *Enforcer) SetModel(m model.Model) {
e.model = m
e.fm = model.LoadFunctionMap()
+ e.resetPolicyCaches()
e.initialize()
}
@@ -455,6 +460,7 @@ func (e *Enforcer) applyModifiedModel(newModel model.Model)
error {
}
e.model = newModel
+ e.resetPolicyCaches()
e.invalidateMatcherMap()
return nil
}
@@ -716,6 +722,47 @@ func (e *Enforcer) invalidateMatcherMap() {
e.bumpPolicyVersion()
}
+func (e *Enforcer) resetPolicyCaches() {
+ e.policyIndexes.Range(func(_, value interface{}) bool {
+ value.(*policyIndex).reset()
+ return true
+ })
+ e.denyCache.Range(func(key, _ interface{}) bool {
+ e.denyCache.Delete(key)
+ return true
+ })
+}
+
+type denyCacheEntry struct {
+ version uint64
+ hasDeny bool
+ allValid bool
+}
+
+func (e *Enforcer) policyHasDeny(pType string, pTokens map[string]int) (bool,
bool) {
+ ast := e.model["p"][pType]
+ if value, ok := e.denyCache.Load(pType); ok {
+ entry := value.(denyCacheEntry)
+ if entry.version == ast.PolicyVersion {
+ return entry.hasDeny, entry.allValid
+ }
+ }
+
+ eftIndex, hasEft := pTokens[pType+"_eft"]
+ hasDeny, allValid := false, true
+ for _, rule := range ast.Policy {
+ if len(rule) != len(ast.Tokens) {
+ allValid = false
+ }
+ if hasEft && eftIndex < len(rule) && rule[eftIndex] == "deny" {
+ hasDeny = true
+ }
+ }
+
+ e.denyCache.Store(pType, denyCacheEntry{version: ast.PolicyVersion,
hasDeny: hasDeny, allValid: allValid})
+ return hasDeny, allValid
+}
+
func (e *Enforcer) getPolicyVersion() uint64 {
return atomic.LoadUint64(&e.policyVersion)
}
@@ -838,13 +885,33 @@ func (e *Enforcer) enforce(matcher string, explains
*[]string, rvals ...interfac
rvals)
}
+ effectExpr := e.model["e"][eType].Value
+ policyLen := len(e.model["p"][pType].Policy)
+ if policyLen != 0 && strings.Contains(expString, pType+"_") {
+ if _, ok := e.eft.(*effector.DefaultEffector); ok {
+ if effectExpr == constant.AllowAndDenyEffect {
+ if hasDeny, allValid := e.policyHasDeny(pType,
pTokens); allValid && !hasDeny {
+ effectExpr =
constant.AllowOverrideEffect
+ }
+ }
+ }
+ }
+
+ var bucketIDs []uint32
+ var requestBucket uint32
+ var usePolicyIndex bool
+ if pIndex := e.getPolicyIndex(pType); pIndex != nil {
+ bucketIDs = pIndex.ensureBuilt(e.model["p"][pType])
+ requestBucket, usePolicyIndex = pIndex.requestBucket(rvals)
+ }
+
var policyEffects []effector.Effect
var matcherResults []float64
var effect effector.Effect
var explainIndex int
- if policyLen := len(e.model["p"][pType].Policy); policyLen != 0 &&
strings.Contains(expString, pType+"_") { //nolint:nestif // TODO: reduce
function complexity
+ if policyLen != 0 && strings.Contains(expString, pType+"_") {
//nolint:nestif // TODO: reduce function complexity
policyEffects = make([]effector.Effect, policyLen)
matcherResults = make([]float64, policyLen)
@@ -858,6 +925,33 @@ func (e *Enforcer) enforce(matcher string, explains
*[]string, rvals ...interfac
pvals)
}
+ if j, ok := pTokens[pType+"_eft"]; ok {
+ eft := pvals[j]
+ switch eft {
+ case "allow":
+ policyEffects[policyIndex] =
effector.Allow
+ case "deny":
+ policyEffects[policyIndex] =
effector.Deny
+ default:
+ policyEffects[policyIndex] =
effector.Indeterminate
+ }
+ } else {
+ policyEffects[policyIndex] = effector.Allow
+ }
+
+ // set to no-match at first
+ matcherResults[policyIndex] = 0
+ if usePolicyIndex && !policyMatchesBucket(bucketIDs,
policyIndex, requestBucket) {
+ effect, explainIndex, err =
e.eft.MergeEffects(effectExpr, policyEffects, matcherResults, policyIndex,
policyLen)
+ if err != nil {
+ return false, err
+ }
+ if effect != effector.Indeterminate {
+ break
+ }
+ continue
+ }
+
parameters.pVals = pvals
result, err := expression.Eval(parameters)
@@ -866,8 +960,6 @@ func (e *Enforcer) enforce(matcher string, explains
*[]string, rvals ...interfac
return false, err
}
- // set to no-match at first
- matcherResults[policyIndex] = 0
switch result := result.(type) {
case bool:
if result {
@@ -881,25 +973,11 @@ func (e *Enforcer) enforce(matcher string, explains
*[]string, rvals ...interfac
return false, errors.New("matcher result should
be bool, int or float")
}
- if j, ok := parameters.pTokens[pType+"_eft"]; ok {
- eft := parameters.pVals[j]
- switch eft {
- case "allow":
- policyEffects[policyIndex] =
effector.Allow
- case "deny":
- policyEffects[policyIndex] =
effector.Deny
- default:
- policyEffects[policyIndex] =
effector.Indeterminate
- }
- } else {
- policyEffects[policyIndex] = effector.Allow
- }
-
// if e.model["e"]["e"].Value == "priority(p_eft) ||
deny" {
// break
// }
- effect, explainIndex, err =
e.eft.MergeEffects(e.model["e"][eType].Value, policyEffects, matcherResults,
policyIndex, policyLen)
+ effect, explainIndex, err =
e.eft.MergeEffects(effectExpr, policyEffects, matcherResults, policyIndex,
policyLen)
if err != nil {
return false, err
}
@@ -929,7 +1007,7 @@ func (e *Enforcer) enforce(matcher string, explains
*[]string, rvals ...interfac
policyEffects[0] = effector.Indeterminate
}
- effect, explainIndex, err =
e.eft.MergeEffects(e.model["e"][eType].Value, policyEffects, matcherResults, 0,
1)
+ effect, explainIndex, err = e.eft.MergeEffects(effectExpr,
policyEffects, matcherResults, 0, 1)
if err != nil {
return false, err
}
diff --git a/model/assertion.go b/model/assertion.go
index 3766ec4..3bc17dd 100644
--- a/model/assertion.go
+++ b/model/assertion.go
@@ -34,6 +34,7 @@ type Assertion struct {
ParamsTokens []string
Policy [][]string
PolicyMap map[string]int
+ PolicyVersion uint64
RM rbac.RoleManager
CondRM rbac.ConditionalRoleManager
FieldIndexMap map[string]int
@@ -190,6 +191,7 @@ func (ast *Assertion) copy() *Assertion {
Key: ast.Key,
Value: ast.Value,
PolicyMap: policyMap,
+ PolicyVersion: ast.PolicyVersion,
Tokens: tokens,
Policy: policy,
FieldIndexMap: fieldIndexMap,
diff --git a/model/model.go b/model/model.go
index 487d35b..5f8c171 100644
--- a/model/model.go
+++ b/model/model.go
@@ -265,6 +265,7 @@ func (model Model) SortPoliciesBySubjectHierarchy() error {
for i, policy := range assertion.Policy {
assertion.PolicyMap[strings.Join(policy, ",")] = i
}
+ assertion.PolicyVersion++
}
return nil
}
@@ -345,6 +346,7 @@ func (model Model) SortPoliciesByPriority() error {
for i, policy := range assertion.Policy {
assertion.PolicyMap[strings.Join(policy, ",")] = i
}
+ assertion.PolicyVersion++
}
return nil
}
diff --git a/model/policy.go b/model/policy.go
index c09527d..d9a7829 100644
--- a/model/policy.go
+++ b/model/policy.go
@@ -102,11 +102,13 @@ func (model Model) ClearPolicy() {
for _, ast := range model["p"] {
ast.Policy = nil
ast.PolicyMap = map[string]int{}
+ ast.PolicyVersion++
}
for _, ast := range model["g"] {
ast.Policy = nil
ast.PolicyMap = map[string]int{}
+ ast.PolicyVersion++
}
}
@@ -204,6 +206,7 @@ func (model Model) AddPolicy(sec string, ptype string, rule
[]string) error {
}
assertion.Policy = append(assertion.Policy, rule)
assertion.PolicyMap[strings.Join(rule, DefaultSep)] =
len(model[sec][ptype].Policy) - 1
+ assertion.PolicyVersion++
hasPriority := false
if _, ok := assertion.FieldIndexMap[constant.PriorityIndex]; ok {
@@ -276,6 +279,7 @@ func (model Model) RemovePolicy(sec string, ptype string,
rule []string) (bool,
for i := index; i < len(ast.Policy); i++ {
ast.PolicyMap[strings.Join(ast.Policy[i], DefaultSep)] = i
}
+ ast.PolicyVersion++
return true, nil
}
@@ -294,6 +298,7 @@ func (model Model) UpdatePolicy(sec string, ptype string,
oldRule []string, newR
model[sec][ptype].Policy[index] = newRule
delete(model[sec][ptype].PolicyMap, oldPolicy)
model[sec][ptype].PolicyMap[strings.Join(newRule, DefaultSep)] = index
+ model[sec][ptype].PolicyVersion++
return true, nil
}
@@ -327,6 +332,7 @@ func (model Model) UpdatePolicies(sec string, ptype string,
oldRules, newRules [
assertion.Policy[indexes[i]] = newRule
assertion.PolicyMap[strings.Join(newRule, DefaultSep)] =
indexes[i]
}
+ assertion.PolicyVersion++
return true, nil
}
@@ -376,6 +382,7 @@ func (model Model) RemovePoliciesWithAffected(sec string,
ptype string, rules []
assertion.Policy = compactPolicy
assertion.PolicyMap = compactPolicyMap
+ assertion.PolicyVersion++
return affected, nil
}
@@ -410,6 +417,7 @@ func (model Model) RemoveFilteredPolicy(sec string, ptype
string, fieldIndex int
if len(tmp) != len(model[sec][ptype].Policy) {
model[sec][ptype].Policy = tmp
+ model[sec][ptype].PolicyVersion++
res = true
}
diff --git a/policy_index.go b/policy_index.go
new file mode 100644
index 0000000..9241112
--- /dev/null
+++ b/policy_index.go
@@ -0,0 +1,209 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package casbin
+
+import (
+ "errors"
+ "fmt"
+ "sync"
+
+ "github.com/casbin/casbin/v3/model"
+)
+
+const (
+ wildcardPolicyBucket uint32 = ^uint32(0)
+ missingPolicyBucket uint32 = ^uint32(1)
+)
+
+// PolicyIndex partitions the policy rules of one policy type so that Enforce
+// only evaluates the rules in the request's partition.
+//
+// RequestKey receives the request values, with an optional EnforceContext
+// removed, and returns the partition key for the request. Returning false
+// disables the index for that request, so every rule is evaluated.
+//
+// PolicyKey returns the partition key for one policy rule. The key "*" puts
+// the rule in every partition.
+//
+// Enforce skips the matcher of a rule whose key differs from the request key.
+// The caller must guarantee that such a rule can never match the request;
+// otherwise the decision changes. This includes matchers that evaluate policy
+// fields with eval(). Rules are still merged by the configured effector, so
+// deny rules must be reachable through the request key or "*".
+type PolicyIndex struct {
+ RequestKey func(rvals []interface{}) (string, bool)
+ PolicyKey func(rule []string) string
+}
+
+// SetPolicyIndex installs a policy partition for ptype. It replaces an
+// existing index for the same policy type. The index is rebuilt automatically
+// when the policy changes.
+func (e *Enforcer) SetPolicyIndex(ptype string, index PolicyIndex) error {
+ if index.RequestKey == nil || index.PolicyKey == nil {
+ return errors.New("policy index functions cannot be nil")
+ }
+ if _, ok := e.model["p"][ptype]; !ok {
+ return fmt.Errorf("policy type %q does not exist", ptype)
+ }
+
+ e.policyIndexes.Store(ptype, &policyIndex{
+ requestKey: index.RequestKey,
+ policyKey: index.PolicyKey,
+ })
+ return nil
+}
+
+// SetPolicyIndexByToken installs a policy partition that uses the string value
+// of requestToken as the request key and the string value of policyToken as
+// the rule key. It uses the default request type "r"; use SetPolicyIndex with
+// a custom function when enforcing through an EnforceContext.
+func (e *Enforcer) SetPolicyIndexByToken(ptype string, requestToken string,
policyToken string) error {
+ rAssertion, ok := e.model["r"]["r"]
+ if !ok {
+ return errors.New("request type \"r\" does not exist")
+ }
+ requestIndex := -1
+ for i, token := range rAssertion.Tokens {
+ if token == requestToken {
+ requestIndex = i
+ break
+ }
+ }
+ if requestIndex == -1 {
+ return fmt.Errorf("request token %q does not exist",
requestToken)
+ }
+
+ pAssertion, ok := e.model["p"][ptype]
+ if !ok {
+ return fmt.Errorf("policy type %q does not exist", ptype)
+ }
+ policyIndex := -1
+ for i, token := range pAssertion.Tokens {
+ if token == policyToken {
+ policyIndex = i
+ break
+ }
+ }
+ if policyIndex == -1 {
+ return fmt.Errorf("policy token %q does not exist", policyToken)
+ }
+
+ return e.SetPolicyIndex(ptype, PolicyIndex{
+ RequestKey: func(rvals []interface{}) (string, bool) {
+ if requestIndex >= len(rvals) {
+ return "", false
+ }
+ key, ok := rvals[requestIndex].(string)
+ return key, ok
+ },
+ PolicyKey: func(rule []string) string {
+ if policyIndex >= len(rule) {
+ return ""
+ }
+ return rule[policyIndex]
+ },
+ })
+}
+
+// RemovePolicyIndex removes the policy partition for ptype.
+func (e *Enforcer) RemovePolicyIndex(ptype string) {
+ e.policyIndexes.Delete(ptype)
+}
+
+func (e *Enforcer) getPolicyIndex(ptype string) *policyIndex {
+ if value, ok := e.policyIndexes.Load(ptype); ok {
+ return value.(*policyIndex)
+ }
+ return nil
+}
+
+type policyIndex struct {
+ requestKey func(rvals []interface{}) (string, bool)
+ policyKey func(rule []string) string
+
+ mu sync.Mutex
+ built bool
+ version uint64
+ bucketIDs []uint32
+ names map[string]uint32
+}
+
+func (idx *policyIndex) ensureBuilt(ast *model.Assertion) []uint32 {
+ idx.mu.Lock()
+ defer idx.mu.Unlock()
+
+ if idx.built && idx.version == ast.PolicyVersion && len(idx.bucketIDs)
== len(ast.Policy) {
+ return idx.bucketIDs
+ }
+
+ names := make(map[string]uint32)
+ bucketIDs := make([]uint32, len(ast.Policy))
+ var next uint32
+ for i, rule := range ast.Policy {
+ key := idx.policyKey(rule)
+ if key == "*" {
+ bucketIDs[i] = wildcardPolicyBucket
+ continue
+ }
+
+ id, ok := names[key]
+ if !ok {
+ id = next
+ names[key] = id
+ next++
+ }
+ bucketIDs[i] = id
+ }
+
+ idx.version = ast.PolicyVersion
+ idx.bucketIDs = bucketIDs
+ idx.names = names
+ idx.built = true
+ return bucketIDs
+}
+
+func (idx *policyIndex) reset() {
+ idx.mu.Lock()
+ idx.built = false
+ idx.bucketIDs = nil
+ idx.names = nil
+ idx.mu.Unlock()
+}
+
+func (idx *policyIndex) requestBucket(rvals []interface{}) (uint32, bool) {
+ key, ok := idx.requestKey(rvals)
+ if !ok {
+ return 0, false
+ }
+
+ idx.mu.Lock()
+ id, found := idx.names[key]
+ idx.mu.Unlock()
+ if !found {
+ return missingPolicyBucket, true
+ }
+ return id, true
+}
+
+func policyMatchesBucket(bucketIDs []uint32, policyIndex int, bucket uint32)
bool {
+ if policyIndex < 0 || policyIndex >= len(bucketIDs) {
+ return true
+ }
+ id := bucketIDs[policyIndex]
+ return id == wildcardPolicyBucket || id == bucket
+}
diff --git a/policy_index_b_test.go b/policy_index_b_test.go
new file mode 100644
index 0000000..29295f1
--- /dev/null
+++ b/policy_index_b_test.go
@@ -0,0 +1,65 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package casbin
+
+import (
+ "fmt"
+ "testing"
+)
+
+func BenchmarkPolicyIndex(b *testing.B) {
+ const (
+ typeCount = 100
+ perType = 100
+ )
+
+ e, _ := newIndexTestEnforcer(b, "some(where (p.eft == allow)) &&
!some(where (p.eft == deny))", false)
+
+ rules := make([][]string, 0, typeCount*perType)
+ for typeIndex := 0; typeIndex < typeCount; typeIndex++ {
+ objType := fmt.Sprintf("type%d", typeIndex)
+ for objIndex := 0; objIndex < perType; objIndex++ {
+ rules = append(rules, []string{"alice", objType,
fmt.Sprintf("obj%d", objIndex), "read"})
+ }
+ }
+ if _, err := e.AddPolicies(rules); err != nil {
+ b.Fatal(err)
+ }
+
+ request := []interface{}{"alice", "type50", "obj50", "read"}
+
+ b.Run("unindexed", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ if ok, err := e.Enforce(request...); err != nil || !ok {
+ b.Fatal(ok, err)
+ }
+ }
+ })
+
+ setObjectTypeIndex(b, e)
+
+ b.Run("indexed", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ if ok, err := e.Enforce(request...); err != nil || !ok {
+ b.Fatal(ok, err)
+ }
+ }
+ })
+}
diff --git a/policy_index_test.go b/policy_index_test.go
new file mode 100644
index 0000000..51eccf0
--- /dev/null
+++ b/policy_index_test.go
@@ -0,0 +1,373 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package casbin
+
+import (
+ "reflect"
+ "testing"
+
+ "github.com/casbin/casbin/v3/model"
+)
+
+func newIndexTestEnforcer(t testing.TB, effect string, withEft bool)
(*Enforcer, *int) {
+ t.Helper()
+
+ m := model.NewModel()
+ m.AddDef("r", "r", "sub, obj_type, obj, act")
+ if withEft {
+ m.AddDef("p", "p", "sub, obj_type, obj, act, eft")
+ } else {
+ m.AddDef("p", "p", "sub, obj_type, obj, act")
+ }
+ m.AddDef("g", "g", "_, _")
+ m.AddDef("e", "e", effect)
+ m.AddDef("m", "m", "g(r.sub, p.sub) && matchObj(p.obj_type, p.obj,
r.obj_type, r.obj) && r.act == p.act")
+
+ e, err := NewEnforcer(m)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ calls := 0
+ addMatchObjFunction(e, &calls)
+
+ return e, &calls
+}
+
+func addMatchObjFunction(e *Enforcer, calls *int) {
+ e.AddFunction("matchObj", func(args ...interface{}) (interface{},
error) {
+ *calls++
+ pType := args[0].(string)
+ pObj := args[1].(string)
+ rType := args[2].(string)
+ rObj := args[3].(string)
+ return (pType == "*" || pType == rType) && pObj == rObj, nil
+ })
+}
+
+func addIndexTestPolicies(t testing.TB, e *Enforcer, withEft bool, withDeny
bool) {
+ t.Helper()
+
+ rules := [][]string{
+ {"alice", "doc", "doc1", "read"},
+ {"alice", "doc", "doc2", "read"},
+ {"alice", "img", "img1", "read"},
+ {"alice", "*", "video1", "read"},
+ }
+ if withEft {
+ for i := range rules {
+ rules[i] = append(rules[i], "allow")
+ }
+ }
+
+ if _, err := e.AddPolicies(rules); err != nil {
+ t.Fatal(err)
+ }
+ if !withDeny {
+ return
+ }
+
+ if !withEft {
+ t.Fatal("a deny policy requires the eft token")
+ }
+ if _, err := e.AddPolicy("alice", "doc", "doc3", "read", "deny"); err
!= nil {
+ t.Fatal(err)
+ }
+}
+
+func setObjectTypeIndex(t testing.TB, e *Enforcer) {
+ t.Helper()
+
+ err := e.SetPolicyIndex("p", PolicyIndex{
+ RequestKey: func(rvals []interface{}) (string, bool) {
+ return rvals[1].(string), true
+ },
+ PolicyKey: func(rule []string) string {
+ return rule[1]
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPolicyIndexMatchesUnindexedDecisions(t *testing.T) {
+ const effect = "some(where (p.eft == allow)) && !some(where (p.eft ==
deny))"
+
+ indexed, _ := newIndexTestEnforcer(t, effect, true)
+ addIndexTestPolicies(t, indexed, true, true)
+ setObjectTypeIndex(t, indexed)
+
+ unindexed, _ := newIndexTestEnforcer(t, effect, true)
+ addIndexTestPolicies(t, unindexed, true, true)
+
+ if _, err := indexed.AddGroupingPolicy("bob", "alice"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := unindexed.AddGroupingPolicy("bob", "alice"); err != nil {
+ t.Fatal(err)
+ }
+
+ requests := [][]interface{}{
+ {"alice", "doc", "doc1", "read"},
+ {"alice", "doc", "doc2", "read"},
+ {"alice", "doc", "doc3", "read"},
+ {"alice", "img", "img1", "read"},
+ {"alice", "audio", "video1", "read"},
+ {"alice", "audio", "missing", "read"},
+ {"alice", "doc", "doc1", "write"},
+ {"bob", "doc", "doc1", "read"},
+ {"bob", "img", "img1", "read"},
+ {"bob", "audio", "video1", "read"},
+ }
+
+ for _, request := range requests {
+ indexedOK, indexedExplain, err := indexed.EnforceEx(request...)
+ if err != nil {
+ t.Fatal(err)
+ }
+ unindexedOK, unindexedExplain, err :=
unindexed.EnforceEx(request...)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if indexedOK != unindexedOK {
+ t.Fatalf("%v: indexed decision %v, unindexed %v",
request, indexedOK, unindexedOK)
+ }
+ if !reflect.DeepEqual(indexedExplain, unindexedExplain) {
+ t.Fatalf("%v: indexed explain %v, unindexed %v",
request, indexedExplain, unindexedExplain)
+ }
+ }
+}
+
+func TestPolicyIndexSkipsUnrelatedRules(t *testing.T) {
+ e, calls := newIndexTestEnforcer(t, "some(where (p.eft == allow)) &&
!some(where (p.eft == deny))", true)
+ addIndexTestPolicies(t, e, true, true)
+ setObjectTypeIndex(t, e)
+
+ tests := []struct {
+ request []interface{}
+ calls int
+ }{
+ {[]interface{}{"alice", "doc", "doc1", "read"}, 4},
+ {[]interface{}{"alice", "img", "img1", "read"}, 2},
+ {[]interface{}{"alice", "audio", "video1", "read"}, 1},
+ }
+
+ for _, test := range tests {
+ *calls = 0
+ if _, err := e.Enforce(test.request...); err != nil {
+ t.Fatal(err)
+ }
+ if *calls != test.calls {
+ t.Fatalf("%v: evaluated %d rules, want %d",
test.request, *calls, test.calls)
+ }
+ }
+
+ e.RemovePolicyIndex("p")
+ *calls = 0
+ if _, err := e.Enforce("alice", "doc", "doc1", "read"); err != nil {
+ t.Fatal(err)
+ }
+ if *calls != 5 {
+ t.Fatalf("without index: evaluated %d rules, want 5", *calls)
+ }
+}
+
+func TestPolicyIndexRebuildsAfterPolicyChanges(t *testing.T) {
+ e, calls := newIndexTestEnforcer(t, "some(where (p.eft == allow)) &&
!some(where (p.eft == deny))", true)
+ addIndexTestPolicies(t, e, true, true)
+ setObjectTypeIndex(t, e)
+
+ if ok, err := e.Enforce("alice", "doc", "doc4", "read"); err != nil ||
ok {
+ t.Fatalf("doc4 before add: ok=%v err=%v", ok, err)
+ }
+
+ if _, err := e.AddPolicy("alice", "doc", "doc4", "read", "allow"); err
!= nil {
+ t.Fatal(err)
+ }
+ *calls = 0
+ if ok, err := e.Enforce("alice", "doc", "doc4", "read"); err != nil ||
!ok {
+ t.Fatalf("doc4 after add: ok=%v err=%v", ok, err)
+ }
+ if *calls != 5 {
+ t.Fatalf("after add: evaluated %d rules, want 5", *calls)
+ }
+
+ if _, err := e.RemovePolicy("alice", "doc", "doc4", "read", "allow");
err != nil {
+ t.Fatal(err)
+ }
+ if ok, err := e.Enforce("alice", "doc", "doc4", "read"); err != nil ||
ok {
+ t.Fatalf("doc4 after remove: ok=%v err=%v", ok, err)
+ }
+
+ oldRule := []string{"alice", "img", "img1", "read", "allow"}
+ newRule := []string{"alice", "doc", "doc4", "read", "allow"}
+ if ok, err := e.UpdatePolicy(oldRule, newRule); err != nil || !ok {
+ t.Fatalf("update policy: ok=%v err=%v", ok, err)
+ }
+ if ok, err := e.Enforce("alice", "img", "img1", "read"); err != nil ||
ok {
+ t.Fatalf("img1 after update: ok=%v err=%v", ok, err)
+ }
+ if ok, err := e.Enforce("alice", "doc", "doc4", "read"); err != nil ||
!ok {
+ t.Fatalf("doc4 after update: ok=%v err=%v", ok, err)
+ }
+
+ e.ClearPolicy()
+ if ok, err := e.Enforce("alice", "doc", "doc1", "read"); err != nil ||
ok {
+ t.Fatalf("doc1 after clear: ok=%v err=%v", ok, err)
+ }
+ if _, err := e.AddPolicy("alice", "doc", "doc1", "read", "allow"); err
!= nil {
+ t.Fatal(err)
+ }
+ if ok, err := e.Enforce("alice", "doc", "doc1", "read"); err != nil ||
!ok {
+ t.Fatalf("doc1 after clear and add: ok=%v err=%v", ok, err)
+ }
+}
+
+func TestPolicyIndexDisabledForUnpartitionedRequest(t *testing.T) {
+ e, calls := newIndexTestEnforcer(t, "some(where (p.eft == allow)) &&
!some(where (p.eft == deny))", true)
+ addIndexTestPolicies(t, e, true, true)
+
+ err := e.SetPolicyIndex("p", PolicyIndex{
+ RequestKey: func(rvals []interface{}) (string, bool) {
+ return "", false
+ },
+ PolicyKey: func(rule []string) string {
+ return rule[1]
+ },
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ *calls = 0
+ if _, err := e.Enforce("alice", "doc", "doc1", "read"); err != nil {
+ t.Fatal(err)
+ }
+ if *calls != 5 {
+ t.Fatalf("evaluated %d rules, want 5", *calls)
+ }
+}
+
+func TestPolicyIndexValidation(t *testing.T) {
+ e, _ := newIndexTestEnforcer(t, "some(where (p.eft == allow))", false)
+
+ if err := e.SetPolicyIndex("missing", PolicyIndex{
+ RequestKey: func([]interface{}) (string, bool) { return "",
true },
+ PolicyKey: func([]string) string { return "" },
+ }); err == nil {
+ t.Fatal("expected an error for a missing policy type")
+ }
+
+ if err := e.SetPolicyIndex("p", PolicyIndex{}); err == nil {
+ t.Fatal("expected an error for nil index functions")
+ }
+
+ setObjectTypeIndex(t, e)
+ e.RemovePolicyIndex("p")
+}
+
+func TestAllowAndDenyShortCircuitsWithoutDenyRules(t *testing.T) {
+ for _, withEft := range []bool{false, true} {
+ e, calls := newIndexTestEnforcer(t, "some(where (p.eft ==
allow)) && !some(where (p.eft == deny))", withEft)
+ addIndexTestPolicies(t, e, withEft, false)
+
+ *calls = 0
+ if ok, err := e.Enforce("alice", "doc", "doc1", "read"); err !=
nil || !ok {
+ t.Fatalf("withEft=%v: ok=%v err=%v", withEft, ok, err)
+ }
+ if *calls != 1 {
+ t.Fatalf("withEft=%v: evaluated %d rules, want 1",
withEft, *calls)
+ }
+ }
+}
+
+func TestAllowAndDenyStillEvaluatesWhenDenyExists(t *testing.T) {
+ e, calls := newIndexTestEnforcer(t, "some(where (p.eft == allow)) &&
!some(where (p.eft == deny))", true)
+ addIndexTestPolicies(t, e, true, true)
+
+ *calls = 0
+ if ok, err := e.Enforce("alice", "doc", "doc1", "read"); err != nil ||
!ok {
+ t.Fatalf("ok=%v err=%v", ok, err)
+ }
+ if *calls != 5 {
+ t.Fatalf("evaluated %d rules, want 5", *calls)
+ }
+}
+
+func TestPolicyIndexByToken(t *testing.T) {
+ e, calls := newIndexTestEnforcer(t, "some(where (p.eft == allow)) &&
!some(where (p.eft == deny))", true)
+ addIndexTestPolicies(t, e, true, true)
+
+ if err := e.SetPolicyIndexByToken("p", "r_obj_type", "p_obj_type"); err
!= nil {
+ t.Fatal(err)
+ }
+
+ *calls = 0
+ if ok, err := e.Enforce("alice", "doc", "doc1", "read"); err != nil ||
!ok {
+ t.Fatalf("ok=%v err=%v", ok, err)
+ }
+ if *calls != 4 {
+ t.Fatalf("evaluated %d rules, want 4", *calls)
+ }
+
+ if err := e.SetPolicyIndexByToken("p", "r_missing", "p_obj_type"); err
== nil {
+ t.Fatal("expected an error for a missing request token")
+ }
+ if err := e.SetPolicyIndexByToken("p", "r_obj_type", "p_missing"); err
== nil {
+ t.Fatal("expected an error for a missing policy token")
+ }
+}
+
+func TestPolicyIndexResetsWhenModelChanges(t *testing.T) {
+ const effect = "some(where (p.eft == allow)) && !some(where (p.eft ==
deny))"
+
+ e, calls := newIndexTestEnforcer(t, effect, false)
+ if _, err := e.AddPolicy("alice", "doc", "doc1", "read"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := e.AddPolicy("alice", "img", "img1", "read"); err != nil {
+ t.Fatal(err)
+ }
+ setObjectTypeIndex(t, e)
+
+ if ok, err := e.Enforce("alice", "img", "img1", "read"); err != nil ||
!ok {
+ t.Fatalf("before SetModel: ok=%v err=%v", ok, err)
+ }
+
+ replacement := model.NewModel()
+ replacement.AddDef("r", "r", "sub, obj_type, obj, act")
+ replacement.AddDef("p", "p", "sub, obj_type, obj, act")
+ replacement.AddDef("g", "g", "_, _")
+ replacement.AddDef("e", "e", effect)
+ replacement.AddDef("m", "m", "g(r.sub, p.sub) && matchObj(p.obj_type,
p.obj, r.obj_type, r.obj) && r.act == p.act")
+ if err := replacement.AddPolicy("p", "p", []string{"alice", "img",
"img1", "read"}); err != nil {
+ t.Fatal(err)
+ }
+ if err := replacement.AddPolicy("p", "p", []string{"alice", "doc",
"doc1", "read"}); err != nil {
+ t.Fatal(err)
+ }
+
+ e.SetModel(replacement)
+ addMatchObjFunction(e, calls)
+
+ if ok, err := e.Enforce("alice", "img", "img1", "read"); err != nil ||
!ok {
+ t.Fatalf("after SetModel: ok=%v err=%v", ok, err)
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]