// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. package casbin import ( "context" "errors" "testing" "github.com/casbin/casbin/v3/model" "github.com/casbin/casbin/v3/persist" ) // MockTransactionalAdapter implements TransactionalAdapter interface for testing. type MockTransactionalAdapter struct { Enforcer *Enforcer } // MockTransactionContext implements TransactionContext interface for testing. type MockTransactionContext struct { adapter *MockTransactionalAdapter committed bool rolledBack bool } // NewMockTransactionalAdapter creates a new mock adapter. func NewMockTransactionalAdapter() *MockTransactionalAdapter { return &MockTransactionalAdapter{} } // LoadPolicy implements Adapter interface. func (a *MockTransactionalAdapter) LoadPolicy(model model.Model) error { return nil } // SavePolicy implements Adapter interface. func (a *MockTransactionalAdapter) SavePolicy(model model.Model) error { return nil } // AddPolicy implements Adapter interface. func (a *MockTransactionalAdapter) AddPolicy(sec string, ptype string, rule []string) error { return nil } // RemovePolicy implements Adapter interface. func (a *MockTransactionalAdapter) RemovePolicy(sec string, ptype string, rule []string) error { return nil } // RemoveFilteredPolicy implements Adapter interface. func (a *MockTransactionalAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error { return nil } // BeginTransaction implements TransactionalAdapter interface. func (a *MockTransactionalAdapter) BeginTransaction(ctx context.Context) (persist.TransactionContext, error) { return &MockTransactionContext{adapter: a}, nil } // Commit implements TransactionContext interface. func (tx *MockTransactionContext) Commit() error { if tx.committed || tx.rolledBack { return errors.New("transaction already finished") } tx.committed = true return nil } // Rollback implements TransactionContext interface. func (tx *MockTransactionContext) Rollback() error { if tx.committed || tx.rolledBack { return errors.New("transaction already finished") } tx.rolledBack = true return nil } // GetAdapter implements TransactionContext interface. func (tx *MockTransactionContext) GetAdapter() persist.Adapter { return tx.adapter } // Test basic transaction functionality. func TestTransactionBasicOperations(t *testing.T) { 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 ctx := context.Background() // Begin transaction. tx, err := e.BeginTransaction(ctx) if err != nil { t.Fatalf("Failed to begin transaction: %v", err) } // Add policies in transaction. ok, err := tx.AddPolicy("alice", "data1", "read") if !ok || err != nil { t.Fatalf("Failed to add policy in transaction: %v", err) } ok, err = tx.AddPolicy("bob", "data2", "write") if !ok || err != nil { t.Fatalf("Failed to add policy in transaction: %v", err) } // Commit transaction. if err := tx.Commit(); err != nil { t.Fatalf("Failed to commit transaction: %v", err) } // Verify transaction was committed. if !tx.IsCommitted() { t.Error("Transaction should be committed") } } // Test transaction rollback. func TestTransactionRollback(t *testing.T) { 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 ctx := context.Background() // Begin transaction. tx, err := e.BeginTransaction(ctx) if err != nil { t.Fatalf("Failed to begin transaction: %v", err) } // Add policy in transaction. ok, err := tx.AddPolicy("alice", "data1", "read") if !ok || err != nil { t.Fatalf("Failed to add policy in transaction: %v", err) } // Rollback transaction. if err := tx.Rollback(); err != nil { t.Fatalf("Failed to rollback transaction: %v", err) } // Verify transaction was rolled back. if !tx.IsRolledBack() { t.Error("Transaction should be rolled back") } } // Test concurrent transactions. func TestConcurrentTransactions(t *testing.T) { 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 ctx := context.Background() // Start first transaction tx1, err := e.BeginTransaction(ctx) if err != nil { t.Fatalf("Failed to begin transaction 1: %v", err) } // Add policy in first transaction ok, err := tx1.AddPolicy("alice", "data1", "read") if !ok || err != nil { t.Fatalf("Failed to add policy in transaction 1: %v", err) } // Start second transaction tx2, err := e.BeginTransaction(ctx) if err != nil { t.Fatalf("Failed to begin transaction 2: %v", err) } // Add different policy in second transaction ok, err = tx2.AddPolicy("bob", "data2", "write") if !ok || err != nil { t.Fatalf("Failed to add policy in transaction 2: %v", err) } // Commit first transaction if err := tx1.Commit(); err != nil { t.Fatalf("Failed to commit transaction 1: %v", err) } // Commit second transaction if err := tx2.Commit(); err != nil { t.Fatalf("Failed to commit transaction 2: %v", err) } // Verify transactions were committed if !tx1.IsCommitted() { t.Error("Transaction 1 should be committed") } if !tx2.IsCommitted() { t.Error("Transaction 2 should be committed") } } // Test transaction conflicts. func TestTransactionConflicts(t *testing.T) { 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 ctx := context.Background() // Test Case 1: Two transactions commit t.Run("TwoTransactionsCommit", func(t *testing.T) { tx1, _ := e.BeginTransaction(ctx) tx2, _ := e.BeginTransaction(ctx) // Commit both transactions if err := tx1.Commit(); err != nil { t.Fatalf("Failed to commit tx1: %v", err) } if err := tx2.Commit(); err != nil { t.Fatalf("Failed to commit tx2: %v", err) } // Verify both transactions were committed if !tx1.IsCommitted() { t.Error("Transaction 1 should be committed") } if !tx2.IsCommitted() { t.Error("Transaction 2 should be committed") } }) // Test Case 2: Transaction rollback t.Run("TransactionRollback", func(t *testing.T) { tx, _ := e.BeginTransaction(ctx) // Rollback transaction if err := tx.Rollback(); err != nil { t.Fatalf("Failed to rollback transaction: %v", err) } // Verify transaction was rolled back if !tx.IsRolledBack() { t.Error("Transaction should be rolled back") } }) // Test Case 3: Cannot commit after rollback t.Run("NoCommitAfterRollback", func(t *testing.T) { tx, _ := e.BeginTransaction(ctx) // Rollback transaction if err := tx.Rollback(); err != nil { t.Fatalf("Failed to rollback transaction: %v", err) } // Try to commit if err := tx.Commit(); err == nil { t.Error("Should not be able to commit after rollback") } }) } // Test transaction buffer operations. func TestTransactionBuffer(t *testing.T) { 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 ctx := context.Background() tx, err := e.BeginTransaction(ctx) if err != nil { t.Fatalf("Failed to begin transaction: %v", err) } // Initially no operations. if tx.HasOperations() { t.Fatal("Transaction should have no operations initially") } if tx.OperationCount() != 0 { t.Fatal("Operation count should be 0 initially") } // Add some operations. tx.AddPolicy("alice", "data1", "read") tx.AddPolicy("bob", "data2", "write") if !tx.HasOperations() { t.Fatal("Transaction should have operations") } if tx.OperationCount() != 2 { t.Fatalf("Expected 2 operations, got %d", tx.OperationCount()) } // Get buffered model. bufferedModel, err := tx.GetBufferedModel() if err != nil { t.Fatalf("Failed to get buffered model: %v", err) } // Check that buffered model contains the policies. hasPolicy, _ := bufferedModel.HasPolicy("p", "p", []string{"alice", "data1", "read"}) if !hasPolicy { t.Fatal("Buffered model should contain the added policy") } 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") } }