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 34297a1  feat: add grouping, batch update and filtered remove APIs to 
Transaction
34297a1 is described below

commit 34297a19bd65cd52d40d146452c20aea7b45c205
Author: Yang Luo <[email protected]>
AuthorDate: Fri Aug 21 16:53:50 2026 +0800

    feat: add grouping, batch update and filtered remove APIs to Transaction
---
 transaction.go      | 401 ++++++++++++++++++++++++++++------------------------
 transaction_test.go | 238 +++++++++++++++++++++++++++++++
 2 files changed, 451 insertions(+), 188 deletions(-)

diff --git a/transaction.go b/transaction.go
index 387432c..eb08512 100644
--- a/transaction.go
+++ b/transaction.go
@@ -20,6 +20,7 @@ package casbin
 import (
        "context"
        "errors"
+       "fmt"
        "sync"
        "time"
 
@@ -70,12 +71,6 @@ func (tx *Transaction) GetAdapter() persist.Adapter {
        return tx.txContext.GetAdapter()
 }
 
-// AddPolicy adds a policy within the transaction.
-// The policy is buffered and will be applied when the transaction is 
committed.
-func (tx *Transaction) AddPolicy(params ...interface{}) (bool, error) {
-       return tx.AddNamedPolicy("p", params...)
-}
-
 // buildRuleFromParams converts parameters to a rule slice.
 func (tx *Transaction) buildRuleFromParams(params ...interface{}) []string {
        if len(params) == 1 {
@@ -101,72 +96,86 @@ func (tx *Transaction) checkTransactionStatus() error {
        return nil
 }
 
-// AddNamedPolicy adds a named policy within the transaction.
-// The policy is buffered and will be applied when the transaction is 
committed.
-func (tx *Transaction) AddNamedPolicy(ptype string, params ...interface{}) 
(bool, error) {
+// getBufferedModel returns the model with all buffered operations applied.
+// The caller must hold tx.mutex.
+func (tx *Transaction) getBufferedModel() (model.Model, error) {
+       if err := tx.checkTransactionStatus(); err != nil {
+               return nil, err
+       }
+
+       return tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+}
+
+// bufferAdd buffers an add operation for the rules that are not present yet.
+// Rules that already exist in the buffered model are skipped, and false is
+// returned when nothing is left to add.
+func (tx *Transaction) bufferAdd(sec string, ptype string, rules [][]string) 
(bool, error) {
        tx.mutex.Lock()
        defer tx.mutex.Unlock()
 
-       if err := tx.checkTransactionStatus(); err != nil {
-               return false, err
+       if len(rules) == 0 {
+               if err := tx.checkTransactionStatus(); err != nil {
+                       return false, err
+               }
+               return false, nil
        }
 
-       rule := tx.buildRuleFromParams(params...)
-
-       // Check if policy already exists in the buffered model.
-       bufferedModel, err := 
tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+       bufferedModel, err := tx.getBufferedModel()
        if err != nil {
                return false, err
        }
 
-       hasPolicy, err := bufferedModel.HasPolicy("p", ptype, rule)
-       if hasPolicy || err != nil {
-               return false, err
+       var validRules [][]string
+       for _, rule := range rules {
+               hasPolicy, err := bufferedModel.HasPolicy(sec, ptype, rule)
+               if err != nil {
+                       return false, err
+               }
+               if !hasPolicy {
+                       validRules = append(validRules, rule)
+               }
+       }
+
+       if len(validRules) == 0 {
+               return false, nil
        }
 
-       // Add operation to buffer.
-       op := persist.PolicyOperation{
+       tx.buffer.AddOperation(persist.PolicyOperation{
                Type:       persist.OperationAdd,
-               Section:    "p",
+               Section:    sec,
                PolicyType: ptype,
-               Rules:      [][]string{rule},
-       }
-       tx.buffer.AddOperation(op)
+               Rules:      validRules,
+       })
 
        return true, nil
 }
 
-// AddPolicies adds multiple policies within the transaction.
-func (tx *Transaction) AddPolicies(rules [][]string) (bool, error) {
-       return tx.AddNamedPolicies("p", rules)
-}
-
-// AddNamedPolicies adds multiple named policies within the transaction.
-func (tx *Transaction) AddNamedPolicies(ptype string, rules [][]string) (bool, 
error) {
+// bufferRemove buffers a remove operation for the rules that are present.
+// Rules that are missing from the buffered model are skipped, and false is
+// returned when nothing is left to remove.
+func (tx *Transaction) bufferRemove(sec string, ptype string, rules 
[][]string) (bool, error) {
        tx.mutex.Lock()
        defer tx.mutex.Unlock()
 
-       if err := tx.checkTransactionStatus(); err != nil {
-               return false, err
-       }
-
        if len(rules) == 0 {
+               if err := tx.checkTransactionStatus(); err != nil {
+                       return false, err
+               }
                return false, nil
        }
 
-       // Check if any policies already exist in the buffered model.
-       bufferedModel, err := 
tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+       bufferedModel, err := tx.getBufferedModel()
        if err != nil {
                return false, err
        }
 
        var validRules [][]string
        for _, rule := range rules {
-               hasPolicy, err := bufferedModel.HasPolicy("p", ptype, rule)
+               hasPolicy, err := bufferedModel.HasPolicy(sec, ptype, rule)
                if err != nil {
                        return false, err
                }
-               if !hasPolicy {
+               if hasPolicy {
                        validRules = append(validRules, rule)
                }
        }
@@ -175,108 +184,158 @@ func (tx *Transaction) AddNamedPolicies(ptype string, 
rules [][]string) (bool, e
                return false, nil
        }
 
-       // Add operation to buffer.
-       op := persist.PolicyOperation{
-               Type:       persist.OperationAdd,
-               Section:    "p",
+       tx.buffer.AddOperation(persist.PolicyOperation{
+               Type:       persist.OperationRemove,
+               Section:    sec,
                PolicyType: ptype,
                Rules:      validRules,
-       }
-       tx.buffer.AddOperation(op)
+       })
 
        return true, nil
 }
 
-// RemovePolicy removes a policy within the transaction.
-func (tx *Transaction) RemovePolicy(params ...interface{}) (bool, error) {
-       return tx.RemoveNamedPolicy("p", params...)
-}
-
-// RemoveNamedPolicy removes a named policy within the transaction.
-func (tx *Transaction) RemoveNamedPolicy(ptype string, params ...interface{}) 
(bool, error) {
+// bufferRemoveFiltered resolves the filter against the buffered model and
+// buffers the matched rules for removal. Resolving the filter here keeps the
+// operation a plain remove, so it works with every adapter and takes part in
+// conflict detection like any other remove.
+func (tx *Transaction) bufferRemoveFiltered(sec string, ptype string, 
fieldIndex int, fieldValues []string) (bool, error) {
        tx.mutex.Lock()
        defer tx.mutex.Unlock()
 
-       if err := tx.checkTransactionStatus(); err != nil {
+       bufferedModel, err := tx.getBufferedModel()
+       if err != nil {
                return false, err
        }
 
-       rule := tx.buildRuleFromParams(params...)
-
-       // Check if policy exists in the buffered model.
-       bufferedModel, err := 
tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+       matched, err := bufferedModel.GetFilteredPolicy(sec, ptype, fieldIndex, 
fieldValues...)
        if err != nil {
                return false, err
        }
+       if len(matched) == 0 {
+               return false, nil
+       }
 
-       hasPolicy, err := bufferedModel.HasPolicy("p", ptype, rule)
-       if !hasPolicy || err != nil {
-               return false, err
+       // The matched rules belong to a throwaway model copy, so keep our own 
copy.
+       rules := make([][]string, 0, len(matched))
+       for _, rule := range matched {
+               rules = append(rules, append([]string(nil), rule...))
        }
 
-       // Add operation to buffer.
-       op := persist.PolicyOperation{
+       tx.buffer.AddOperation(persist.PolicyOperation{
                Type:       persist.OperationRemove,
-               Section:    "p",
+               Section:    sec,
                PolicyType: ptype,
-               Rules:      [][]string{rule},
-       }
-       tx.buffer.AddOperation(op)
+               Rules:      rules,
+       })
 
        return true, nil
 }
 
-// RemovePolicies removes multiple policies within the transaction.
-func (tx *Transaction) RemovePolicies(rules [][]string) (bool, error) {
-       return tx.RemoveNamedPolicies("p", rules)
-}
-
-// RemoveNamedPolicies removes multiple named policies within the transaction.
-func (tx *Transaction) RemoveNamedPolicies(ptype string, rules [][]string) 
(bool, error) {
+// bufferUpdate buffers an update operation. The update is all-or-nothing: it 
is
+// buffered only if every old rule exists and no new rule exists yet.
+func (tx *Transaction) bufferUpdate(sec string, ptype string, oldRules 
[][]string, newRules [][]string) (bool, error) {
        tx.mutex.Lock()
        defer tx.mutex.Unlock()
 
-       if err := tx.checkTransactionStatus(); err != nil {
-               return false, err
+       if len(oldRules) != len(newRules) {
+               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 len(rules) == 0 {
+       if len(oldRules) == 0 {
+               if err := tx.checkTransactionStatus(); err != nil {
+                       return false, err
+               }
                return false, nil
        }
 
-       // Check if policies exist in the buffered model.
-       bufferedModel, err := 
tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+       bufferedModel, err := tx.getBufferedModel()
        if err != nil {
                return false, err
        }
 
-       var validRules [][]string
-       for _, rule := range rules {
-               hasPolicy, err := bufferedModel.HasPolicy("p", ptype, rule)
+       for i, oldRule := range oldRules {
+               hasOldPolicy, err := bufferedModel.HasPolicy(sec, ptype, 
oldRule)
                if err != nil {
                        return false, err
                }
-               if hasPolicy {
-                       validRules = append(validRules, rule)
+               if !hasOldPolicy {
+                       return false, nil
                }
-       }
 
-       if len(validRules) == 0 {
-               return false, nil
+               hasNewPolicy, err := bufferedModel.HasPolicy(sec, ptype, 
newRules[i])
+               if err != nil {
+                       return false, err
+               }
+               if hasNewPolicy {
+                       return false, nil
+               }
        }
 
-       // Add operation to buffer.
-       op := persist.PolicyOperation{
-               Type:       persist.OperationRemove,
-               Section:    "p",
+       tx.buffer.AddOperation(persist.PolicyOperation{
+               Type:       persist.OperationUpdate,
+               Section:    sec,
                PolicyType: ptype,
-               Rules:      validRules,
-       }
-       tx.buffer.AddOperation(op)
+               Rules:      newRules,
+               OldRules:   oldRules,
+       })
 
        return true, nil
 }
 
+// AddPolicy adds a policy within the transaction.
+// The policy is buffered and will be applied when the transaction is 
committed.
+func (tx *Transaction) AddPolicy(params ...interface{}) (bool, error) {
+       return tx.AddNamedPolicy("p", params...)
+}
+
+// AddNamedPolicy adds a named policy within the transaction.
+// The policy is buffered and will be applied when the transaction is 
committed.
+func (tx *Transaction) AddNamedPolicy(ptype string, params ...interface{}) 
(bool, error) {
+       return tx.bufferAdd("p", ptype, 
[][]string{tx.buildRuleFromParams(params...)})
+}
+
+// AddPolicies adds multiple policies within the transaction.
+func (tx *Transaction) AddPolicies(rules [][]string) (bool, error) {
+       return tx.AddNamedPolicies("p", rules)
+}
+
+// AddNamedPolicies adds multiple named policies within the transaction.
+func (tx *Transaction) AddNamedPolicies(ptype string, rules [][]string) (bool, 
error) {
+       return tx.bufferAdd("p", ptype, rules)
+}
+
+// RemovePolicy removes a policy within the transaction.
+func (tx *Transaction) RemovePolicy(params ...interface{}) (bool, error) {
+       return tx.RemoveNamedPolicy("p", params...)
+}
+
+// RemoveNamedPolicy removes a named policy within the transaction.
+func (tx *Transaction) RemoveNamedPolicy(ptype string, params ...interface{}) 
(bool, error) {
+       return tx.bufferRemove("p", ptype, 
[][]string{tx.buildRuleFromParams(params...)})
+}
+
+// RemovePolicies removes multiple policies within the transaction.
+func (tx *Transaction) RemovePolicies(rules [][]string) (bool, error) {
+       return tx.RemoveNamedPolicies("p", rules)
+}
+
+// RemoveNamedPolicies removes multiple named policies within the transaction.
+func (tx *Transaction) RemoveNamedPolicies(ptype string, rules [][]string) 
(bool, error) {
+       return tx.bufferRemove("p", ptype, rules)
+}
+
+// RemoveFilteredPolicy removes the policies that match the filter within the 
transaction.
+// An empty string in fieldValues means "match any value" for that field.
+func (tx *Transaction) RemoveFilteredPolicy(fieldIndex int, fieldValues 
...string) (bool, error) {
+       return tx.RemoveFilteredNamedPolicy("p", fieldIndex, fieldValues...)
+}
+
+// RemoveFilteredNamedPolicy removes the named policies that match the filter 
within the transaction.
+// An empty string in fieldValues means "match any value" for that field.
+func (tx *Transaction) RemoveFilteredNamedPolicy(ptype string, fieldIndex int, 
fieldValues ...string) (bool, error) {
+       return tx.bufferRemoveFiltered("p", ptype, fieldIndex, fieldValues)
+}
+
 // UpdatePolicy updates a policy within the transaction.
 func (tx *Transaction) UpdatePolicy(oldPolicy []string, newPolicy []string) 
(bool, error) {
        return tx.UpdateNamedPolicy("p", oldPolicy, newPolicy)
@@ -284,46 +343,19 @@ func (tx *Transaction) UpdatePolicy(oldPolicy []string, 
newPolicy []string) (boo
 
 // UpdateNamedPolicy updates a named policy within the transaction.
 func (tx *Transaction) UpdateNamedPolicy(ptype string, oldPolicy []string, 
newPolicy []string) (bool, error) {
-       tx.mutex.Lock()
-       defer tx.mutex.Unlock()
-
-       if err := tx.checkTransactionStatus(); err != nil {
-               return false, err
-       }
-
-       // Check if old policy exists and new policy doesn't exist.
-       bufferedModel, err := 
tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
-       if err != nil {
-               return false, err
-       }
-
-       hasOldPolicy, err := bufferedModel.HasPolicy("p", ptype, oldPolicy)
-       if err != nil {
-               return false, err
-       }
-       if !hasOldPolicy {
-               return false, nil
-       }
-
-       hasNewPolicy, errNew := bufferedModel.HasPolicy("p", ptype, newPolicy)
-       if errNew != nil {
-               return false, errNew
-       }
-       if hasNewPolicy {
-               return false, nil
-       }
+       return tx.bufferUpdate("p", ptype, [][]string{oldPolicy}, 
[][]string{newPolicy})
+}
 
-       // Add operation to buffer.
-       op := persist.PolicyOperation{
-               Type:       persist.OperationUpdate,
-               Section:    "p",
-               PolicyType: ptype,
-               Rules:      [][]string{newPolicy},
-               OldRules:   [][]string{oldPolicy},
-       }
-       tx.buffer.AddOperation(op)
+// UpdatePolicies updates multiple policies within the transaction.
+// Nothing is buffered unless every old policy exists and no new policy exists 
yet.
+func (tx *Transaction) UpdatePolicies(oldPolicies [][]string, newPolicies 
[][]string) (bool, error) {
+       return tx.UpdateNamedPolicies("p", oldPolicies, newPolicies)
+}
 
-       return true, nil
+// UpdateNamedPolicies updates multiple named policies within the transaction.
+// Nothing is buffered unless every old policy exists and no new policy exists 
yet.
+func (tx *Transaction) UpdateNamedPolicies(ptype string, oldPolicies 
[][]string, newPolicies [][]string) (bool, error) {
+       return tx.bufferUpdate("p", ptype, oldPolicies, newPolicies)
 }
 
 // AddGroupingPolicy adds a grouping policy within the transaction.
@@ -333,36 +365,17 @@ func (tx *Transaction) AddGroupingPolicy(params 
...interface{}) (bool, error) {
 
 // AddNamedGroupingPolicy adds a named grouping policy within the transaction.
 func (tx *Transaction) AddNamedGroupingPolicy(ptype string, params 
...interface{}) (bool, error) {
-       tx.mutex.Lock()
-       defer tx.mutex.Unlock()
-
-       if err := tx.checkTransactionStatus(); err != nil {
-               return false, err
-       }
-
-       rule := tx.buildRuleFromParams(params...)
-
-       // Check if grouping policy already exists in the buffered model.
-       bufferedModel, err := 
tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
-       if err != nil {
-               return false, err
-       }
-
-       hasPolicy, err := bufferedModel.HasPolicy("g", ptype, rule)
-       if hasPolicy || err != nil {
-               return false, err
-       }
+       return tx.bufferAdd("g", ptype, 
[][]string{tx.buildRuleFromParams(params...)})
+}
 
-       // Add operation to buffer.
-       op := persist.PolicyOperation{
-               Type:       persist.OperationAdd,
-               Section:    "g",
-               PolicyType: ptype,
-               Rules:      [][]string{rule},
-       }
-       tx.buffer.AddOperation(op)
+// AddGroupingPolicies adds multiple grouping policies within the transaction.
+func (tx *Transaction) AddGroupingPolicies(rules [][]string) (bool, error) {
+       return tx.AddNamedGroupingPolicies("g", rules)
+}
 
-       return true, nil
+// AddNamedGroupingPolicies adds multiple named grouping policies within the 
transaction.
+func (tx *Transaction) AddNamedGroupingPolicies(ptype string, rules 
[][]string) (bool, error) {
+       return tx.bufferAdd("g", ptype, rules)
 }
 
 // RemoveGroupingPolicy removes a grouping policy within the transaction.
@@ -372,36 +385,52 @@ func (tx *Transaction) RemoveGroupingPolicy(params 
...interface{}) (bool, error)
 
 // RemoveNamedGroupingPolicy removes a named grouping policy within the 
transaction.
 func (tx *Transaction) RemoveNamedGroupingPolicy(ptype string, params 
...interface{}) (bool, error) {
-       tx.mutex.Lock()
-       defer tx.mutex.Unlock()
+       return tx.bufferRemove("g", ptype, 
[][]string{tx.buildRuleFromParams(params...)})
+}
 
-       if err := tx.checkTransactionStatus(); err != nil {
-               return false, err
-       }
+// RemoveGroupingPolicies removes multiple grouping policies within the 
transaction.
+func (tx *Transaction) RemoveGroupingPolicies(rules [][]string) (bool, error) {
+       return tx.RemoveNamedGroupingPolicies("g", rules)
+}
 
-       rule := tx.buildRuleFromParams(params...)
+// RemoveNamedGroupingPolicies removes multiple named grouping policies within 
the transaction.
+func (tx *Transaction) RemoveNamedGroupingPolicies(ptype string, rules 
[][]string) (bool, error) {
+       return tx.bufferRemove("g", ptype, rules)
+}
 
-       // Check if grouping policy exists in the buffered model.
-       bufferedModel, err := 
tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
-       if err != nil {
-               return false, err
-       }
+// RemoveFilteredGroupingPolicy removes the grouping policies that match the 
filter within the transaction.
+// An empty string in fieldValues means "match any value" for that field.
+func (tx *Transaction) RemoveFilteredGroupingPolicy(fieldIndex int, 
fieldValues ...string) (bool, error) {
+       return tx.RemoveFilteredNamedGroupingPolicy("g", fieldIndex, 
fieldValues...)
+}
 
-       hasPolicy, err := bufferedModel.HasPolicy("g", ptype, rule)
-       if !hasPolicy || err != nil {
-               return false, err
-       }
+// RemoveFilteredNamedGroupingPolicy removes the named grouping policies that 
match
+// the filter within the transaction.
+// An empty string in fieldValues means "match any value" for that field.
+func (tx *Transaction) RemoveFilteredNamedGroupingPolicy(ptype string, 
fieldIndex int, fieldValues ...string) (bool, error) {
+       return tx.bufferRemoveFiltered("g", ptype, fieldIndex, fieldValues)
+}
 
-       // Add operation to buffer.
-       op := persist.PolicyOperation{
-               Type:       persist.OperationRemove,
-               Section:    "g",
-               PolicyType: ptype,
-               Rules:      [][]string{rule},
-       }
-       tx.buffer.AddOperation(op)
+// UpdateGroupingPolicy updates a grouping policy within the transaction.
+func (tx *Transaction) UpdateGroupingPolicy(oldRule []string, newRule 
[]string) (bool, error) {
+       return tx.UpdateNamedGroupingPolicy("g", oldRule, newRule)
+}
 
-       return true, nil
+// UpdateNamedGroupingPolicy updates a named grouping policy within the 
transaction.
+func (tx *Transaction) UpdateNamedGroupingPolicy(ptype string, oldRule 
[]string, newRule []string) (bool, error) {
+       return tx.bufferUpdate("g", ptype, [][]string{oldRule}, 
[][]string{newRule})
+}
+
+// UpdateGroupingPolicies updates multiple grouping policies within the 
transaction.
+// Nothing is buffered unless every old rule exists and no new rule exists yet.
+func (tx *Transaction) UpdateGroupingPolicies(oldRules [][]string, newRules 
[][]string) (bool, error) {
+       return tx.UpdateNamedGroupingPolicies("g", oldRules, newRules)
+}
+
+// UpdateNamedGroupingPolicies updates multiple named grouping policies within 
the transaction.
+// Nothing is buffered unless every old rule exists and no new rule exists yet.
+func (tx *Transaction) UpdateNamedGroupingPolicies(ptype string, oldRules 
[][]string, newRules [][]string) (bool, error) {
+       return tx.bufferUpdate("g", ptype, oldRules, newRules)
 }
 
 // GetBufferedModel returns the model as it would look after applying all 
buffered operations.
@@ -410,11 +439,7 @@ func (tx *Transaction) GetBufferedModel() (model.Model, 
error) {
        tx.mutex.RLock()
        defer tx.mutex.RUnlock()
 
-       if err := tx.checkTransactionStatus(); err != nil {
-               return nil, err
-       }
-
-       return tx.buffer.ApplyOperationsToModel(tx.buffer.GetModelSnapshot())
+       return tx.getBufferedModel()
 }
 
 // HasOperations returns true if the transaction has any buffered operations.
diff --git a/transaction_test.go b/transaction_test.go
index 555699f..466d65c 100644
--- a/transaction_test.go
+++ b/transaction_test.go
@@ -338,3 +338,241 @@ func TestTransactionBuffer(t *testing.T) {
 
        tx.Rollback()
 }
+
+// newTestTransactionalEnforcer creates a transactional enforcer backed by the 
mock adapter.
+func newTestTransactionalEnforcer(t *testing.T) *TransactionalEnforcer {
+       t.Helper()
+
+       adapter := NewMockTransactionalAdapter()
+       e, err := NewTransactionalEnforcer("examples/rbac_model.conf", adapter)
+       if err != nil {
+               t.Fatalf("Failed to create transactional enforcer: %v", err)
+       }
+       adapter.Enforcer = e.Enforcer
+
+       return e
+}
+
+// hasPolicy reports whether the enforcer model holds the given policy rule.
+func hasPolicy(t *testing.T, e *TransactionalEnforcer, params ...interface{}) 
bool {
+       t.Helper()
+
+       ok, err := e.HasPolicy(params...)
+       if err != nil {
+               t.Fatalf("Failed to check policy %v: %v", params, err)
+       }
+       return ok
+}
+
+// hasGroupingPolicy reports whether the enforcer model holds the given 
grouping policy rule.
+func hasGroupingPolicy(t *testing.T, e *TransactionalEnforcer, params 
...interface{}) bool {
+       t.Helper()
+
+       ok, err := e.HasGroupingPolicy(params...)
+       if err != nil {
+               t.Fatalf("Failed to check grouping policy %v: %v", params, err)
+       }
+       return ok
+}
+
+// Test batch grouping policy operations within a transaction.
+func TestTransactionGroupingPolicies(t *testing.T) {
+       e := newTestTransactionalEnforcer(t)
+       ctx := context.Background()
+
+       err := e.WithTransaction(ctx, func(tx *Transaction) error {
+               if ok, err := tx.AddPolicy("admin", "data1", "read"); !ok || 
err != nil {
+                       t.Fatalf("Failed to add policy in transaction: %v", err)
+               }
+               if ok, err := tx.AddGroupingPolicies([][]string{{"alice", 
"admin"}, {"bob", "admin"}}); !ok || err != nil {
+                       t.Fatalf("Failed to add grouping policies in 
transaction: %v", err)
+               }
+               return nil
+       })
+       if err != nil {
+               t.Fatalf("Failed to commit transaction: %v", err)
+       }
+
+       if !hasGroupingPolicy(t, e, "alice", "admin") || !hasGroupingPolicy(t, 
e, "bob", "admin") {
+               t.Fatal("Both grouping policies should have been added")
+       }
+
+       // Role links must have been rebuilt for the committed grouping 
policies.
+       if allowed, enforceErr := e.Enforce("alice", "data1", "read"); 
enforceErr != nil || !allowed {
+               t.Fatalf("alice should inherit the admin permission, got %v 
(err %v)", allowed, enforceErr)
+       }
+
+       // Removing them again must take effect too.
+       err = e.WithTransaction(ctx, func(tx *Transaction) error {
+               removed, removeErr := 
tx.RemoveGroupingPolicies([][]string{{"alice", "admin"}, {"bob", "admin"}})
+               if !removed || removeErr != nil {
+                       t.Fatalf("Failed to remove grouping policies in 
transaction: %v", removeErr)
+               }
+               return nil
+       })
+       if err != nil {
+               t.Fatalf("Failed to commit transaction: %v", err)
+       }
+
+       if hasGroupingPolicy(t, e, "alice", "admin") || hasGroupingPolicy(t, e, 
"bob", "admin") {
+               t.Fatal("Both grouping policies should have been removed")
+       }
+       if ok, _ := e.Enforce("alice", "data1", "read"); ok {
+               t.Fatal("alice should no longer inherit the admin permission")
+       }
+}
+
+// Test updating a grouping policy within a transaction.
+func TestTransactionUpdateGroupingPolicy(t *testing.T) {
+       e := newTestTransactionalEnforcer(t)
+       ctx := context.Background()
+
+       if err := e.WithTransaction(ctx, func(tx *Transaction) error {
+               _, err := tx.AddGroupingPolicy("alice", "admin")
+               return err
+       }); err != nil {
+               t.Fatalf("Failed to commit transaction: %v", err)
+       }
+
+       // Updating a rule that does not exist changes nothing.
+       if err := e.WithTransaction(ctx, func(tx *Transaction) error {
+               ok, err := tx.UpdateGroupingPolicy([]string{"carol", "admin"}, 
[]string{"carol", "user"})
+               if err != nil {
+                       return err
+               }
+               if ok {
+                       t.Fatal("Updating a missing grouping policy should 
return false")
+               }
+               return nil
+       }); err != nil {
+               t.Fatalf("Failed to commit transaction: %v", err)
+       }
+
+       // Mismatched batch lengths are rejected.
+       if err := e.WithTransaction(ctx, func(tx *Transaction) error {
+               if _, err := tx.UpdateGroupingPolicies([][]string{{"alice", 
"admin"}}, [][]string{}); err == nil {
+                       t.Fatal("Mismatched old/new rule counts should return 
an error")
+               }
+               return nil
+       }); err != nil {
+               t.Fatalf("Failed to commit transaction: %v", err)
+       }
+
+       if err := e.WithTransaction(ctx, func(tx *Transaction) error {
+               ok, err := tx.UpdateGroupingPolicy([]string{"alice", "admin"}, 
[]string{"alice", "user"})
+               if !ok || err != nil {
+                       t.Fatalf("Failed to update grouping policy in 
transaction: %v", err)
+               }
+               return nil
+       }); err != nil {
+               t.Fatalf("Failed to commit transaction: %v", err)
+       }
+
+       if hasGroupingPolicy(t, e, "alice", "admin") {
+               t.Fatal("The old grouping policy should have been removed")
+       }
+       if !hasGroupingPolicy(t, e, "alice", "user") {
+               t.Fatal("The new grouping policy should have been added")
+       }
+}
+
+// Test filtered removal within a transaction.
+func TestTransactionRemoveFilteredPolicy(t *testing.T) {
+       e := newTestTransactionalEnforcer(t)
+       ctx := context.Background()
+
+       if err := e.WithTransaction(ctx, func(tx *Transaction) error {
+               if _, err := tx.AddPolicies([][]string{
+                       {"alice", "data1", "read"},
+                       {"alice", "data2", "write"},
+                       {"bob", "data2", "write"},
+               }); err != nil {
+                       return err
+               }
+               _, err := tx.AddGroupingPolicies([][]string{{"alice", "admin"}, 
{"bob", "admin"}})
+               return err
+       }); err != nil {
+               t.Fatalf("Failed to commit transaction: %v", err)
+       }
+
+       // A filter that matches nothing is a no-op.
+       if err := e.WithTransaction(ctx, func(tx *Transaction) error {
+               ok, err := tx.RemoveFilteredPolicy(0, "carol")
+               if err != nil {
+                       return err
+               }
+               if ok || tx.HasOperations() {
+                       t.Fatal("A filter matching no rule should buffer 
nothing")
+               }
+               return nil
+       }); err != nil {
+               t.Fatalf("Failed to commit transaction: %v", err)
+       }
+
+       if err := e.WithTransaction(ctx, func(tx *Transaction) error {
+               ok, err := tx.RemoveFilteredPolicy(0, "alice")
+               if !ok || err != nil {
+                       t.Fatalf("Failed to remove filtered policy in 
transaction: %v", err)
+               }
+
+               // The removal is visible inside the transaction before 
committing.
+               bufferedModel, err := tx.GetBufferedModel()
+               if err != nil {
+                       return err
+               }
+               if has, _ := bufferedModel.HasPolicy("p", "p", 
[]string{"alice", "data1", "read"}); has {
+                       t.Fatal("The buffered model should no longer contain 
alice's policy")
+               }
+
+               // But not outside of it.
+               if !hasPolicy(t, e, "alice", "data1", "read") {
+                       t.Fatal("The enforcer model should still contain 
alice's policy before commit")
+               }
+
+               ok, err = tx.RemoveFilteredGroupingPolicy(1, "admin")
+               if !ok || err != nil {
+                       t.Fatalf("Failed to remove filtered grouping policy in 
transaction: %v", err)
+               }
+               return nil
+       }); err != nil {
+               t.Fatalf("Failed to commit transaction: %v", err)
+       }
+
+       if hasPolicy(t, e, "alice", "data1", "read") || hasPolicy(t, e, 
"alice", "data2", "write") {
+               t.Fatal("All of alice's policies should have been removed")
+       }
+       if !hasPolicy(t, e, "bob", "data2", "write") {
+               t.Fatal("bob's policy should have been kept")
+       }
+       if hasGroupingPolicy(t, e, "alice", "admin") || hasGroupingPolicy(t, e, 
"bob", "admin") {
+               t.Fatal("All grouping policies for admin should have been 
removed")
+       }
+}
+
+// Test that a rolled back filtered removal leaves the model untouched.
+func TestTransactionRemoveFilteredPolicyRollback(t *testing.T) {
+       e := newTestTransactionalEnforcer(t)
+       ctx := context.Background()
+
+       if err := e.WithTransaction(ctx, func(tx *Transaction) error {
+               _, err := tx.AddPolicy("alice", "data1", "read")
+               return err
+       }); err != nil {
+               t.Fatalf("Failed to commit transaction: %v", err)
+       }
+
+       wantErr := errors.New("business operation failed")
+       err := e.WithTransaction(ctx, func(tx *Transaction) error {
+               if _, err := tx.RemoveFilteredPolicy(0, "alice"); err != nil {
+                       return err
+               }
+               return wantErr
+       })
+       if !errors.Is(err, wantErr) {
+               t.Fatalf("Expected the business error to be returned, got %v", 
err)
+       }
+
+       if !hasPolicy(t, e, "alice", "data1", "read") {
+               t.Fatal("The policy should still exist after a rollback")
+       }
+}


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

Reply via email to