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]

Reply via email to