Repository: casbin/casbin

Issue:
[Feature] Add ctx to AddPolicy and others in v3
**Want to prioritize this issue? Try:**

[![issuehunt-to-marktext](https://github.com/BoostIO/issuehunt-materials/raw/master/v1/issuehunt-button-v1.svg)](https://issuehunt.io/r/casbin/casbin)

------

**Is your feature request related to a problem? Please describe.**

With v3 being worked on, I think it would be great to add ctx to all methods which call into adapters. This allows adapters then to have access to ctx so that they can access per-request logging. Also, in our case we store current request-level transaction into ctx. We would like that adapter has access to it so that it can be rolled-back if overall transaction fails.

**Describe the solution you'd like**

Adapters in v2 already have interface `ContextAdapter` for ctx-based methods. But casbin does not really call them. I think we should extend casbin enforcer methods to accept ctx and call then into `ContextAdapter`.

**Describe alternatives you've considered**

Use some global state with mutex to store current context when calling those methods. But that is error prone and inefficient.

Discussion:
@D0000M: #1558
This is adraft implementation to demonstrate the approach.What do you think? We plan to continue building upon this foundation.@mitar: If we are targeting with this casbin v3, I would suggest this is simply made as a breaking change and all methods get `ctx` as the first method while keeping existing names. I think adding `EnforceCtx` is just adding a lot of noise. And generally doing a refactor with find&replace of `Enforce(` to `Enforce(ctx, ` and then fix all compiler errors for missing `ctx` is a pretty easy and quick fix when upgrading to v3.@D0000M: Sure, I totally agree with your approach. Let's keep this draft here for now. Upgrading to v3 is a big step, and we'll definitely factor this in as we plan our migration strategy.@hsluoyz: Fixed in: https://github.com/casbin/casbin/pull/1553@github-actions: :tada: This issue has been resolved in version 2.128.0 :tada:

The release is available on [GitHub release](https://github.com/casbin/casbin/releases/tag/v2.128.0)

Your **[semantic-release](https://github.com/semantic-release/semantic-release)** bot :package::rocket:

Solution patch:
diff --git a/enforcer_context.go b/enforcer_context.go
new file mode 100644
index 0000000..ef845b7
--- /dev/null
+++ b/enforcer_context.go
@@ -0,0 +1,858 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// Licensed 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 (
+	"context"
+	"errors"
+	"fmt"
+
+	Err "github.com/casbin/casbin/v2/errors"
+	"github.com/casbin/casbin/v2/model"
+	"github.com/casbin/casbin/v2/persist"
+)
+
+// ContextEnforcer wraps Enforcer and provides context-aware operations.
+type ContextEnforcer struct {
+	*Enforcer
+	adapterCtx persist.ContextAdapter
+}
+
+// NewContextEnforcer creates a context-aware enforcer via file or DB.
+func NewContextEnforcer(params ...interface{}) (IEnforcerContext, error) {
+	e := &ContextEnforcer{}
+	var err error
+	e.Enforcer, err = NewEnforcer(params...)
+	if err != nil {
+		return nil, err
+	}
+
+	if e.Enforcer.adapter != nil {
+		if contextAdapter, ok := e.Enforcer.adapter.(persist.ContextAdapter); ok {
+			e.adapterCtx = contextAdapter
+		} else {
+			return nil, errors.New("adapter does not support context operations, ContextAdapter interface not implemented")
+		}
+	} else {
+		return nil, errors.New("no adapter provided, ContextEnforcer requires a ContextAdapter")
+	}
+
+	return e, nil
+}
+
+// LoadPolicyCtx loads all policy rules from the storage with context.
+func (e *ContextEnforcer) LoadPolicyCtx(ctx context.Context) error {
+	newModel, err := e.loadPolicyFromAdapterCtx(ctx, e.model)
+	if err != nil {
+		return err
+	}
+	err = e.applyModifiedModel(newModel)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (e *ContextEnforcer) loadPolicyFromAdapterCtx(ctx context.Context, baseModel model.Model) (model.Model, error) {
+	newModel := baseModel.Copy()
+	newModel.ClearPolicy()
+
+	if err := e.adapterCtx.LoadPolicyCtx(ctx, newModel); err != nil && err.Error() != "invalid file path, file path cannot be empty" {
+		return nil, err
+	}
+
+	if err := newModel.SortPoliciesBySubjectHierarchy(); err != nil {
+		return nil, err
+	}
+
+	if err := newModel.SortPoliciesByPriority(); err != nil {
+		return nil, err
+	}
+
+	return newModel, nil
+}
+
+// LoadFilteredPolicyCtx loads all policy rules from the storage with context and filter.
+func (e *Enforcer) LoadFilteredPolicyCtx(ctx context.Context, filter interface{}) error {
+	e.model.ClearPolicy()
+	return e.loadFilteredPolicyCtx(ctx, filter)
+}
+
+// LoadIncrementalFilteredPolicyCtx append a filtered policy from file/database with context.
+func (e *Enforcer) LoadIncrementalFilteredPolicyCtx(ctx context.Context, filter interface{}) error {
+	return e.loadFilteredPolicyCtx(ctx, filter)
+}
+
+func (e *Enforcer) loadFilteredPolicyCtx(ctx context.Context, filter interface{}) error {
+	e.invalidateMatcherMap()
+
+	var filteredAdapter persist.ContextFilteredAdapter
+
+	// Attempt to cast the Adapter as a FilteredAdapter
+	switch adapter := e.adapter.(type) {
+	case persist.ContextFilteredAdapter:
+		filteredAdapter = adapter
+	default:
+		return errors.New("filtered policies are not supported by this adapter")
+	}
+	if err := filteredAdapter.LoadFilteredPolicyCtx(ctx, e.model, filter); err != nil && err.Error() != "invalid file path, file path cannot be empty" {
+		return err
+	}
+
+	if err := e.model.SortPoliciesBySubjectHierarchy(); err != nil {
+		return err
+	}
+
+	if err := e.model.SortPoliciesByPriority(); err != nil {
+		return err
+	}
+
+	e.initRmMap()
+	e.model.PrintPolicy()
+	if e.autoBuildRoleLinks {
+		err := e.BuildRoleLinks()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// IsFilteredCtx returns true if the loaded policy has been filtered with context.
+func (e *ContextEnforcer) IsFilteredCtx(ctx context.Context) bool {
+	if adapter, ok := e.adapter.(persist.ContextFilteredAdapter); ok {
+		return adapter.IsFilteredCtx(ctx)
+	} else {
+		return false
+	}
+}
+
+func (e *ContextEnforcer) SavePolicyCtx(ctx context.Context) error {
+	if e.IsFiltered() {
+		return errors.New("cannot save a filtered policy")
+	}
+	if err := e.adapterCtx.SavePolicyCtx(ctx, e.model); err != nil {
+		return err
+	}
+	if e.watcher != nil {
+		var err error
+		if watcher, ok := e.watcher.(persist.WatcherEx); ok {
+			err = watcher.UpdateForSavePolicy(e.model)
+		} else {
+			err = e.watcher.Update()
+		}
+		return err
+	}
+	return nil
+}
+
+// AddPolicyCtx adds a policy rule to the storage with context.
+func (e *ContextEnforcer) AddPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) {
+	return e.AddNamedPolicyCtx(ctx, "p", params...)
+}
+
+// AddPoliciesCtx adds policy rules to the storage with context.
+func (e *ContextEnforcer) AddPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) {
+	return e.AddNamedPoliciesCtx(ctx, "p", rules)
+}
+
+// AddNamedPolicyCtx adds a named policy rule to the storage with context.
+func (e *ContextEnforcer) AddNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) {
+	if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
+		strSlice = append(make([]string, 0, len(strSlice)), strSlice...)
+		return e.addPolicyCtx(ctx, "p", ptype, strSlice)
+	}
+	policy := make([]string, 0)
+	for _, param := range params {
+		policy = append(policy, param.(string))
+	}
+
+	return e.addPolicyCtx(ctx, "p", ptype, policy)
+}
+
+// AddNamedPoliciesCtx adds named policy rules to the storage with context.
+func (e *ContextEnforcer) AddNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
+	return e.addPoliciesCtx(ctx, "p", ptype, rules, false)
+}
+
+func (e *ContextEnforcer) AddPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error) {
+	return e.AddNamedPoliciesExCtx(ctx, "p", rules)
+}
+
+func (e *ContextEnforcer) AddNamedPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
+	return e.addPoliciesCtx(ctx, "p", ptype, rules, true)
+}
+
+// RemovePolicyCtx removes a policy rule from the storage with context.
+func (e *ContextEnforcer) RemovePolicyCtx(ctx context.Context, params ...interface{}) (bool, error) {
+	return e.RemoveNamedPolicyCtx(ctx, "p", params...)
+}
+
+// RemoveNamedPolicyCtx removes a named policy rule from the storage with context.
+func (e *ContextEnforcer) RemoveNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) {
+	if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
+		return e.removePolicyCtx(ctx, "p", ptype, strSlice)
+	}
+	policy := make([]string, 0)
+	for _, param := range params {
+		policy = append(policy, param.(string))
+	}
+
+	return e.removePolicyCtx(ctx, "p", ptype, policy)
+}
+
+// RemovePoliciesCtx removes policy rules from the storage with context.
+func (e *ContextEnforcer) RemovePoliciesCtx(ctx context.Context, rules [][]string) (bool, error) {
+	return e.RemoveNamedPoliciesCtx(ctx, "p", rules)
+}
+
+// RemoveNamedPoliciesCtx removes named policy rules from the storage with context.
+func (e *ContextEnforcer) RemoveNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
+	return e.removePoliciesCtx(ctx, "p", ptype, rules)
+}
+
+// RemoveFilteredPolicyCtx removes policy rules that match the filter from the storage with context.
+func (e *ContextEnforcer) RemoveFilteredPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error) {
+	return e.RemoveFilteredNamedPolicyCtx(ctx, "p", fieldIndex, fieldValues...)
+}
+
+// RemoveFilteredNamedPolicyCtx removes named policy rules that match the filter from the storage with context.
+func (e *ContextEnforcer) RemoveFilteredNamedPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error) {
+	return e.removeFilteredPolicyCtx(ctx, "p", ptype, fieldIndex, fieldValues)
+}
+
+// UpdatePolicyCtx updates a policy rule in the storage with context.
+func (e *ContextEnforcer) UpdatePolicyCtx(ctx context.Context, oldPolicy []string, newPolicy []string) (bool, error) {
+	return e.UpdateNamedPolicyCtx(ctx, "p", oldPolicy, newPolicy)
+}
+
+// UpdateNamedPolicyCtx updates a named policy rule in the storage with context.
+func (e *ContextEnforcer) UpdateNamedPolicyCtx(ctx context.Context, ptype string, p1 []string, p2 []string) (bool, error) {
+	return e.updatePolicyCtx(ctx, "p", ptype, p1, p2)
+}
+
+// UpdatePoliciesCtx updates policy rules in the storage with context.
+func (e *ContextEnforcer) UpdatePoliciesCtx(ctx context.Context, oldPolicies [][]string, newPolicies [][]string) (bool, error) {
+	return e.UpdateNamedPoliciesCtx(ctx, "p", oldPolicies, newPolicies)
+}
+
+// UpdateNamedPoliciesCtx updates named policy rules in the storage with context.
+func (e *ContextEnforcer) UpdateNamedPoliciesCtx(ctx context.Context, ptype string, p1 [][]string, p2 [][]string) (bool, error) {
+	return e.updatePoliciesCtx(ctx, "p", ptype, p1, p2)
+}
+
+// UpdateFilteredPoliciesCtx updates policy rules that match the filter in the storage with context.
+func (e *ContextEnforcer) UpdateFilteredPoliciesCtx(ctx context.Context, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) {
+	return e.UpdateFilteredNamedPoliciesCtx(ctx, "p", newPolicies, fieldIndex, fieldValues...)
+}
+
+// UpdateFilteredNamedPoliciesCtx updates named policy rules that match the filter in the storage with context.
+func (e *ContextEnforcer) UpdateFilteredNamedPoliciesCtx(ctx context.Context, ptype string, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error) {
+	return e.updateFilteredPoliciesCtx(ctx, "p", ptype, newPolicies, fieldIndex, fieldValues...)
+}
+
+// Grouping Policy Context Methods
+
+// AddGroupingPolicyCtx adds a grouping policy rule to the storage with context.
+func (e *ContextEnforcer) AddGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) {
+	return e.AddNamedGroupingPolicyCtx(ctx, "g", params...)
+}
+
+// AddGroupingPoliciesCtx adds grouping policy rules to the storage with context.
+func (e *ContextEnforcer) AddGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) {
+	return e.AddNamedGroupingPoliciesCtx(ctx, "g", rules)
+}
+
+func (e *ContextEnforcer) AddGroupingPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error) {
+	return e.AddNamedGroupingPoliciesExCtx(ctx, "g", rules)
+}
+
+// AddNamedGroupingPolicyCtx adds a named grouping policy rule to the storage with context.
+func (e *ContextEnforcer) AddNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) {
+	var ruleAdded bool
+	var err error
+	if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
+		ruleAdded, err = e.addPolicyCtx(ctx, "g", ptype, strSlice)
+	} else {
+		policy := make([]string, 0)
+		for _, param := range params {
+			policy = append(policy, param.(string))
+		}
+		ruleAdded, err = e.addPolicyCtx(ctx, "g", ptype, policy)
+	}
+
+	return ruleAdded, err
+}
+
+// AddNamedGroupingPoliciesCtx adds named grouping policy rules to the storage with context.
+func (e *ContextEnforcer) AddNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
+	return e.addPoliciesCtx(ctx, "g", ptype, rules, false)
+}
+
+func (e *ContextEnforcer) AddNamedGroupingPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
+	return e.addPoliciesCtx(ctx, "g", ptype, rules, true)
+}
+
+// RemoveGroupingPolicyCtx removes a grouping policy rule from the storage with context.
+func (e *ContextEnforcer) RemoveGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error) {
+	return e.RemoveNamedGroupingPolicyCtx(ctx, "g", params...)
+}
+
+// RemoveNamedGroupingPolicyCtx removes a named grouping policy rule from the storage with context.
+func (e *ContextEnforcer) RemoveNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error) {
+	var ruleRemoved bool
+	var err error
+	if strSlice, ok := params[0].([]string); len(params) == 1 && ok {
+		ruleRemoved, err = e.removePolicyCtx(ctx, "g", ptype, strSlice)
+	} else {
+		policy := make([]string, 0)
+		for _, param := range params {
+			policy = append(policy, param.(string))
+		}
+
+		ruleRemoved, err = e.removePolicyCtx(ctx, "g", ptype, policy)
+	}
+
+	return ruleRemoved, err
+}
+
+// RemoveGroupingPoliciesCtx removes grouping policy rules from the storage with context.
+func (e *ContextEnforcer) RemoveGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error) {
+	return e.RemoveNamedGroupingPoliciesCtx(ctx, "g", rules)
+}
+
+// RemoveNamedGroupingPoliciesCtx removes named grouping policy rules from the storage with context.
+func (e *ContextEnforcer) RemoveNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error) {
+	return e.removePoliciesCtx(ctx, "g", ptype, rules)
+}
+
+// RemoveFilteredGroupingPolicyCtx removes grouping policy rules that match the filter from the storage with context.
+func (e *ContextEnforcer) RemoveFilteredGroupingPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error) {
+	return e.RemoveFilteredNamedGroupingPolicyCtx(ctx, "g", fieldIndex, fieldValues...)
+}
+
+// RemoveFilteredNamedGroupingPolicyCtx removes named grouping policy rules that match the filter from the storage with context.
+func (e *ContextEnforcer) RemoveFilteredNamedGroupingPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error) {
+	return e.removeFilteredPolicyCtx(ctx, "g", ptype, fieldIndex, fieldValues)
+}
+
+// UpdateGroupingPolicyCtx updates a grouping policy rule in the storage with context.
+func (e *ContextEnforcer) UpdateGroupingPolicyCtx(ctx context.Context, oldRule []string, newRule []string) (bool, error) {
+	return e.UpdateNamedGroupingPolicyCtx(ctx, "g", oldRule, newRule)
+}
+
+// UpdateNamedGroupingPolicyCtx updates a named grouping policy rule in the storage with context.
+func (e *ContextEnforcer) UpdateNamedGroupingPolicyCtx(ctx context.Context, ptype string, oldRule []string, newRule []string) (bool, error) {
+	return e.updatePolicyCtx(ctx, "g", ptype, oldRule, newRule)
+}
+
+// UpdateGroupingPoliciesCtx updates grouping policy rules in the storage with context.
+func (e *ContextEnforcer) UpdateGroupingPoliciesCtx(ctx context.Context, oldRules [][]string, newRules [][]string) (bool, error) {
+	return e.UpdateNamedGroupingPoliciesCtx(ctx, "g", oldRules, newRules)
+}
+
+// UpdateNamedGroupingPoliciesCtx updates named grouping policy rules in the storage with context.
+func (e *ContextEnforcer) UpdateNamedGroupingPoliciesCtx(ctx context.Context, ptype string, oldRules [][]string, newRules [][]string) (bool, error) {
+	return e.updatePoliciesCtx(ctx, "g", ptype, oldRules, newRules)
+}
+
+// Self Context Methods (bypass watcher notifications)
+
+// SelfAddPolicyCtx adds a policy rule to the current policy with context.
+func (e *ContextEnforcer) SelfAddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) {
+	return e.addPolicyWithoutNotifyCtx(ctx, sec, ptype, rule)
+}
+
+// SelfAddPoliciesCtx adds policy rules to the current policy with context.
+func (e *ContextEnforcer) SelfAddPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) {
+	return e.addPoliciesWithoutNotifyCtx(ctx, sec, ptype, rules, false)
+}
+
+func (e *ContextEnforcer) SelfAddPoliciesExCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) {
+	return e.addPoliciesWithoutNotifyCtx(ctx, sec, ptype, rules, true)
+}
+
+// SelfRemovePolicyCtx removes a policy rule from the current policy with context.
+func (e *ContextEnforcer) SelfRemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) {
+	return e.removePolicyWithoutNotifyCtx(ctx, sec, ptype, rule)
+}
+
+// SelfRemovePoliciesCtx removes policy rules from the current policy with context.
+func (e *ContextEnforcer) SelfRemovePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) {
+	return e.removePoliciesWithoutNotifyCtx(ctx, sec, ptype, rules)
+}
+
+// SelfRemoveFilteredPolicyCtx removes policy rules that match the filter from the current policy with context.
+func (e *ContextEnforcer) SelfRemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error) {
+	return e.removeFilteredPolicyWithoutNotifyCtx(ctx, sec, ptype, fieldIndex, fieldValues)
+}
+
+// SelfUpdatePolicyCtx updates a policy rule in the current policy with context.
+func (e *ContextEnforcer) SelfUpdatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule, newRule []string) (bool, error) {
+	return e.updatePolicyWithoutNotifyCtx(ctx, sec, ptype, oldRule, newRule)
+}
+
+// SelfUpdatePoliciesCtx updates policy rules in the current policy with context.
+func (e *ContextEnforcer) SelfUpdatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules, newRules [][]string) (bool, error) {
+	return e.updatePoliciesWithoutNotifyCtx(ctx, sec, ptype, oldRules, newRules)
+}
+
+// Internal API methods with context support
+
+// 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) {
+	if e.dispatcher != nil && e.autoNotifyDispatcher {
+		return true, e.dispatcher.AddPolicies(sec, ptype, [][]string{rule})
+	}
+
+	hasPolicy, err := e.model.HasPolicy(sec, ptype, rule)
+	if hasPolicy || err != nil {
+		return false, err
+	}
+
+	if e.shouldPersist() {
+		if err = e.adapterCtx.AddPolicyCtx(ctx, sec, ptype, rule); err != nil {
+			if err.Error() != notImplemented {
+				return false, err
+			}
+		}
+	}
+
+	err = e.model.AddPolicy(sec, ptype, rule)
+	if err != nil {
+		return false, err
+	}
+
+	if sec == "g" {
+		err := e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{rule})
+		if err != nil {
+			return true, err
+		}
+	}
+
+	return true, nil
+}
+
+// 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) {
+	if e.dispatcher != nil && e.autoNotifyDispatcher {
+		return true, e.dispatcher.AddPolicies(sec, ptype, rules)
+	}
+
+	if !autoRemoveRepeat {
+		hasPolicies, err := e.model.HasPolicies(sec, ptype, rules)
+		if hasPolicies || err != nil {
+			return false, err
+		}
+	}
+
+	if e.shouldPersist() {
+		if err := e.adapterCtx.(persist.ContextBatchAdapter).AddPoliciesCtx(ctx, sec, ptype, rules); err != nil {
+			if err.Error() != notImplemented {
+				return false, err
+			}
+		}
+	}
+
+	err := e.model.AddPolicies(sec, ptype, rules)
+	if err != nil {
+		return false, err
+	}
+
+	if sec == "g" {
+		err := e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, rules)
+		if err != nil {
+			return true, err
+		}
+
+		err = e.BuildIncrementalConditionalRoleLinks(model.PolicyAdd, ptype, rules)
+		if err != nil {
+			return true, err
+		}
+	}
+
+	return true, nil
+}
+
+// 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) {
+	if e.dispatcher != nil && e.autoNotifyDispatcher {
+		return true, e.dispatcher.RemovePolicies(sec, ptype, [][]string{rule})
+	}
+
+	if e.shouldPersist() {
+		if err := e.adapterCtx.RemovePolicyCtx(ctx, sec, ptype, rule); err != nil {
+			if err.Error() != notImplemented {
+				return false, err
+			}
+		}
+	}
+
+	ruleRemoved, err := e.model.RemovePolicy(sec, ptype, rule)
+	if !ruleRemoved || err != nil {
+		return ruleRemoved, err
+	}
+
+	if sec == "g" {
+		err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{rule})
+		if err != nil {
+			return ruleRemoved, err
+		}
+	}
+
+	return ruleRemoved, nil
+}
+
+// removePoliciesWithoutNotifyCtx removes rules from the current policy with context.
+func (e *ContextEnforcer) removePoliciesWithoutNotifyCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) {
+	if hasPolicies, err := e.model.HasPolicies(sec, ptype, rules); !hasPolicies || err != nil {
+		return hasPolicies, err
+	}
+
+	if e.dispatcher != nil && e.autoNotifyDispatcher {
+		return true, e.dispatcher.RemovePolicies(sec, ptype, rules)
+	}
+
+	if e.shouldPersist() {
+		if err := e.adapterCtx.(persist.ContextBatchAdapter).RemovePoliciesCtx(ctx, sec, ptype, rules); err != nil {
+			if err.Error() != notImplemented {
+				return false, err
+			}
+		}
+	}
+
+	rulesRemoved, err := e.model.RemovePolicies(sec, ptype, rules)
+	if !rulesRemoved || err != nil {
+		return rulesRemoved, err
+	}
+
+	if sec == "g" {
+		err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, rules)
+		if err != nil {
+			return rulesRemoved, err
+		}
+	}
+	return rulesRemoved, nil
+}
+
+// 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) {
+	if len(fieldValues) == 0 {
+		return false, Err.ErrInvalidFieldValuesParameter
+	}
+
+	if e.dispatcher != nil && e.autoNotifyDispatcher {
+		return true, e.dispatcher.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
+	}
+
+	if e.shouldPersist() {
+		if err := e.adapterCtx.RemoveFilteredPolicyCtx(ctx, sec, ptype, fieldIndex, fieldValues...); err != nil {
+			if err.Error() != notImplemented {
+				return false, err
+			}
+		}
+	}
+
+	ruleRemoved, effects, err := e.model.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
+	if !ruleRemoved || err != nil {
+		return ruleRemoved, err
+	}
+
+	if sec == "g" {
+		err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, effects)
+		if err != nil {
+			return ruleRemoved, err
+		}
+	}
+
+	return ruleRemoved, nil
+}
+
+// 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) {
+	if e.dispatcher != nil && e.autoNotifyDispatcher {
+		return true, e.dispatcher.UpdatePolicy(sec, ptype, oldRule, newRule)
+	}
+
+	if e.shouldPersist() {
+		if err := e.adapterCtx.(persist.ContextUpdatableAdapter).UpdatePolicyCtx(ctx, sec, ptype, oldRule, newRule); err != nil {
+			if err.Error() != notImplemented {
+				return false, err
+			}
+		}
+	}
+	ruleUpdated, err := e.model.UpdatePolicy(sec, ptype, oldRule, newRule)
+	if !ruleUpdated || err != nil {
+		return ruleUpdated, err
+	}
+
+	if sec == "g" {
+		err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, [][]string{oldRule}) // remove the old rule
+		if err != nil {
+			return ruleUpdated, err
+		}
+		err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, [][]string{newRule}) // add the new rule
+		if err != nil {
+			return ruleUpdated, err
+		}
+	}
+
+	return ruleUpdated, nil
+}
+
+func (e *ContextEnforcer) updatePoliciesWithoutNotifyCtx(ctx context.Context, sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, error) {
+	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))
+	}
+
+	if e.dispatcher != nil && e.autoNotifyDispatcher {
+		return true, e.dispatcher.UpdatePolicies(sec, ptype, oldRules, newRules)
+	}
+
+	if e.shouldPersist() {
+		if err := e.adapterCtx.(persist.ContextUpdatableAdapter).UpdatePoliciesCtx(ctx, sec, ptype, oldRules, newRules); err != nil {
+			if err.Error() != notImplemented {
+				return false, err
+			}
+		}
+	}
+
+	ruleUpdated, err := e.model.UpdatePolicies(sec, ptype, oldRules, newRules)
+	if !ruleUpdated || err != nil {
+		return ruleUpdated, err
+	}
+
+	if sec == "g" {
+		err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rules
+		if err != nil {
+			return ruleUpdated, err
+		}
+		err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rules
+		if err != nil {
+			return ruleUpdated, err
+		}
+	}
+
+	return ruleUpdated, nil
+}
+
+func (e *ContextEnforcer) addPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) {
+	ok, err := e.addPolicyWithoutNotifyCtx(ctx, sec, ptype, rule)
+	if !ok || err != nil {
+		return ok, err
+	}
+
+	if e.shouldNotify() {
+		var err error
+		if watcher, ok := e.watcher.(persist.WatcherEx); ok {
+			err = watcher.UpdateForAddPolicy(sec, ptype, rule...)
+		} else {
+			err = e.watcher.Update()
+		}
+		return true, err
+	}
+
+	return true, nil
+}
+
+func (e *ContextEnforcer) addPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string, autoRemoveRepeat bool) (bool, error) {
+	ok, err := e.addPoliciesWithoutNotifyCtx(ctx, sec, ptype, rules, autoRemoveRepeat)
+	if !ok || err != nil {
+		return ok, err
+	}
+
+	if e.shouldNotify() {
+		var err error
+		if watcher, ok := e.watcher.(persist.WatcherEx); ok {
+			err = watcher.UpdateForAddPolicies(sec, ptype, rules...)
+		} else {
+			err = e.watcher.Update()
+		}
+		return true, err
+	}
+
+	return true, nil
+}
+
+func (e *ContextEnforcer) updatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule []string, newRule []string) (bool, error) {
+	ok, err := e.updatePolicyWithoutNotifyCtx(ctx, sec, ptype, oldRule, newRule)
+	if !ok || err != nil {
+		return ok, err
+	}
+
+	if e.shouldNotify() {
+		var err error
+		if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok {
+			err = watcher.UpdateForUpdatePolicy(sec, ptype, oldRule, newRule)
+		} else {
+			err = e.watcher.Update()
+		}
+		return true, err
+	}
+
+	return true, nil
+}
+
+func (e *ContextEnforcer) updatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules [][]string, newRules [][]string) (bool, error) {
+	ok, err := e.updatePoliciesWithoutNotifyCtx(ctx, sec, ptype, oldRules, newRules)
+	if !ok || err != nil {
+		return ok, err
+	}
+
+	if e.shouldNotify() {
+		var err error
+		if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok {
+			err = watcher.UpdateForUpdatePolicies(sec, ptype, oldRules, newRules)
+		} else {
+			err = e.watcher.Update()
+		}
+		return true, err
+	}
+
+	return true, nil
+}
+
+func (e *ContextEnforcer) removePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error) {
+	ok, err := e.removePolicyWithoutNotifyCtx(ctx, sec, ptype, rule)
+	if !ok || err != nil {
+		return ok, err
+	}
+
+	if e.shouldNotify() {
+		var err error
+		if watcher, ok := e.watcher.(persist.WatcherEx); ok {
+			err = watcher.UpdateForRemovePolicy(sec, ptype, rule...)
+		} else {
+			err = e.watcher.Update()
+		}
+		return true, err
+	}
+
+	return true, nil
+}
+
+func (e *ContextEnforcer) removePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error) {
+	ok, err := e.removePoliciesWithoutNotifyCtx(ctx, sec, ptype, rules)
+	if !ok || err != nil {
+		return ok, err
+	}
+
+	if e.shouldNotify() {
+		var err error
+		if watcher, ok := e.watcher.(persist.WatcherEx); ok {
+			err = watcher.UpdateForRemovePolicies(sec, ptype, rules...)
+		} else {
+			err = e.watcher.Update()
+		}
+		return true, err
+	}
+
+	return true, nil
+}
+
+// removeFilteredPolicy removes rules based on field filters from the current policy.
+func (e *ContextEnforcer) removeFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues []string) (bool, error) {
+	ok, err := e.removeFilteredPolicyWithoutNotifyCtx(ctx, sec, ptype, fieldIndex, fieldValues)
+	if !ok || err != nil {
+		return ok, err
+	}
+
+	if e.shouldNotify() {
+		var err error
+		if watcher, ok := e.watcher.(persist.WatcherEx); ok {
+			err = watcher.UpdateForRemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
+		} else {
+			err = e.watcher.Update()
+		}
+		return true, err
+	}
+
+	return true, nil
+}
+
+func (e *ContextEnforcer) updateFilteredPoliciesCtx(ctx context.Context, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) (bool, error) {
+	oldRules, err := e.updateFilteredPoliciesWithoutNotifyCtx(ctx, sec, ptype, newRules, fieldIndex, fieldValues...)
+	ok := len(oldRules) != 0
+	if !ok || err != nil {
+		return ok, err
+	}
+
+	if e.shouldNotify() {
+		var err error
+		if watcher, ok := e.watcher.(persist.UpdatableWatcher); ok {
+			err = watcher.UpdateForUpdatePolicies(sec, ptype, oldRules, newRules)
+		} else {
+			err = e.watcher.Update()
+		}
+		return true, err
+	}
+
+	return true, nil
+}
+
+func (e *ContextEnforcer) updateFilteredPoliciesWithoutNotifyCtx(ctx context.Context, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) {
+	var (
+		oldRules [][]string
+		err      error
+	)
+
+	if _, err = e.model.GetAssertion(sec, ptype); err != nil {
+		return oldRules, err
+	}
+
+	if e.shouldPersist() {
+		if oldRules, err = e.adapter.(persist.ContextUpdatableAdapter).UpdateFilteredPoliciesCtx(ctx, sec, ptype, newRules, fieldIndex, fieldValues...); err != nil {
+			if err.Error() != notImplemented {
+				return nil, err
+			}
+		}
+		// For compatibility, because some adapters return oldRules containing ptype, see https://github.com/casbin/xorm-adapter/issues/49
+		for i, oldRule := range oldRules {
+			if len(oldRules[i]) == len(e.model[sec][ptype].Tokens)+1 {
+				oldRules[i] = oldRule[1:]
+			}
+		}
+	}
+
+	if e.dispatcher != nil && e.autoNotifyDispatcher {
+		return oldRules, e.dispatcher.UpdateFilteredPolicies(sec, ptype, oldRules, newRules)
+	}
+
+	ruleChanged, err := e.model.RemovePolicies(sec, ptype, oldRules)
+	if err != nil {
+		return oldRules, err
+	}
+	err = e.model.AddPolicies(sec, ptype, newRules)
+	if err != nil {
+		return oldRules, err
+	}
+	ruleChanged = ruleChanged && len(newRules) != 0
+	if !ruleChanged {
+		return make([][]string, 0), nil
+	}
+
+	if sec == "g" {
+		err := e.BuildIncrementalRoleLinks(model.PolicyRemove, ptype, oldRules) // remove the old rules
+		if err != nil {
+			return oldRules, err
+		}
+		err = e.BuildIncrementalRoleLinks(model.PolicyAdd, ptype, newRules) // add the new rules
+		if err != nil {
+			return oldRules, err
+		}
+	}
+
+	return oldRules, nil
+}
diff --git a/enforcer_context_interface.go b/enforcer_context_interface.go
new file mode 100644
index 0000000..95c36c8
--- /dev/null
+++ b/enforcer_context_interface.go
@@ -0,0 +1,96 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// Licensed 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 "context"
+
+type IEnforcerContext interface {
+	IEnforcer
+
+	/* Enforcer API */
+	LoadPolicyCtx(ctx context.Context) error
+	LoadFilteredPolicyCtx(ctx context.Context, filter interface{}) error
+	LoadIncrementalFilteredPolicyCtx(ctx context.Context, filter interface{}) error
+	IsFilteredCtx(ctx context.Context) bool
+	SavePolicyCtx(ctx context.Context) error
+
+	/* RBAC API */
+	AddRoleForUserCtx(ctx context.Context, user string, role string, domain ...string) (bool, error)
+	AddPermissionForUserCtx(ctx context.Context, user string, permission ...string) (bool, error)
+	AddPermissionsForUserCtx(ctx context.Context, user string, permissions ...[]string) (bool, error)
+	DeletePermissionForUserCtx(ctx context.Context, user string, permission ...string) (bool, error)
+	DeletePermissionsForUserCtx(ctx context.Context, user string) (bool, error)
+
+	DeleteRoleForUserCtx(ctx context.Context, user string, role string, domain ...string) (bool, error)
+	DeleteRolesForUserCtx(ctx context.Context, user string, domain ...string) (bool, error)
+	DeleteUserCtx(ctx context.Context, user string) (bool, error)
+	DeleteRoleCtx(ctx context.Context, role string) (bool, error)
+	DeletePermissionCtx(ctx context.Context, permission ...string) (bool, error)
+
+	/* RBAC API with domains*/
+	AddRoleForUserInDomainCtx(ctx context.Context, user string, role string, domain string) (bool, error)
+	DeleteRoleForUserInDomainCtx(ctx context.Context, user string, role string, domain string) (bool, error)
+	DeleteRolesForUserInDomainCtx(ctx context.Context, user string, domain string) (bool, error)
+	DeleteAllUsersByDomainCtx(ctx context.Context, domain string) (bool, error)
+	DeleteDomainsCtx(ctx context.Context, domains ...string) (bool, error)
+
+	/* Management API */
+	AddPolicyCtx(ctx context.Context, params ...interface{}) (bool, error)
+	AddPoliciesCtx(ctx context.Context, rules [][]string) (bool, error)
+	AddNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error)
+	AddNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
+	AddPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error)
+	AddNamedPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
+
+	RemovePolicyCtx(ctx context.Context, params ...interface{}) (bool, error)
+	RemovePoliciesCtx(ctx context.Context, rules [][]string) (bool, error)
+	RemoveFilteredPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error)
+	RemoveNamedPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error)
+	RemoveNamedPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
+	RemoveFilteredNamedPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error)
+
+	AddGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error)
+	AddGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error)
+	AddGroupingPoliciesExCtx(ctx context.Context, rules [][]string) (bool, error)
+	AddNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error)
+	AddNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
+	AddNamedGroupingPoliciesExCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
+
+	RemoveGroupingPolicyCtx(ctx context.Context, params ...interface{}) (bool, error)
+	RemoveGroupingPoliciesCtx(ctx context.Context, rules [][]string) (bool, error)
+	RemoveFilteredGroupingPolicyCtx(ctx context.Context, fieldIndex int, fieldValues ...string) (bool, error)
+	RemoveNamedGroupingPolicyCtx(ctx context.Context, ptype string, params ...interface{}) (bool, error)
+	RemoveNamedGroupingPoliciesCtx(ctx context.Context, ptype string, rules [][]string) (bool, error)
+	RemoveFilteredNamedGroupingPolicyCtx(ctx context.Context, ptype string, fieldIndex int, fieldValues ...string) (bool, error)
+
+	UpdatePolicyCtx(ctx context.Context, oldPolicy []string, newPolicy []string) (bool, error)
+	UpdatePoliciesCtx(ctx context.Context, oldPolicies [][]string, newPolicies [][]string) (bool, error)
+	UpdateFilteredPoliciesCtx(ctx context.Context, newPolicies [][]string, fieldIndex int, fieldValues ...string) (bool, error)
+
+	UpdateGroupingPolicyCtx(ctx context.Context, oldRule []string, newRule []string) (bool, error)
+	UpdateGroupingPoliciesCtx(ctx context.Context, oldRules [][]string, newRules [][]string) (bool, error)
+	UpdateNamedGroupingPolicyCtx(ctx context.Context, ptype string, oldRule []string, newRule []string) (bool, error)
+	UpdateNamedGroupingPoliciesCtx(ctx context.Context, ptype string, oldRules [][]string, newRules [][]string) (bool, error)
+
+	/* Management API with autoNotifyWatcher disabled */
+	SelfAddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error)
+	SelfAddPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error)
+	SelfAddPoliciesExCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error)
+	SelfRemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) (bool, error)
+	SelfRemovePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) (bool, error)
+	SelfRemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) (bool, error)
+	SelfUpdatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule, newRule []string) (bool, error)
+	SelfUpdatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules, newRules [][]string) (bool, error)
+}
diff --git a/persist/file-adapter/adapter_context.go b/persist/file-adapter/adapter_context.go
new file mode 100644
index 0000000..60f11ce
--- /dev/null
+++ b/persist/file-adapter/adapter_context.go
@@ -0,0 +1,117 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// Licensed 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 fileadapter
+
+import (
+	"context"
+
+	"github.com/casbin/casbin/v2/model"
+)
+
+func (a *Adapter) UpdatePolicyCtx(ctx context.Context, sec string, ptype string, oldRule, newRule []string) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.UpdatePolicy(sec, ptype, oldRule, newRule)
+}
+
+func (a *Adapter) UpdatePoliciesCtx(ctx context.Context, sec string, ptype string, oldRules, newRules [][]string) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.UpdatePolicies(sec, ptype, oldRules, newRules)
+}
+
+func (a *Adapter) UpdateFilteredPoliciesCtx(ctx context.Context, sec string, ptype string, newRules [][]string, fieldIndex int, fieldValues ...string) ([][]string, error) {
+	if err := checkCtx(ctx); err != nil {
+		return nil, err
+	}
+
+	return a.UpdateFilteredPolicies(sec, ptype, newRules, fieldIndex, fieldValues...)
+}
+
+// LoadPolicyCtx loads all policy rules from the storage with context.
+func (a *Adapter) LoadPolicyCtx(ctx context.Context, model model.Model) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.LoadPolicy(model)
+}
+
+// SavePolicyCtx saves all policy rules to the storage with context.
+func (a *Adapter) SavePolicyCtx(ctx context.Context, model model.Model) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.SavePolicy(model)
+}
+
+// AddPolicyCtx adds a policy rule to the storage with context.
+func (a *Adapter) AddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.AddPolicy(sec, ptype, rule)
+}
+
+// AddPoliciesCtx adds policy rules to the storage with context.
+func (a *Adapter) AddPoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.AddPolicies(sec, ptype, rules)
+}
+
+// RemovePolicyCtx removes a policy rule from the storage with context.
+func (a *Adapter) RemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.RemovePolicy(sec, ptype, rule)
+}
+
+// RemovePoliciesCtx removes policy rules from the storage with context.
+func (a *Adapter) RemovePoliciesCtx(ctx context.Context, sec string, ptype string, rules [][]string) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.RemovePolicies(sec, ptype, rules)
+}
+
+// RemoveFilteredPolicyCtx removes policy rules that match the filter from the storage with context.
+func (a *Adapter) RemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
+}
+
+func checkCtx(ctx context.Context) error {
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+		return nil
+	}
+}
diff --git a/persist/file-adapter/adapter_filtered_context.go b/persist/file-adapter/adapter_filtered_context.go
new file mode 100644
index 0000000..b92d7fb
--- /dev/null
+++ b/persist/file-adapter/adapter_filtered_context.go
@@ -0,0 +1,57 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// Licensed 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 fileadapter
+
+import (
+	"context"
+
+	"github.com/casbin/casbin/v2/model"
+)
+
+// LoadPolicyCtx loads all policy rules from the storage with context.
+func (a *FilteredAdapter) LoadPolicyCtx(ctx context.Context, model model.Model) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.LoadPolicy(model)
+}
+
+// LoadFilteredPolicyCtx loads only policy rules that match the filter with context.
+func (a *FilteredAdapter) LoadFilteredPolicyCtx(ctx context.Context, model model.Model, filter interface{}) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.LoadFilteredPolicy(model, filter)
+}
+
+// SavePolicyCtx saves all policy rules to the storage with context.
+func (a *FilteredAdapter) SavePolicyCtx(ctx context.Context, model model.Model) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.SavePolicy(model)
+}
+
+// IsFilteredCtx returns true if the loaded policy has been filtered with context.
+func (a *FilteredAdapter) IsFilteredCtx(ctx context.Context) bool {
+	if err := checkCtx(ctx); err != nil {
+		return false
+	}
+
+	return a.IsFiltered()
+}
diff --git a/persist/string-adapter/adapter_context.go b/persist/string-adapter/adapter_context.go
new file mode 100644
index 0000000..881430d
--- /dev/null
+++ b/persist/string-adapter/adapter_context.go
@@ -0,0 +1,75 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// Licensed 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 stringadapter
+
+import (
+	"context"
+
+	"github.com/casbin/casbin/v2/model"
+)
+
+// LoadPolicyCtx loads all policy rules from the storage with context.
+func (a *Adapter) LoadPolicyCtx(ctx context.Context, model model.Model) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.LoadPolicy(model)
+}
+
+// SavePolicyCtx saves all policy rules to the storage with context.
+func (a *Adapter) SavePolicyCtx(ctx context.Context, model model.Model) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.SavePolicy(model)
+}
+
+// AddPolicyCtx adds a policy rule to the storage with context.
+func (a *Adapter) AddPolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.AddPolicy(sec, ptype, rule)
+}
+
+// RemovePolicyCtx removes a policy rule from the storage with context.
+func (a *Adapter) RemovePolicyCtx(ctx context.Context, sec string, ptype string, rule []string) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.RemovePolicy(sec, ptype, rule)
+}
+
+// RemoveFilteredPolicyCtx removes policy rules that match the filter from the storage with context.
+func (a *Adapter) RemoveFilteredPolicyCtx(ctx context.Context, sec string, ptype string, fieldIndex int, fieldValues ...string) error {
+	if err := checkCtx(ctx); err != nil {
+		return err
+	}
+
+	return a.RemoveFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
+}
+
+func checkCtx(ctx context.Context) error {
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+		return nil
+	}
+}
diff --git a/rbac_api_context.go b/rbac_api_context.go
new file mode 100644
index 0000000..fd39eb2
--- /dev/null
+++ b/rbac_api_context.go
@@ -0,0 +1,131 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// Licensed 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.
+
+// rbac_api_context.go
+package casbin
+
+import (
+	"context"
+
+	"github.com/casbin/casbin/v2/constant"
+	"github.com/casbin/casbin/v2/errors"
+	"github.com/casbin/casbin/v2/util"
+)
+
+// AddRoleForUserCtx adds a role for a user with context support.
+// Returns false if the user already has the role (aka not affected).
+func (e *ContextEnforcer) AddRoleForUserCtx(ctx context.Context, user string, role string, domain ...string) (bool, error) {
+	args := []string{user, role}
+	args = append(args, domain...)
+	return e.AddGroupingPolicyCtx(ctx, args)
+}
+
+// DeleteRoleForUserCtx deletes a role for a user with context support.
+// Returns false if the user does not have the role (aka not affected).
+func (e *ContextEnforcer) DeleteRoleForUserCtx(ctx context.Context, user string, role string, domain ...string) (bool, error) {
+	args := []string{user, role}
+	args = append(args, domain...)
+	return e.RemoveGroupingPolicyCtx(ctx, args)
+}
+
+// DeleteRolesForUserCtx deletes all roles for a user with context support.
+// Returns false if the user does not have any roles (aka not affected).
+func (e *ContextEnforcer) DeleteRolesForUserCtx(ctx context.Context, user string, domain ...string) (bool, error) {
+	var args []string
+	if len(domain) == 0 {
+		args = []string{user}
+	} else if len(domain) > 1 {
+		return false, errors.ErrDomainParameter
+	} else {
+		args = []string{user, "", domain[0]}
+	}
+	return e.RemoveFilteredGroupingPolicyCtx(ctx, 0, args...)
+}
+
+// DeleteUserCtx deletes a user with context support.
+// Returns false if the user does not exist (aka not affected).
+func (e *ContextEnforcer) DeleteUserCtx(ctx context.Context, user string) (bool, error) {
+	var err error
+	res1, err := e.RemoveFilteredGroupingPolicyCtx(ctx, 0, user)
+	if err != nil {
+		return res1, err
+	}
+
+	subIndex, err := e.GetFieldIndex("p", constant.SubjectIndex)
+	if err != nil {
+		return false, err
+	}
+	res2, err := e.RemoveFilteredPolicyCtx(ctx, subIndex, user)
+	return res1 || res2, err
+}
+
+// DeleteRoleCtx deletes a role with context support.
+// Returns false if the role does not exist (aka not affected).
+func (e *ContextEnforcer) DeleteRoleCtx(ctx context.Context, role string) (bool, error) {
+	var err error
+	res1, err := e.RemoveFilteredGroupingPolicyCtx(ctx, 0, role)
+	if err != nil {
+		return res1, err
+	}
+
+	res2, err := e.RemoveFilteredGroupingPolicyCtx(ctx, 1, role)
+	if err != nil {
+		return res1, err
+	}
+
+	subIndex, err := e.GetFieldIndex("p", constant.SubjectIndex)
+	if err != nil {
+		return false, err
+	}
+	res3, err := e.RemoveFilteredPolicyCtx(ctx, subIndex, role)
+	return res1 || res2 || res3, err
+}
+
+// DeletePermissionCtx deletes a permission with context support.
+// Returns false if the permission does not exist (aka not affected).
+func (e *ContextEnforcer) DeletePermissionCtx(ctx context.Context, permission ...string) (bool, error) {
+	return e.RemoveFilteredPolicyCtx(ctx, 1, permission...)
+}
+
+// AddPermissionForUserCtx adds a permission for a user or role with context support.
+// Returns false if the user or role already has the permission (aka not affected).
+func (e *ContextEnforcer) AddPermissionForUserCtx(ctx context.Context, user string, permission ...string) (bool, error) {
+	return e.AddPolicyCtx(ctx, util.JoinSlice(user, permission...))
+}
+
+// AddPermissionsForUserCtx adds multiple permissions for a user or role with context support.
+// Returns false if the user or role already has one of the permissions (aka not affected).
+func (e *ContextEnforcer) AddPermissionsForUserCtx(ctx context.Context, user string, permissions ...[]string) (bool, error) {
+	var rules [][]string
+	for _, permission := range permissions {
+		rules = append(rules, util.JoinSlice(user, permission...))
+	}
+	return e.AddPoliciesCtx(ctx, rules)
+}
+
+// DeletePermissionForUserCtx deletes a permission for a user or role with context support.
+// Returns false if the user or role does not have the permission (aka not affected).
+func (e *ContextEnforcer) DeletePermissionForUserCtx(ctx context.Context, user string, permission ...string) (bool, error) {
+	return e.RemovePolicyCtx(ctx, util.JoinSlice(user, permission...))
+}
+
+// DeletePermissionsForUserCtx deletes permissions for a user or role with context support.
+// Returns false if the user or role does not have any permissions (aka not affected).
+func (e *ContextEnforcer) DeletePermissionsForUserCtx(ctx context.Context, user string) (bool, error) {
+	subIndex, err := e.GetFieldIndex("p", constant.SubjectIndex)
+	if err != nil {
+		return false, err
+	}
+	return e.RemoveFilteredPolicyCtx(ctx, subIndex, user)
+}
diff --git a/rbac_api_with_domains_context.go b/rbac_api_with_domains_context.go
new file mode 100644
index 0000000..78f3f9e
--- /dev/null
+++ b/rbac_api_with_domains_context.go
@@ -0,0 +1,113 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// Licensed 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 (
+	"context"
+	"fmt"
+
+	"github.com/casbin/casbin/v2/constant"
+)
+
+// AddRoleForUserInDomainCtx adds a role for a user inside a domain with context support.
+// Returns false if the user already has the role (aka not affected).
+func (e *ContextEnforcer) AddRoleForUserInDomainCtx(ctx context.Context, user string, role string, domain string) (bool, error) {
+	return e.AddGroupingPolicyCtx(ctx, user, role, domain)
+}
+
+// DeleteRoleForUserInDomainCtx deletes a role for a user inside a domain with context support.
+// Returns false if the user does not have the role (aka not affected).
+func (e *ContextEnforcer) DeleteRoleForUserInDomainCtx(ctx context.Context, user string, role string, domain string) (bool, error) {
+	return e.RemoveGroupingPolicyCtx(ctx, user, role, domain)
+}
+
+// DeleteRolesForUserInDomainCtx deletes all roles for a user inside a domain with context support.
+// Returns false if the user does not have any roles (aka not affected).
+func (e *ContextEnforcer) DeleteRolesForUserInDomainCtx(ctx context.Context, user string, domain string) (bool, error) {
+	if e.GetRoleManager() == nil {
+		return false, fmt.Errorf("role manager is not initialized")
+	}
+	roles, err := e.GetRoleManager().GetRoles(user, domain)
+	if err != nil {
+		return false, err
+	}
+
+	var rules [][]string
+	for _, role := range roles {
+		rules = append(rules, []string{user, role, domain})
+	}
+
+	return e.RemoveGroupingPoliciesCtx(ctx, rules)
+}
+
+// DeleteAllUsersByDomainCtx deletes all users associated with the domain with context support.
+func (e *ContextEnforcer) DeleteAllUsersByDomainCtx(ctx context.Context, domain string) (bool, error) {
+	g, err := e.model.GetAssertion("g", "g")
+	if err != nil {
+		return false, err
+	}
+	p := e.model["p"]["p"]
+	index, err := e.GetFieldIndex("p", constant.DomainIndex)
+	if err != nil {
+		return false, err
+	}
+
+	getUser := func(index int, policies [][]string, domain string) [][]string {
+		if len(policies) == 0 || len(policies[0]) <= index {
+			return [][]string{}
+		}
+		res := make([][]string, 0)
+		for _, policy := range policies {
+			if policy[index] == domain {
+				res = append(res, policy)
+			}
+		}
+		return res
+	}
+
+	users := getUser(2, g.Policy, domain)
+	if _, err = e.RemoveGroupingPoliciesCtx(ctx, users); err != nil {
+		return false, err
+	}
+	users = getUser(index, p.Policy, domain)
+	if _, err = e.RemovePoliciesCtx(ctx, users); err != nil {
+		return false, err
+	}
+	return true, nil
+}
+
+// DeleteDomainsCtx deletes all associated policies for domains with context support.
+// It would delete all domains if parameter is not provided.
+func (e *ContextEnforcer) DeleteDomainsCtx(ctx context.Context, domains ...string) (bool, error) {
+	if len(domains) == 0 {
+		var err error
+		domains, err = e.GetAllDomains()
+		if err != nil {
+			return false, err
+		}
+	}
+	for _, domain := range domains {
+		if _, err := e.DeleteAllUsersByDomainCtx(ctx, domain); err != nil {
+			return false, err
+		}
+		// remove the domain from the RoleManager.
+		if e.GetRoleManager() != nil {
+			if err := e.GetRoleManager().DeleteDomain(domain); err != nil {
+				return false, err
+			}
+		}
+	}
+	return true, nil
+}



Test patch:
diff --git a/enforcer_context_test.go b/enforcer_context_test.go
new file mode 100644
index 0000000..4f07d00
--- /dev/null
+++ b/enforcer_context_test.go
@@ -0,0 +1,232 @@
+// Copyright 2025 The casbin Authors. All Rights Reserved.
+//
+// Licensed 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 (
+	"context"
+	"testing"
+	"time"
+)
+
+func TestIEnforcerContext_BasicOperations(t *testing.T) {
+	e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+	if err != nil {
+		t.Fatalf("NewContextEnforcer failed: %v", err)
+	}
+
+	ctx := context.Background()
+
+	err = e.LoadPolicyCtx(ctx)
+	if err != nil {
+		t.Fatalf("LoadPolicyCtx failed: %v", err)
+	}
+
+	added, err := e.AddPolicyCtx(ctx, "eve", "data3", "read")
+	if err != nil {
+		t.Fatalf("AddPolicyCtx failed: %v", err)
+	}
+	if !added {
+		t.Error("AddPolicyCtx should return true for new policy")
+	}
+
+	removed, err := e.RemovePolicyCtx(ctx, "eve", "data3", "read")
+	if err != nil {
+		t.Fatalf("RemovePolicyCtx failed: %v", err)
+	}
+	if !removed {
+		t.Error("RemovePolicyCtx should return true for existing policy")
+	}
+
+	err = e.SavePolicyCtx(ctx)
+	if err != nil {
+		t.Fatalf("SavePolicyCtx failed: %v", err)
+	}
+}
+
+func TestIEnforcerContext_RBACOperations(t *testing.T) {
+	e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+	if err != nil {
+		t.Fatalf("NewContextEnforcer failed: %v", err)
+	}
+
+	ctx := context.Background()
+
+	added, err := e.AddRoleForUserCtx(ctx, "eve", "data1_admin")
+	if err != nil {
+		t.Fatalf("AddRoleForUserCtx failed: %v", err)
+	}
+	if !added {
+		t.Error("AddRoleForUserCtx should return true for new role")
+	}
+
+	added, err = e.AddPermissionForUserCtx(ctx, "eve", "data3", "write")
+	if err != nil {
+		t.Fatalf("AddPermissionForUserCtx failed: %v", err)
+	}
+	if !added {
+		t.Error("AddPermissionForUserCtx should return true for new permission")
+	}
+
+	deleted, err := e.DeleteRoleForUserCtx(ctx, "eve", "data1_admin")
+	if err != nil {
+		t.Fatalf("DeleteRoleForUserCtx failed: %v", err)
+	}
+	if !deleted {
+		t.Error("DeleteRoleForUserCtx should return true for existing role")
+	}
+
+	deleted, err = e.DeletePermissionForUserCtx(ctx, "eve", "data3", "write")
+	if err != nil {
+		t.Fatalf("DeletePermissionForUserCtx failed: %v", err)
+	}
+	if !deleted {
+		t.Error("DeletePermissionForUserCtx should return true for existing permission")
+	}
+}
+
+func TestIEnforcerContext_BatchOperations(t *testing.T) {
+	e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+	if err != nil {
+		t.Fatalf("NewContextEnforcer failed: %v", err)
+	}
+
+	ctx := context.Background()
+
+	rules := [][]string{
+		{"eve", "data3", "read"},
+		{"eve", "data3", "write"},
+	}
+	added, err := e.AddPoliciesCtx(ctx, rules)
+	if err != nil {
+		t.Fatalf("AddPoliciesCtx failed: %v", err)
+	}
+	if !added {
+		t.Error("AddPoliciesCtx should return true for new policies")
+	}
+
+	removed, err := e.RemovePoliciesCtx(ctx, rules)
+	if err != nil {
+		t.Fatalf("RemovePoliciesCtx failed: %v", err)
+	}
+	if !removed {
+		t.Error("RemovePoliciesCtx should return true for existing policies")
+	}
+}
+
+func TestIEnforcerContext_ContextCancellation(t *testing.T) {
+	e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+	if err != nil {
+		t.Fatalf("NewContextEnforcer failed: %v", err)
+	}
+
+	ctx, cancel := context.WithCancel(context.Background())
+	cancel()
+
+	err = e.LoadPolicyCtx(ctx)
+	if err != nil {
+		t.Logf("LoadPolicyCtx with cancelled context returned error: %v", err)
+	}
+}
+
+func TestIEnforcerContext_ContextTimeout(t *testing.T) {
+	e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+	if err != nil {
+		t.Fatalf("NewContextEnforcer failed: %v", err)
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
+	defer cancel()
+
+	time.Sleep(2 * time.Millisecond)
+
+	_, err = e.AddPolicyCtx(ctx, "test", "data", "read")
+	if err != nil {
+		t.Logf("AddPolicyCtx with timeout context returned error: %v", err)
+	}
+}
+
+func TestIEnforcerContext_GroupingPolicyOperations(t *testing.T) {
+	e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+	if err != nil {
+		t.Fatalf("NewContextEnforcer failed: %v", err)
+	}
+
+	ctx := context.Background()
+
+	added, err := e.AddGroupingPolicyCtx(ctx, "eve", "data3_admin")
+	if err != nil {
+		t.Fatalf("AddGroupingPolicyCtx failed: %v", err)
+	}
+	if !added {
+		t.Error("AddGroupingPolicyCtx should return true for new grouping policy")
+	}
+
+	removed, err := e.RemoveGroupingPolicyCtx(ctx, "eve", "data3_admin")
+	if err != nil {
+		t.Fatalf("RemoveGroupingPolicyCtx failed: %v", err)
+	}
+	if !removed {
+		t.Error("RemoveGroupingPolicyCtx should return true for existing grouping policy")
+	}
+}
+
+func TestIEnforcerContext_UpdateOperations(t *testing.T) {
+	e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+	if err != nil {
+		t.Fatalf("NewContextEnforcer failed: %v", err)
+	}
+
+	ctx := context.Background()
+
+	_, err = e.AddPolicyCtx(ctx, "eve", "data3", "read")
+	if err != nil {
+		t.Fatalf("AddPolicyCtx failed: %v", err)
+	}
+
+	updated, err := e.UpdatePolicyCtx(ctx, []string{"eve", "data3", "read"}, []string{"eve", "data3", "write"})
+	if err != nil {
+		t.Fatalf("UpdatePolicyCtx failed: %v", err)
+	}
+	if !updated {
+		t.Error("UpdatePolicyCtx should return true for successful update")
+	}
+
+	_, _ = e.RemovePolicyCtx(ctx, "eve", "data3", "write")
+}
+
+func TestIEnforcerContext_SelfMethods(t *testing.T) {
+	e, err := NewContextEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
+	if err != nil {
+		t.Fatalf("NewContextEnforcer failed: %v", err)
+	}
+
+	ctx := context.Background()
+
+	added, err := e.SelfAddPolicyCtx(ctx, "p", "p", []string{"eve", "data3", "read"})
+	if err != nil {
+		t.Fatalf("SelfAddPolicyCtx failed: %v", err)
+	}
+	if !added {
+		t.Error("SelfAddPolicyCtx should return true for new policy")
+	}
+
+	removed, err := e.SelfRemovePolicyCtx(ctx, "p", "p", []string{"eve", "data3", "read"})
+	if err != nil {
+		t.Fatalf("SelfRemovePolicyCtx failed: %v", err)
+	}
+	if !removed {
+		t.Error("SelfRemovePolicyCtx should return true for existing policy")
+	}
+}