From a8b5cce6d61a807cb6f4450e40278ef911368788 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:54:30 +0000 Subject: [PATCH 1/4] Initial plan From 0cca5f5d7b49c4ec2e8336447890d7957f37cd98 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:58:39 +0000 Subject: [PATCH 2/4] Add comprehensive transaction support documentation and examples Co-authored-by: nomeguy <85475922+nomeguy@users.noreply.github.com> --- README.md | 24 ++ TRANSACTION_GUIDE.md | 510 ++++++++++++++++++++++++++++++++ examples/transaction/README.md | 94 ++++++ examples/transaction/adapter.go | 94 ++++++ examples/transaction/go.mod | 7 + examples/transaction/go.sum | 6 + examples/transaction/main.go | 235 +++++++++++++++ 7 files changed, 970 insertions(+) create mode 100644 TRANSACTION_GUIDE.md create mode 100644 examples/transaction/README.md create mode 100644 examples/transaction/adapter.go create mode 100644 examples/transaction/go.mod create mode 100644 examples/transaction/go.sum create mode 100644 examples/transaction/main.go diff --git a/README.md b/README.md index 89dc9af8..2513db80 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Casbin is a powerful and efficient open-source access control library for Golang - [Get started](#get-started) - [Policy management](#policy-management) - [Policy persistence](#policy-persistence) +- [Transaction support](#transaction-support) - [Policy consistence between multiple nodes](#policy-consistence-between-multiple-nodes) - [Role manager](#role-manager) - [Benchmarks](#benchmarks) @@ -200,6 +201,29 @@ We also provide a [web-based UI](https://casbin.org/docs/admin-portal) for model https://casbin.org/docs/adapters +## Transaction support + +Casbin provides built-in support for transactional policy updates through the `TransactionalEnforcer`. This allows you to ensure atomic consistency between Casbin policy operations and business database operations. + +```go +// Create a transactional enforcer +enforcer, _ := casbin.NewTransactionalEnforcer("model.conf", transactionalAdapter) + +// Use transactions to ensure atomicity +err := enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + // Update business data in your database + db.UpdateUserRole(userId, "admin") + + // Update Casbin policies in the same transaction + tx.AddGroupingPolicy(userId, "admin") + tx.AddPolicy("admin", "resource", "write") + + return nil // Commits both changes atomically +}) +``` + +See [TRANSACTION_GUIDE.md](TRANSACTION_GUIDE.md) for comprehensive documentation and [examples/transaction](examples/transaction) for working examples. + ## Policy consistence between multiple nodes https://casbin.org/docs/watchers diff --git a/TRANSACTION_GUIDE.md b/TRANSACTION_GUIDE.md new file mode 100644 index 00000000..b8e04062 --- /dev/null +++ b/TRANSACTION_GUIDE.md @@ -0,0 +1,510 @@ +# Transaction Consistency Guide + +## Overview + +Casbin provides built-in support for transaction consistency between policy operations and business data through the `TransactionalEnforcer`. This guide explains how to ensure atomic updates when modifying both Casbin authorization policies and business database records. + +## The Problem + +In applications that update both business data and Casbin policies, there's a risk of data inconsistency if operations happen in separate transactions: + +```go +// ❌ NOT ATOMIC - Risk of inconsistency +db.UpdateUserRole(userId, "admin") // Business transaction +enforcer.AddGroupingPolicy(userId, "admin") // Separate Casbin operation +// If second operation fails, data is inconsistent! +``` + +## The Solution: TransactionalEnforcer + +Casbin's `TransactionalEnforcer` coordinates policy operations with database transactions to ensure atomicity. + +### Key Features + +- **Atomic Operations**: Policy changes and database updates happen atomically +- **Two-Phase Commit**: Ensures consistency between database and in-memory model +- **Conflict Detection**: Detects and prevents concurrent modification conflicts +- **Optimistic Locking**: Uses version numbers to detect concurrent changes +- **Rollback Support**: Automatically rolls back on failure + +## Quick Start + +### 1. Use a TransactionalAdapter + +Your adapter must implement the `persist.TransactionalAdapter` interface: + +```go +type TransactionalAdapter interface { + Adapter + BeginTransaction(ctx context.Context) (TransactionContext, error) +} +``` + +### 2. Create a TransactionalEnforcer + +```go +import "github.com/casbin/casbin/v3" + +// Create enforcer with a transactional adapter +enforcer, err := casbin.NewTransactionalEnforcer("model.conf", adapter) +if err != nil { + log.Fatal(err) +} +``` + +### 3. Use Transactions + +#### Option A: Using WithTransaction (Recommended) + +```go +err := enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + // Update business data + if err := db.UpdateUserRole(userId, "admin"); err != nil { + return err // Transaction will be rolled back + } + + // Update Casbin policy + if _, err := tx.AddGroupingPolicy(userId, "admin"); err != nil { + return err // Transaction will be rolled back + } + + return nil // Transaction will be committed +}) +``` + +#### Option B: Manual Transaction Management + +```go +// Begin transaction +tx, err := enforcer.BeginTransaction(ctx) +if err != nil { + return err +} + +// Add policy operations +ok, err := tx.AddGroupingPolicy("alice", "admin") +if err != nil { + tx.Rollback() + return err +} + +ok, err = tx.AddPolicy("admin", "data1", "write") +if err != nil { + tx.Rollback() + return err +} + +// Commit transaction +if err := tx.Commit(); err != nil { + return err +} +``` + +## Complete Example: User Role Management + +This example shows how to atomically update user roles in both the business database and Casbin: + +```go +package main + +import ( + "context" + "database/sql" + "fmt" + "log" + + "github.com/casbin/casbin/v3" + _ "github.com/lib/pq" +) + +type UserService struct { + db *sql.DB + enforcer *casbin.TransactionalEnforcer +} + +// UpdateUserRole atomically updates user role in database and Casbin +func (s *UserService) UpdateUserRole(ctx context.Context, userId, oldRole, newRole string) error { + return s.enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + // Get database transaction from adapter + // (This requires your adapter to provide access to the DB transaction) + + // Update user role in business database + _, err := s.db.ExecContext(ctx, + "UPDATE users SET role = $1 WHERE id = $2", + newRole, userId) + if err != nil { + return fmt.Errorf("failed to update user role: %w", err) + } + + // Remove old role mapping in Casbin + if oldRole != "" { + if _, err := tx.RemoveGroupingPolicy(userId, oldRole); err != nil { + return fmt.Errorf("failed to remove old role: %w", err) + } + } + + // Add new role mapping in Casbin + if _, err := tx.AddGroupingPolicy(userId, newRole); err != nil { + return fmt.Errorf("failed to add new role: %w", err) + } + + return nil + }) +} + +// CreateUser atomically creates a user with initial permissions +func (s *UserService) CreateUser(ctx context.Context, userId, role string, permissions [][]string) error { + return s.enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + // Insert user into database + _, err := s.db.ExecContext(ctx, + "INSERT INTO users (id, role) VALUES ($1, $2)", + userId, role) + if err != nil { + return fmt.Errorf("failed to create user: %w", err) + } + + // Assign role in Casbin + if _, err := tx.AddGroupingPolicy(userId, role); err != nil { + return fmt.Errorf("failed to assign role: %w", err) + } + + // Add initial permissions + for _, perm := range permissions { + if _, err := tx.AddPolicy(perm...); err != nil { + return fmt.Errorf("failed to add permission: %w", err) + } + } + + return nil + }) +} +``` + +## Transaction Operations + +The `Transaction` type supports all standard policy operations: + +### Policy Operations +- `AddPolicy(params ...interface{}) (bool, error)` +- `AddPolicies(rules [][]string) (bool, error)` +- `RemovePolicy(params ...interface{}) (bool, error)` +- `RemovePolicies(rules [][]string) (bool, error)` +- `UpdatePolicy(oldPolicy, newPolicy []string) (bool, error)` + +### Grouping Policy Operations +- `AddGroupingPolicy(params ...interface{}) (bool, error)` +- `RemoveGroupingPolicy(params ...interface{}) (bool, error)` + +### Named Operations +- `AddNamedPolicy(ptype string, params ...interface{}) (bool, error)` +- `AddNamedPolicies(ptype string, rules [][]string) (bool, error)` +- `RemoveNamedPolicy(ptype string, params ...interface{}) (bool, error)` +- `RemoveNamedPolicies(ptype string, rules [][]string) (bool, error)` +- `UpdateNamedPolicy(ptype string, oldPolicy, newPolicy []string) (bool, error)` +- `AddNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error)` +- `RemoveNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error)` + +### Transaction State +- `IsActive() bool` - Check if transaction is still active +- `IsCommitted() bool` - Check if transaction was committed +- `IsRolledBack() bool` - Check if transaction was rolled back +- `HasOperations() bool` - Check if transaction has buffered operations +- `OperationCount() int` - Get number of buffered operations +- `GetBufferedModel() (model.Model, error)` - Preview model state after operations + +## Implementing a TransactionalAdapter + +To use transactions, your adapter must implement the `persist.TransactionalAdapter` interface: + +```go +package myadapter + +import ( + "context" + "database/sql" + + "github.com/casbin/casbin/v3/persist" +) + +type MyAdapter struct { + db *sql.DB +} + +// BeginTransaction starts a database transaction +func (a *MyAdapter) BeginTransaction(ctx context.Context) (persist.TransactionContext, error) { + tx, err := a.db.BeginTx(ctx, nil) + if err != nil { + return nil, err + } + + return &MyTransactionContext{ + tx: tx, + adapter: a, + }, nil +} + +// MyTransactionContext wraps a database transaction +type MyTransactionContext struct { + tx *sql.Tx + adapter *MyAdapter +} + +func (tc *MyTransactionContext) Commit() error { + return tc.tx.Commit() +} + +func (tc *MyTransactionContext) Rollback() error { + return tc.tx.Rollback() +} + +func (tc *MyTransactionContext) GetAdapter() persist.Adapter { + // Return an adapter that uses this transaction + return &MyTransactionalAdapter{ + tx: tc.tx, + adapter: tc.adapter, + } +} + +// MyTransactionalAdapter is an adapter that operates within a transaction +type MyTransactionalAdapter struct { + tx *sql.Tx + adapter *MyAdapter +} + +func (a *MyTransactionalAdapter) AddPolicy(sec string, ptype string, rule []string) error { + // Use a.tx instead of a.adapter.db for queries + _, err := a.tx.Exec("INSERT INTO casbin_rule (...) VALUES (...)") + return err +} + +// Implement other Adapter methods using a.tx... +``` + +## Using with GORM Adapter + +If you're using the GORM adapter, here's how to ensure transaction support: + +```go +import ( + "github.com/casbin/gorm-adapter/v3" + "gorm.io/gorm" +) + +// Custom GORM adapter with transaction support +type GormTransactionalAdapter struct { + *gormadapter.Adapter + db *gorm.DB +} + +func NewGormTransactionalAdapter(db *gorm.DB) (*GormTransactionalAdapter, error) { + adapter, err := gormadapter.NewAdapterByDB(db) + if err != nil { + return nil, err + } + + return &GormTransactionalAdapter{ + Adapter: adapter, + db: db, + }, nil +} + +func (a *GormTransactionalAdapter) BeginTransaction(ctx context.Context) (persist.TransactionContext, error) { + tx := a.db.Begin() + if tx.Error != nil { + return nil, tx.Error + } + + // Create adapter for this transaction + txAdapter, err := gormadapter.NewAdapterByDB(tx) + if err != nil { + tx.Rollback() + return nil, err + } + + return &GormTransactionContext{ + tx: tx, + adapter: txAdapter, + }, nil +} + +type GormTransactionContext struct { + tx *gorm.DB + adapter *gormadapter.Adapter +} + +func (tc *GormTransactionContext) Commit() error { + return tc.tx.Commit().Error +} + +func (tc *GormTransactionContext) Rollback() error { + return tc.tx.Rollback().Error +} + +func (tc *GormTransactionContext) GetAdapter() persist.Adapter { + return tc.adapter +} + +// Usage +func main() { + db, _ := gorm.Open(...) + adapter, _ := NewGormTransactionalAdapter(db) + enforcer, _ := casbin.NewTransactionalEnforcer("model.conf", adapter) + + enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + // Your transactional operations + return nil + }) +} +``` + +## Best Practices + +### 1. Always Use WithTransaction for Automatic Cleanup + +```go +// ✅ Good - Automatic rollback on error or panic +err := enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + // Operations + return nil +}) + +// ❌ Avoid - Manual management is error-prone +tx, _ := enforcer.BeginTransaction(ctx) +// Easy to forget rollback on error +tx.Commit() +``` + +### 2. Keep Transactions Short + +```go +// ✅ Good - Quick transaction +err := enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + tx.AddPolicy("alice", "data1", "read") + tx.AddGroupingPolicy("alice", "admin") + return nil +}) + +// ❌ Avoid - Long-running operations in transaction +err := enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + // External API call - can be slow! + result := callExternalAPI() + tx.AddPolicy(result...) + return nil +}) +``` + +### 3. Handle Context Cancellation + +```go +err := enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + // Check context regularly in long operations + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + // Do work + return nil +}) +``` + +### 4. Use Buffered Model for Validation + +```go +err := enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + tx.AddPolicy("alice", "data1", "read") + tx.AddGroupingPolicy("alice", "admin") + + // Preview what the model will look like after commit + bufferedModel, err := tx.GetBufferedModel() + if err != nil { + return err + } + + // Validate before committing + hasPolicy, _ := bufferedModel.HasPolicy("p", "p", []string{"alice", "data1", "read"}) + if !hasPolicy { + return errors.New("policy not added correctly") + } + + return nil +}) +``` + +## Error Handling + +### Transaction Errors + +```go +err := enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + if _, err := tx.AddPolicy("alice", "data1", "read"); err != nil { + // Error is automatically rolled back + return fmt.Errorf("failed to add policy: %w", err) + } + return nil +}) + +if err != nil { + // Handle transaction failure + log.Printf("Transaction failed: %v", err) +} +``` + +### Conflict Detection + +```go +// Transaction detects concurrent modifications +tx1, _ := enforcer.BeginTransaction(ctx) +tx2, _ := enforcer.BeginTransaction(ctx) + +tx1.AddPolicy("alice", "data1", "read") +tx1.Commit() // Success + +tx2.AddPolicy("bob", "data2", "write") +err := tx2.Commit() // May fail with conflict error if policies overlap +``` + +## Performance Considerations + +1. **Batch Operations**: Use `AddPolicies` instead of multiple `AddPolicy` calls +2. **Transaction Duration**: Keep transactions as short as possible +3. **Database Locks**: Be aware of database locking behavior +4. **In-Memory Model**: Model updates happen after database commit + +## Limitations + +1. The adapter must implement `persist.TransactionalAdapter` +2. Transactions are not distributed - they only cover one database +3. In-memory model is updated after database commit (brief inconsistency window) + +## Frequently Asked Questions + +### Q: Can I use regular Enforcer methods during a transaction? + +No, you must use the Transaction methods. Regular enforcer operations are not part of the transaction. + +### Q: What happens if the adapter doesn't support transactions? + +`BeginTransaction()` will return an error: "adapter does not support transactions" + +### Q: Can I have multiple active transactions? + +Yes, but be aware of potential conflicts. The last transaction to commit may fail if it conflicts with earlier commits. + +### Q: How do I share the database transaction with my business code? + +Your adapter's `TransactionContext` should provide access to the underlying database transaction so business code can use the same transaction. + +## Related Resources + +- [Casbin Documentation](https://casbin.org/docs) +- [Adapter List](https://casbin.org/docs/adapters) +- [Policy Management API](https://casbin.org/docs/management-api) +- [GORM Adapter](https://github.com/casbin/gorm-adapter) + +## Support + +For questions or issues: +- GitHub Issues: https://github.com/casbin/casbin/issues +- Discord: https://discord.gg/S5UjpzGZjN diff --git a/examples/transaction/README.md b/examples/transaction/README.md new file mode 100644 index 00000000..87f0ea72 --- /dev/null +++ b/examples/transaction/README.md @@ -0,0 +1,94 @@ +# Casbin Transaction Example + +This example demonstrates how to use Casbin's `TransactionalEnforcer` to ensure atomic updates between business data and authorization policies. + +## Overview + +The TransactionalEnforcer provides transaction support for Casbin policy operations, allowing you to: + +- Update business data and Casbin policies atomically +- Ensure consistency between your database and authorization model +- Automatically rollback changes on errors +- Prevent concurrent modification conflicts + +## Running the Example + +```bash +cd examples/transaction +go run . +``` + +## What This Example Demonstrates + +1. **Basic Transaction Usage**: Using `WithTransaction` for automatic transaction management +2. **Automatic Rollback**: How transactions automatically rollback on errors +3. **User Role Management**: Real-world scenario of updating user roles atomically +4. **Batch Operations**: Efficiently adding multiple policies in one transaction + +## Code Structure + +- `main.go` - Example scenarios demonstrating transaction usage +- `adapter.go` - Mock adapter implementation for the examples + +## Key Concepts + +### Using WithTransaction (Recommended) + +```go +err := enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + // Add policies + tx.AddPolicy("alice", "data1", "read") + tx.AddGroupingPolicy("alice", "admin") + + // Any error will cause automatic rollback + if someCondition { + return errors.New("validation failed") + } + + return nil // Commits transaction +}) +``` + +### Manual Transaction Management + +```go +tx, err := enforcer.BeginTransaction(ctx) +if err != nil { + return err +} + +// Add operations +tx.AddPolicy("alice", "data1", "read") + +// Commit or rollback +if err := tx.Commit(); err != nil { + return err +} +``` + +## Using with Real Databases + +To use transactions with a real database, your adapter must implement the `persist.TransactionalAdapter` interface: + +```go +type TransactionalAdapter interface { + Adapter + BeginTransaction(ctx context.Context) (TransactionContext, error) +} +``` + +See the main [TRANSACTION_GUIDE.md](../../TRANSACTION_GUIDE.md) for complete implementation examples with GORM and other adapters. + +## Next Steps + +- Read the [Transaction Guide](../../TRANSACTION_GUIDE.md) for comprehensive documentation +- Implement `TransactionalAdapter` in your custom adapter +- Integrate transaction support into your application +- Check the [official Casbin documentation](https://casbin.org/docs) + +## Related Resources + +- [Casbin Documentation](https://casbin.org/docs) +- [Management API](https://casbin.org/docs/management-api) +- [RBAC API](https://casbin.org/docs/rbac-api) +- [Adapters](https://casbin.org/docs/adapters) diff --git a/examples/transaction/adapter.go b/examples/transaction/adapter.go new file mode 100644 index 00000000..f8cba5e5 --- /dev/null +++ b/examples/transaction/adapter.go @@ -0,0 +1,94 @@ +// 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 main + +import ( + "context" + "errors" + + "github.com/casbin/casbin/v3" + "github.com/casbin/casbin/v3/model" + "github.com/casbin/casbin/v3/persist" +) + +// MockTransactionalAdapter implements TransactionalAdapter interface for examples. +type MockTransactionalAdapter struct { + Enforcer *casbin.Enforcer +} + +// 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 +} + +// MockTransactionContext implements TransactionContext interface for examples. +type MockTransactionContext struct { + adapter *MockTransactionalAdapter + committed bool + rolledBack bool +} + +// 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 +} diff --git a/examples/transaction/go.mod b/examples/transaction/go.mod new file mode 100644 index 00000000..77a5117c --- /dev/null +++ b/examples/transaction/go.mod @@ -0,0 +1,7 @@ +module example.com/transaction + +go 1.13 + +replace github.com/casbin/casbin/v3 => ../.. + +require github.com/casbin/casbin/v3 v3.0.0 diff --git a/examples/transaction/go.sum b/examples/transaction/go.sum new file mode 100644 index 00000000..2f3a1c77 --- /dev/null +++ b/examples/transaction/go.sum @@ -0,0 +1,6 @@ +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/examples/transaction/main.go b/examples/transaction/main.go new file mode 100644 index 00000000..5bbc0751 --- /dev/null +++ b/examples/transaction/main.go @@ -0,0 +1,235 @@ +// 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 main demonstrates how to use Casbin's TransactionalEnforcer +// to ensure atomic updates between business data and authorization policies. +package main + +import ( + "context" + "fmt" + "log" + + "github.com/casbin/casbin/v3" +) + +// This example demonstrates how to use TransactionalEnforcer to ensure +// that business data updates and Casbin policy updates happen atomically. +func main() { + fmt.Println("Casbin Transaction Example") + fmt.Println("===========================") + fmt.Println() + + // Example 1: Basic transaction usage + basicTransactionExample() + + // Example 2: Transaction with rollback + transactionRollbackExample() + + // Example 3: Real-world scenario - User role management + userRoleManagementExample() + + // Example 4: Batch operations in transaction + batchOperationsExample() +} + +// basicTransactionExample shows the basic usage of transactions +func basicTransactionExample() { + fmt.Println("Example 1: Basic Transaction Usage") + fmt.Println("-----------------------------------") + + // Create a transactional enforcer with mock adapter + adapter := NewMockTransactionalAdapter() + enforcer, err := casbin.NewTransactionalEnforcer("../rbac_model.conf", adapter) + if err != nil { + log.Fatalf("Failed to create enforcer: %v", err) + } + adapter.Enforcer = enforcer.Enforcer + + ctx := context.Background() + + // Use WithTransaction for automatic transaction management + err = enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + fmt.Println(" Adding policies in transaction...") + + // Add multiple policies atomically + if _, err := tx.AddPolicy("alice", "data1", "read"); err != nil { + return err + } + + if _, err := tx.AddPolicy("bob", "data2", "write"); err != nil { + return err + } + + if _, err := tx.AddGroupingPolicy("alice", "admin"); err != nil { + return err + } + + fmt.Println(" Policies added successfully") + return nil + }) + + if err != nil { + log.Printf("Transaction failed: %v", err) + } else { + fmt.Println(" Transaction committed successfully") + } + + fmt.Println() +} + +// transactionRollbackExample demonstrates automatic rollback on error +func transactionRollbackExample() { + fmt.Println("Example 2: Transaction Rollback on Error") + fmt.Println("-----------------------------------------") + + adapter := NewMockTransactionalAdapter() + enforcer, err := casbin.NewTransactionalEnforcer("../rbac_model.conf", adapter) + if err != nil { + log.Fatalf("Failed to create enforcer: %v", err) + } + adapter.Enforcer = enforcer.Enforcer + + ctx := context.Background() + + err = enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + fmt.Println(" Adding first policy...") + if _, err := tx.AddPolicy("charlie", "data1", "read"); err != nil { + return err + } + + fmt.Println(" Simulating an error...") + // Simulate an error (e.g., business logic validation failure) + return fmt.Errorf("business validation failed") + }) + + if err != nil { + fmt.Printf(" Transaction rolled back: %v\n", err) + fmt.Println(" All changes were reverted") + } + + fmt.Println() +} + +// userRoleManagementExample shows a real-world scenario +func userRoleManagementExample() { + fmt.Println("Example 3: User Role Management") + fmt.Println("--------------------------------") + + adapter := NewMockTransactionalAdapter() + enforcer, err := casbin.NewTransactionalEnforcer("../rbac_model.conf", adapter) + if err != nil { + log.Fatalf("Failed to create enforcer: %v", err) + } + adapter.Enforcer = enforcer.Enforcer + + ctx := context.Background() + + // Simulate updating a user's role + // In a real application, this would also update the database + updateUserRole := func(userId, oldRole, newRole string) error { + return enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + fmt.Printf(" Updating %s from %s to %s...\n", userId, oldRole, newRole) + + // In real code, you would update the database here: + // _, err := db.ExecContext(ctx, "UPDATE users SET role = $1 WHERE id = $2", newRole, userId) + + // Remove old role + if oldRole != "" { + if _, err := tx.RemoveGroupingPolicy(userId, oldRole); err != nil { + return fmt.Errorf("failed to remove old role: %w", err) + } + } + + // Add new role + if _, err := tx.AddGroupingPolicy(userId, newRole); err != nil { + return fmt.Errorf("failed to add new role: %w", err) + } + + // Add role-specific permissions + switch newRole { + case "admin": + tx.AddPolicy("admin", "data1", "write") + tx.AddPolicy("admin", "data2", "write") + case "user": + tx.AddPolicy(userId, "data1", "read") + } + + fmt.Printf(" Successfully updated %s to %s\n", userId, newRole) + return nil + }) + } + + // Update a user's role + if err := updateUserRole("dave", "", "admin"); err != nil { + log.Printf("Failed to update user role: %v", err) + } + + fmt.Println() +} + +// batchOperationsExample demonstrates batch operations in transactions +func batchOperationsExample() { + fmt.Println("Example 4: Batch Operations in Transaction") + fmt.Println("-------------------------------------------") + + adapter := NewMockTransactionalAdapter() + enforcer, err := casbin.NewTransactionalEnforcer("../rbac_model.conf", adapter) + if err != nil { + log.Fatalf("Failed to create enforcer: %v", err) + } + adapter.Enforcer = enforcer.Enforcer + + ctx := context.Background() + + err = enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { + fmt.Println(" Adding batch policies...") + + // Add multiple policies in one operation (more efficient) + policies := [][]string{ + {"eve", "data1", "read"}, + {"eve", "data2", "read"}, + {"eve", "data3", "write"}, + } + + if _, err := tx.AddPolicies(policies); err != nil { + return fmt.Errorf("failed to add policies: %w", err) + } + + // Check buffered model state before commit + bufferedModel, err := tx.GetBufferedModel() + if err != nil { + return err + } + + // Validate that policies were added correctly + hasPolicy, _ := bufferedModel.HasPolicy("p", "p", []string{"eve", "data1", "read"}) + if !hasPolicy { + return fmt.Errorf("policy validation failed") + } + + fmt.Printf(" Successfully added %d policies\n", len(policies)) + fmt.Printf(" Transaction has %d operations\n", tx.OperationCount()) + + return nil + }) + + if err != nil { + log.Printf("Batch operation failed: %v", err) + } else { + fmt.Println(" Batch operation committed successfully") + } + + fmt.Println() +} From b0d9827fbd7d8f298ed673c298d65ca4b554204e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 14:59:19 +0000 Subject: [PATCH 3/4] Add .gitignore for transaction example and remove generated files Co-authored-by: nomeguy <85475922+nomeguy@users.noreply.github.com> --- examples/transaction/.gitignore | 3 +++ examples/transaction/go.mod | 7 ------- examples/transaction/go.sum | 6 ------ 3 files changed, 3 insertions(+), 13 deletions(-) create mode 100644 examples/transaction/.gitignore delete mode 100644 examples/transaction/go.mod delete mode 100644 examples/transaction/go.sum diff --git a/examples/transaction/.gitignore b/examples/transaction/.gitignore new file mode 100644 index 00000000..0a7ef9a0 --- /dev/null +++ b/examples/transaction/.gitignore @@ -0,0 +1,3 @@ +go.mod +go.sum +transaction diff --git a/examples/transaction/go.mod b/examples/transaction/go.mod deleted file mode 100644 index 77a5117c..00000000 --- a/examples/transaction/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module example.com/transaction - -go 1.13 - -replace github.com/casbin/casbin/v3 => ../.. - -require github.com/casbin/casbin/v3 v3.0.0 diff --git a/examples/transaction/go.sum b/examples/transaction/go.sum deleted file mode 100644 index 2f3a1c77..00000000 --- a/examples/transaction/go.sum +++ /dev/null @@ -1,6 +0,0 @@ -github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= -github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= -github.com/casbin/govaluate v1.3.0 h1:VA0eSY0M2lA86dYd5kPPuNZMUD9QkWnOCnavGrw9myc= -github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= From a9de447b52c369d3d727be69f6efa99d84c40afd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 1 Feb 2026 15:00:33 +0000 Subject: [PATCH 4/4] Address code review feedback: clarify examples and add error handling comments Co-authored-by: nomeguy <85475922+nomeguy@users.noreply.github.com> --- TRANSACTION_GUIDE.md | 35 ++++++++++++++++------------------- examples/transaction/main.go | 3 +++ 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/TRANSACTION_GUIDE.md b/TRANSACTION_GUIDE.md index b8e04062..41aa4554 100644 --- a/TRANSACTION_GUIDE.md +++ b/TRANSACTION_GUIDE.md @@ -109,32 +109,29 @@ package main import ( "context" - "database/sql" "fmt" "log" "github.com/casbin/casbin/v3" - _ "github.com/lib/pq" ) type UserService struct { - db *sql.DB enforcer *casbin.TransactionalEnforcer } // UpdateUserRole atomically updates user role in database and Casbin func (s *UserService) UpdateUserRole(ctx context.Context, userId, oldRole, newRole string) error { return s.enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { - // Get database transaction from adapter - // (This requires your adapter to provide access to the DB transaction) + // Note: In a real application, you would access the database transaction + // from your adapter to ensure both database and Casbin operations + // happen in the same transaction. See the GORM example below. - // Update user role in business database - _, err := s.db.ExecContext(ctx, - "UPDATE users SET role = $1 WHERE id = $2", - newRole, userId) - if err != nil { - return fmt.Errorf("failed to update user role: %w", err) - } + // Example (pseudo-code): + // dbTx := tx.GetDatabaseTransaction() // Adapter-specific + // _, err := dbTx.Exec("UPDATE users SET role = $1 WHERE id = $2", newRole, userId) + // if err != nil { + // return fmt.Errorf("failed to update user role: %w", err) + // } // Remove old role mapping in Casbin if oldRole != "" { @@ -155,13 +152,13 @@ func (s *UserService) UpdateUserRole(ctx context.Context, userId, oldRole, newRo // CreateUser atomically creates a user with initial permissions func (s *UserService) CreateUser(ctx context.Context, userId, role string, permissions [][]string) error { return s.enforcer.WithTransaction(ctx, func(tx *casbin.Transaction) error { - // Insert user into database - _, err := s.db.ExecContext(ctx, - "INSERT INTO users (id, role) VALUES ($1, $2)", - userId, role) - if err != nil { - return fmt.Errorf("failed to create user: %w", err) - } + // Note: In a real application, you would insert into your database here + // Example (pseudo-code): + // dbTx := tx.GetDatabaseTransaction() // Adapter-specific + // _, err := dbTx.Exec("INSERT INTO users (id, role) VALUES ($1, $2)", userId, role) + // if err != nil { + // return fmt.Errorf("failed to create user: %w", err) + // } // Assign role in Casbin if _, err := tx.AddGroupingPolicy(userId, role); err != nil { diff --git a/examples/transaction/main.go b/examples/transaction/main.go index 5bbc0751..d83ec953 100644 --- a/examples/transaction/main.go +++ b/examples/transaction/main.go @@ -158,6 +158,9 @@ func userRoleManagementExample() { } // Add role-specific permissions + // Note: In a real application, you would check these errors. + // For this example, we're showing the pattern and ignoring errors + // since the policies might already exist. switch newRole { case "admin": tx.AddPolicy("admin", "data1", "write")