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 27d2b2d  fix: invalidate decision cache on policy change
27d2b2d is described below

commit 27d2b2d3663f536a3466d16b5ce00878fc9cd968
Author: Yang Luo <[email protected]>
AuthorDate: Sat Oct 10 02:26:54 2026 +0800

    fix: invalidate decision cache on policy change
---
 enforcer.go               | 13 +++++++++++++
 enforcer_cached.go        | 46 +++++++++++++++++++++++++++++++++++-----------
 enforcer_cached_synced.go | 41 ++++++++++++++++++++++++++++++++++++-----
 enforcer_context.go       | 16 ++++++++++++++++
 enforcer_distributed.go   | 14 ++++++++++++++
 internal_api.go           | 16 ++++++++++++++++
 6 files changed, 130 insertions(+), 16 deletions(-)

diff --git a/enforcer.go b/enforcer.go
index c575534..b2417c8 100644
--- a/enforcer.go
+++ b/enforcer.go
@@ -23,6 +23,7 @@ import (
        "runtime/debug"
        "strings"
        "sync"
+       "sync/atomic"
 
        "github.com/casbin/casbin/v3/detector"
        "github.com/casbin/casbin/v3/effector"
@@ -39,6 +40,8 @@ import (
 
 // Enforcer is the main interface for authorization enforcement and policy 
management.
 type Enforcer struct {
+       policyVersion uint64
+
        modelPath string
        model     model.Model
        fm        model.FunctionMap
@@ -197,6 +200,7 @@ func (e *Enforcer) initialize() {
        e.eft = effector.NewDefaultEffector()
        e.watcher = nil
        e.matcherMap = sync.Map{}
+       e.bumpPolicyVersion()
 
        e.enabled = true
        e.autoSave = true
@@ -696,6 +700,15 @@ func NewEnforceContext(suffix string) EnforceContext {
 
 func (e *Enforcer) invalidateMatcherMap() {
        e.matcherMap = sync.Map{}
+       e.bumpPolicyVersion()
+}
+
+func (e *Enforcer) getPolicyVersion() uint64 {
+       return atomic.LoadUint64(&e.policyVersion)
+}
+
+func (e *Enforcer) bumpPolicyVersion() {
+       atomic.AddUint64(&e.policyVersion, 1)
 }
 
 // enforce use a custom matcher to decides whether a "subject" can access a 
"object" with the operation "action", input parameters are usually: (matcher, 
sub, obj, act), use model matcher by default when matcher is "".
diff --git a/enforcer_cached.go b/enforcer_cached.go
index a9b4be4..deff218 100644
--- a/enforcer_cached.go
+++ b/enforcer_cached.go
@@ -29,10 +29,11 @@ import (
 // CachedEnforcer wraps Enforcer and provides decision cache.
 type CachedEnforcer struct {
        *Enforcer
-       expireTime  time.Duration
-       cache       cache.Cache
-       enableCache int32
-       locker      *sync.RWMutex
+       cacheVersion uint64
+       expireTime   time.Duration
+       cache        cache.Cache
+       enableCache  int32
+       locker       *sync.RWMutex
 }
 
 type CacheableParam interface {
@@ -51,6 +52,7 @@ func NewCachedEnforcer(params ...interface{}) 
(*CachedEnforcer, error) {
        e.enableCache = 1
        e.cache, _ = cache.NewDefaultCache()
        e.locker = new(sync.RWMutex)
+       e.cacheVersion = e.getPolicyVersion()
        return e, nil
 }
 
@@ -75,6 +77,11 @@ func (e *CachedEnforcer) Enforce(rvals ...interface{}) 
(bool, error) {
                return e.Enforcer.Enforce(rvals...)
        }
 
+       version := e.getPolicyVersion()
+       if err := e.syncCacheVersion(version); err != nil {
+               return false, err
+       }
+
        if res, err := e.getCachedResult(key); err == nil {
                return res, nil
        } else if err != cache.ErrNoSuchKey {
@@ -86,7 +93,7 @@ func (e *CachedEnforcer) Enforce(rvals ...interface{}) (bool, 
error) {
                return false, err
        }
 
-       err = e.setCachedResult(key, res, e.expireTime)
+       err = e.setCachedResult(version, key, res, e.expireTime)
        return res, err
 }
 
@@ -143,12 +150,6 @@ func (e *CachedEnforcer) SetCache(c cache.Cache) {
        e.cache = c
 }
 
-func (e *CachedEnforcer) setCachedResult(key string, res bool, extra 
...interface{}) error {
-       e.locker.Lock()
-       defer e.locker.Unlock()
-       return e.cache.Set(key, res, extra...)
-}
-
 func (e *CachedEnforcer) getKey(params ...interface{}) (string, bool) {
        return GetCacheKey(params...)
 }
@@ -186,3 +187,26 @@ func (e *CachedEnforcer) ClearPolicy() {
        }
        e.Enforcer.ClearPolicy()
 }
+
+// syncCacheVersion drops all cached decisions when the policy has changed 
since they were cached.
+func (e *CachedEnforcer) syncCacheVersion(version uint64) error {
+       e.locker.Lock()
+       defer e.locker.Unlock()
+       if version <= e.cacheVersion {
+               return nil
+       }
+       if err := e.cache.Clear(); err != nil {
+               return err
+       }
+       e.cacheVersion = version
+       return nil
+}
+
+func (e *CachedEnforcer) setCachedResult(version uint64, key string, res bool, 
extra ...interface{}) error {
+       e.locker.Lock()
+       defer e.locker.Unlock()
+       if version != e.cacheVersion {
+               return nil
+       }
+       return e.cache.Set(key, res, extra...)
+}
diff --git a/enforcer_cached_synced.go b/enforcer_cached_synced.go
index 3ae2613..9301c95 100644
--- a/enforcer_cached_synced.go
+++ b/enforcer_cached_synced.go
@@ -27,6 +27,8 @@ import (
 
 // SyncedCachedEnforcer wraps Enforcer and provides decision sync cache.
 type SyncedCachedEnforcer struct {
+       cacheVersion uint64
+
        *SyncedEnforcer
        expireTime  time.Duration
        cache       cache.Cache
@@ -46,6 +48,7 @@ func NewSyncedCachedEnforcer(params ...interface{}) 
(*SyncedCachedEnforcer, erro
        e.enableCache = 1
        e.cache, _ = cache.NewSyncCache()
        e.locker = new(sync.RWMutex)
+       e.cacheVersion = e.getPolicyVersion()
        return e, nil
 }
 
@@ -70,6 +73,11 @@ func (e *SyncedCachedEnforcer) Enforce(rvals ...interface{}) 
(bool, error) {
                return e.SyncedEnforcer.Enforce(rvals...)
        }
 
+       version := e.getPolicyVersion()
+       if err := e.syncCacheVersion(version); err != nil {
+               return false, err
+       }
+
        if res, err := e.getCachedResult(key); err == nil {
                return res, nil
        } else if err != cache.ErrNoSuchKey {
@@ -81,7 +89,7 @@ func (e *SyncedCachedEnforcer) Enforce(rvals ...interface{}) 
(bool, error) {
                return false, err
        }
 
-       err = e.setCachedResult(key, res, e.expireTime)
+       err = e.setCachedResult(version, key, res, e.expireTime)
        return res, err
 }
 
@@ -139,10 +147,6 @@ func (e *SyncedCachedEnforcer) SetCache(c cache.Cache) {
        e.cache = c
 }
 
-func (e *SyncedCachedEnforcer) setCachedResult(key string, res bool, extra 
...interface{}) error {
-       return e.cache.Set(key, res, extra...)
-}
-
 func (e *SyncedCachedEnforcer) getKey(params ...interface{}) (string, bool) {
        return GetCacheKey(params...)
 }
@@ -181,3 +185,30 @@ func (e *SyncedCachedEnforcer) 
checkManyAndRemoveCache(rules [][]string) (bool,
        }
        return true, nil
 }
+
+// syncCacheVersion drops all cached decisions when the policy has changed 
since they were cached.
+func (e *SyncedCachedEnforcer) syncCacheVersion(version uint64) error {
+       if version <= atomic.LoadUint64(&e.cacheVersion) {
+               return nil
+       }
+
+       e.locker.Lock()
+       defer e.locker.Unlock()
+       if version <= e.cacheVersion {
+               return nil
+       }
+       if err := e.cache.Clear(); err != nil {
+               return err
+       }
+       atomic.StoreUint64(&e.cacheVersion, version)
+       return nil
+}
+
+func (e *SyncedCachedEnforcer) setCachedResult(version uint64, key string, res 
bool, extra ...interface{}) error {
+       e.locker.RLock()
+       defer e.locker.RUnlock()
+       if version != e.cacheVersion {
+               return nil
+       }
+       return e.cache.Set(key, res, extra...)
+}
diff --git a/enforcer_context.go b/enforcer_context.go
index b7e4f8a..cf885d3 100644
--- a/enforcer_context.go
+++ b/enforcer_context.go
@@ -416,6 +416,8 @@ func (e *ContextEnforcer) SelfUpdatePoliciesCtx(ctx 
context.Context, sec string,
 
 // addPolicyWithoutNotifyCtx adds a rule to the current policy with context.
 func (e *ContextEnforcer) addPolicyWithoutNotifyCtx(ctx context.Context, sec 
string, ptype string, rule []string) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if e.dispatcher != nil && e.autoNotifyDispatcher {
                return true, e.dispatcher.AddPolicies(sec, ptype, 
[][]string{rule})
        }
@@ -450,6 +452,8 @@ func (e *ContextEnforcer) addPolicyWithoutNotifyCtx(ctx 
context.Context, sec str
 
 // addPoliciesWithoutNotifyCtx adds rules to the current policy with context.
 func (e *ContextEnforcer) addPoliciesWithoutNotifyCtx(ctx context.Context, sec 
string, ptype string, rules [][]string, autoRemoveRepeat bool) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if e.dispatcher != nil && e.autoNotifyDispatcher {
                return true, e.dispatcher.AddPolicies(sec, ptype, rules)
        }
@@ -491,6 +495,8 @@ func (e *ContextEnforcer) addPoliciesWithoutNotifyCtx(ctx 
context.Context, sec s
 
 // removePolicyWithoutNotifyCtx removes a rule from the current policy with 
context.
 func (e *ContextEnforcer) removePolicyWithoutNotifyCtx(ctx context.Context, 
sec string, ptype string, rule []string) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if e.dispatcher != nil && e.autoNotifyDispatcher {
                return true, e.dispatcher.RemovePolicies(sec, ptype, 
[][]string{rule})
        }
@@ -520,6 +526,8 @@ func (e *ContextEnforcer) removePolicyWithoutNotifyCtx(ctx 
context.Context, sec
 
 // removePoliciesWithoutNotifyCtx removes rules from the current policy with 
context.
 func (e *ContextEnforcer) removePoliciesWithoutNotifyCtx(ctx context.Context, 
sec string, ptype string, rules [][]string) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if hasPolicies, err := e.model.HasPolicies(sec, ptype, rules); 
!hasPolicies || err != nil {
                return hasPolicies, err
        }
@@ -552,6 +560,8 @@ func (e *ContextEnforcer) 
removePoliciesWithoutNotifyCtx(ctx context.Context, se
 
 // removeFilteredPolicyWithoutNotifyCtx removes policy rules that match the 
filter from the current policy with context.
 func (e *ContextEnforcer) removeFilteredPolicyWithoutNotifyCtx(ctx 
context.Context, sec string, ptype string, fieldIndex int, fieldValues 
[]string) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if len(fieldValues) == 0 {
                return false, Err.ErrInvalidFieldValuesParameter
        }
@@ -585,6 +595,8 @@ func (e *ContextEnforcer) 
removeFilteredPolicyWithoutNotifyCtx(ctx context.Conte
 
 // updatePolicyWithoutNotifyCtx updates a policy rule in the current policy 
with context.
 func (e *ContextEnforcer) updatePolicyWithoutNotifyCtx(ctx context.Context, 
sec string, ptype string, oldRule, newRule []string) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if e.dispatcher != nil && e.autoNotifyDispatcher {
                return true, e.dispatcher.UpdatePolicy(sec, ptype, oldRule, 
newRule)
        }
@@ -616,6 +628,8 @@ func (e *ContextEnforcer) updatePolicyWithoutNotifyCtx(ctx 
context.Context, sec
 }
 
 func (e *ContextEnforcer) updatePoliciesWithoutNotifyCtx(ctx context.Context, 
sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, 
error) {
+       defer e.bumpPolicyVersion()
+
        if len(newRules) != len(oldRules) {
                return false, fmt.Errorf("the length of oldRules should be 
equal to the length of newRules, but got the length of oldRules is %d, the 
length of newRules is %d", len(oldRules), len(newRules))
        }
@@ -806,6 +820,8 @@ func (e *ContextEnforcer) updateFilteredPoliciesCtx(ctx 
context.Context, sec str
 }
 
 func (e *ContextEnforcer) updateFilteredPoliciesWithoutNotifyCtx(ctx 
context.Context, sec string, ptype string, newRules [][]string, fieldIndex int, 
fieldValues ...string) ([][]string, error) {
+       defer e.bumpPolicyVersion()
+
        var (
                oldRules [][]string
                err      error
diff --git a/enforcer_distributed.go b/enforcer_distributed.go
index fe73a30..361520f 100644
--- a/enforcer_distributed.go
+++ b/enforcer_distributed.go
@@ -46,6 +46,8 @@ func (d *DistributedEnforcer) SetDispatcher(dispatcher 
persist.Dispatcher) {
 // AddPoliciesSelf provides a method for dispatcher to add authorization rules 
to the current policy.
 // The function returns the rules affected and error.
 func (d *DistributedEnforcer) AddPoliciesSelf(shouldPersist func() bool, sec 
string, ptype string, rules [][]string) (affected [][]string, err error) {
+       defer d.bumpPolicyVersion()
+
        d.m.Lock()
        defer d.m.Unlock()
        if shouldPersist != nil && shouldPersist() {
@@ -84,6 +86,8 @@ func (d *DistributedEnforcer) AddPoliciesSelf(shouldPersist 
func() bool, sec str
 // RemovePoliciesSelf provides a method for dispatcher to remove a set of 
rules from current policy.
 // The function returns the rules affected and error.
 func (d *DistributedEnforcer) RemovePoliciesSelf(shouldPersist func() bool, 
sec string, ptype string, rules [][]string) (affected [][]string, err error) {
+       defer d.bumpPolicyVersion()
+
        d.m.Lock()
        defer d.m.Unlock()
        if shouldPersist != nil && shouldPersist() {
@@ -112,6 +116,8 @@ func (d *DistributedEnforcer) 
RemovePoliciesSelf(shouldPersist func() bool, sec
 // RemoveFilteredPolicySelf provides a method for dispatcher to remove an 
authorization rule from the current policy, field filters can be specified.
 // The function returns the rules affected and error.
 func (d *DistributedEnforcer) RemoveFilteredPolicySelf(shouldPersist func() 
bool, sec string, ptype string, fieldIndex int, fieldValues ...string) 
(affected [][]string, err error) {
+       defer d.bumpPolicyVersion()
+
        d.m.Lock()
        defer d.m.Unlock()
        if shouldPersist != nil && shouldPersist() {
@@ -139,6 +145,8 @@ func (d *DistributedEnforcer) 
RemoveFilteredPolicySelf(shouldPersist func() bool
 
 // ClearPolicySelf provides a method for dispatcher to clear all rules from 
the current policy.
 func (d *DistributedEnforcer) ClearPolicySelf(shouldPersist func() bool) error 
{
+       defer d.bumpPolicyVersion()
+
        d.m.Lock()
        defer d.m.Unlock()
        if shouldPersist != nil && shouldPersist() {
@@ -155,6 +163,8 @@ func (d *DistributedEnforcer) ClearPolicySelf(shouldPersist 
func() bool) error {
 
 // UpdatePolicySelf provides a method for dispatcher to update an 
authorization rule from the current policy.
 func (d *DistributedEnforcer) UpdatePolicySelf(shouldPersist func() bool, sec 
string, ptype string, oldRule, newRule []string) (affected bool, err error) {
+       defer d.bumpPolicyVersion()
+
        d.m.Lock()
        defer d.m.Unlock()
        if shouldPersist != nil && shouldPersist() {
@@ -185,6 +195,8 @@ func (d *DistributedEnforcer) 
UpdatePolicySelf(shouldPersist func() bool, sec st
 
 // UpdatePoliciesSelf provides a method for dispatcher to update a set of 
authorization rules from the current policy.
 func (d *DistributedEnforcer) UpdatePoliciesSelf(shouldPersist func() bool, 
sec string, ptype string, oldRules, newRules [][]string) (affected bool, err 
error) {
+       defer d.bumpPolicyVersion()
+
        d.m.Lock()
        defer d.m.Unlock()
        if shouldPersist != nil && shouldPersist() {
@@ -215,6 +227,8 @@ func (d *DistributedEnforcer) 
UpdatePoliciesSelf(shouldPersist func() bool, sec
 
 // UpdateFilteredPoliciesSelf provides a method for dispatcher to update a set 
of authorization rules from the current policy.
 func (d *DistributedEnforcer) UpdateFilteredPoliciesSelf(shouldPersist func() 
bool, sec string, ptype string, newRules [][]string, fieldIndex int, 
fieldValues ...string) (bool, error) {
+       defer d.bumpPolicyVersion()
+
        d.m.Lock()
        defer d.m.Unlock()
        var (
diff --git a/internal_api.go b/internal_api.go
index 04883af..74ae54f 100644
--- a/internal_api.go
+++ b/internal_api.go
@@ -46,6 +46,8 @@ func (e *Enforcer) validateConstraintsForGroupingPolicy() 
error {
 
 // addPolicy adds a rule to the current policy.
 func (e *Enforcer) addPolicyWithoutNotify(sec string, ptype string, rule 
[]string) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if e.dispatcher != nil && e.autoNotifyDispatcher {
                return true, e.dispatcher.AddPolicies(sec, ptype, 
[][]string{rule})
        }
@@ -87,6 +89,8 @@ func (e *Enforcer) addPolicyWithoutNotify(sec string, ptype 
string, rule []strin
 // If autoRemoveRepeat == true, existing rules are automatically filtered
 // Otherwise, false is returned directly.
 func (e *Enforcer) addPoliciesWithoutNotify(sec string, ptype string, rules 
[][]string, autoRemoveRepeat bool) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if e.dispatcher != nil && e.autoNotifyDispatcher {
                return true, e.dispatcher.AddPolicies(sec, ptype, rules)
        }
@@ -133,6 +137,8 @@ func (e *Enforcer) addPoliciesWithoutNotify(sec string, 
ptype string, rules [][]
 
 // removePolicy removes a rule from the current policy.
 func (e *Enforcer) removePolicyWithoutNotify(sec string, ptype string, rule 
[]string) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if e.dispatcher != nil && e.autoNotifyDispatcher {
                return true, e.dispatcher.RemovePolicies(sec, ptype, 
[][]string{rule})
        }
@@ -166,6 +172,8 @@ func (e *Enforcer) removePolicyWithoutNotify(sec string, 
ptype string, rule []st
 }
 
 func (e *Enforcer) updatePolicyWithoutNotify(sec string, ptype string, oldRule 
[]string, newRule []string) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if e.dispatcher != nil && e.autoNotifyDispatcher {
                return true, e.dispatcher.UpdatePolicy(sec, ptype, oldRule, 
newRule)
        }
@@ -202,6 +210,8 @@ func (e *Enforcer) updatePolicyWithoutNotify(sec string, 
ptype string, oldRule [
 }
 
 func (e *Enforcer) updatePoliciesWithoutNotify(sec string, ptype string, 
oldRules [][]string, newRules [][]string) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if len(newRules) != len(oldRules) {
                return false, fmt.Errorf("the length of oldRules should be 
equal to the length of newRules, but got the length of oldRules is %d, the 
length of newRules is %d", len(oldRules), len(newRules))
        }
@@ -244,6 +254,8 @@ func (e *Enforcer) updatePoliciesWithoutNotify(sec string, 
ptype string, oldRule
 
 // removePolicies removes rules from the current policy.
 func (e *Enforcer) removePoliciesWithoutNotify(sec string, ptype string, rules 
[][]string) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if hasPolicies, err := e.model.HasPolicies(sec, ptype, rules); 
!hasPolicies || err != nil {
                return hasPolicies, err
        }
@@ -281,6 +293,8 @@ func (e *Enforcer) removePoliciesWithoutNotify(sec string, 
ptype string, rules [
 
 // removeFilteredPolicy removes rules based on field filters from the current 
policy.
 func (e *Enforcer) removeFilteredPolicyWithoutNotify(sec string, ptype string, 
fieldIndex int, fieldValues []string) (bool, error) {
+       defer e.bumpPolicyVersion()
+
        if len(fieldValues) == 0 {
                return false, Err.ErrInvalidFieldValuesParameter
        }
@@ -318,6 +332,8 @@ func (e *Enforcer) removeFilteredPolicyWithoutNotify(sec 
string, ptype string, f
 }
 
 func (e *Enforcer) updateFilteredPoliciesWithoutNotify(sec string, ptype 
string, newRules [][]string, fieldIndex int, fieldValues ...string) 
([][]string, error) {
+       defer e.bumpPolicyVersion()
+
        var (
                oldRules [][]string
                err      error


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to