Files
upstream-casbin/transaction.go
upstream-mirror d8971e16fb
Some checks failed
Build / test (1.21) (push) Successful in 2m21s
Build / benchmark (1.21) (push) Failing after 2m28s
golangci-lint / golangci (push) Successful in 19m40s
snapshot of https://github.com/casbin/casbin.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:46:19 +05:00

486 lines
17 KiB
Go

// 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"
"fmt"
"sync"
"time"
"github.com/casbin/casbin/v3/model"
"github.com/casbin/casbin/v3/persist"
)
const (
// Default timeout duration for lock acquisition.
defaultLockTimeout = 30 * time.Second
)
// Transaction represents a Casbin transaction.
// It provides methods to perform policy operations within a transaction.
// and commit or rollback all changes atomically.
type Transaction struct {
id string // Unique transaction identifier.
enforcer *TransactionalEnforcer // Reference to the transactional enforcer.
buffer *TransactionBuffer // Buffer for policy operations.
txContext persist.TransactionContext // Database transaction context.
ctx context.Context // Context for the transaction.
baseVersion int64 // Model version at transaction start.
committed bool // Whether the transaction has been committed.
rolledBack bool // Whether the transaction has been rolled back.
startTime time.Time // Transaction start timestamp.
mutex sync.RWMutex // Protects transaction state.
}
// GetAdapter returns the adapter that operates within this transaction.
// Writes done through it join the same database transaction as the buffered
// policy changes, so non-policy operations can be made atomic together with the
// policy changes, e.g. with the GORM adapter:
//
// err := e.WithTransaction(ctx, func(tx *casbin.Transaction) error {
// db := tx.GetAdapter().(*gormadapter.Adapter).GetDb().
// Session(&gorm.Session{NewDB: true})
// if err := db.Where("name = ?", "admin").Delete(&Role{}).Error; err != nil {
// return err
// }
// _, err := tx.RemoveGroupingPolicy("alice", "admin")
// return err
// })
//
// Mind the Session(&gorm.Session{NewDB: true}): the GORM adapter pins its
// *gorm.DB to the policy table, and without resetting the statement every query
// would silently run against casbin_rule instead of the caller's own table.
func (tx *Transaction) GetAdapter() persist.Adapter {
return tx.txContext.GetAdapter()
}
// buildRuleFromParams converts parameters to a rule slice.
func (tx *Transaction) buildRuleFromParams(params ...interface{}) []string {
if len(params) == 1 {
if strSlice, ok := params[0].([]string); ok {
rule := make([]string, 0, len(strSlice))
rule = append(rule, strSlice...)
return rule
}
}
rule := make([]string, 0, len(params))
for _, param := range params {
rule = append(rule, param.(string))
}
return rule
}
// checkTransactionStatus checks if the transaction is active.
func (tx *Transaction) checkTransactionStatus() error {
if tx.committed || tx.rolledBack {
return errors.New("transaction is not active")
}
return nil
}
// 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 len(rules) == 0 {
if err := tx.checkTransactionStatus(); err != nil {
return false, err
}
return false, nil
}
bufferedModel, err := tx.getBufferedModel()
if 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
}
tx.buffer.AddOperation(persist.PolicyOperation{
Type: persist.OperationAdd,
Section: sec,
PolicyType: ptype,
Rules: validRules,
})
return true, nil
}
// 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 len(rules) == 0 {
if err := tx.checkTransactionStatus(); err != nil {
return false, err
}
return false, nil
}
bufferedModel, err := tx.getBufferedModel()
if 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
}
tx.buffer.AddOperation(persist.PolicyOperation{
Type: persist.OperationRemove,
Section: sec,
PolicyType: ptype,
Rules: validRules,
})
return true, nil
}
// 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()
bufferedModel, err := tx.getBufferedModel()
if err != nil {
return false, err
}
matched, err := bufferedModel.GetFilteredPolicy(sec, ptype, fieldIndex, fieldValues...)
if err != nil {
return false, err
}
if len(matched) == 0 {
return false, nil
}
// 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...))
}
tx.buffer.AddOperation(persist.PolicyOperation{
Type: persist.OperationRemove,
Section: sec,
PolicyType: ptype,
Rules: rules,
})
return true, nil
}
// 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 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(oldRules) == 0 {
if err := tx.checkTransactionStatus(); err != nil {
return false, err
}
return false, nil
}
bufferedModel, err := tx.getBufferedModel()
if err != nil {
return false, err
}
for i, oldRule := range oldRules {
hasOldPolicy, err := bufferedModel.HasPolicy(sec, ptype, oldRule)
if err != nil {
return false, err
}
if !hasOldPolicy {
return false, nil
}
hasNewPolicy, err := bufferedModel.HasPolicy(sec, ptype, newRules[i])
if err != nil {
return false, err
}
if hasNewPolicy {
return false, nil
}
}
tx.buffer.AddOperation(persist.PolicyOperation{
Type: persist.OperationUpdate,
Section: sec,
PolicyType: ptype,
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)
}
// UpdateNamedPolicy updates a named policy within the transaction.
func (tx *Transaction) UpdateNamedPolicy(ptype string, oldPolicy []string, newPolicy []string) (bool, error) {
return tx.bufferUpdate("p", ptype, [][]string{oldPolicy}, [][]string{newPolicy})
}
// 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)
}
// 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.
func (tx *Transaction) AddGroupingPolicy(params ...interface{}) (bool, error) {
return tx.AddNamedGroupingPolicy("g", params...)
}
// AddNamedGroupingPolicy adds a named grouping policy within the transaction.
func (tx *Transaction) AddNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) {
return tx.bufferAdd("g", ptype, [][]string{tx.buildRuleFromParams(params...)})
}
// AddGroupingPolicies adds multiple grouping policies within the transaction.
func (tx *Transaction) AddGroupingPolicies(rules [][]string) (bool, error) {
return tx.AddNamedGroupingPolicies("g", rules)
}
// 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.
func (tx *Transaction) RemoveGroupingPolicy(params ...interface{}) (bool, error) {
return tx.RemoveNamedGroupingPolicy("g", params...)
}
// RemoveNamedGroupingPolicy removes a named grouping policy within the transaction.
func (tx *Transaction) RemoveNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error) {
return tx.bufferRemove("g", ptype, [][]string{tx.buildRuleFromParams(params...)})
}
// RemoveGroupingPolicies removes multiple grouping policies within the transaction.
func (tx *Transaction) RemoveGroupingPolicies(rules [][]string) (bool, error) {
return tx.RemoveNamedGroupingPolicies("g", rules)
}
// 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)
}
// 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...)
}
// 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)
}
// UpdateGroupingPolicy updates a grouping policy within the transaction.
func (tx *Transaction) UpdateGroupingPolicy(oldRule []string, newRule []string) (bool, error) {
return tx.UpdateNamedGroupingPolicy("g", oldRule, newRule)
}
// 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.
// This is useful for preview or validation purposes within the transaction.
func (tx *Transaction) GetBufferedModel() (model.Model, error) {
tx.mutex.RLock()
defer tx.mutex.RUnlock()
return tx.getBufferedModel()
}
// HasOperations returns true if the transaction has any buffered operations.
func (tx *Transaction) HasOperations() bool {
tx.mutex.RLock()
defer tx.mutex.RUnlock()
return tx.buffer.HasOperations()
}
// OperationCount returns the number of buffered operations in the transaction.
func (tx *Transaction) OperationCount() int {
tx.mutex.RLock()
defer tx.mutex.RUnlock()
return tx.buffer.OperationCount()
}
// tryLockWithTimeout attempts to acquire the lock within the specified timeout period.
func tryLockWithTimeout(lock *sync.Mutex, startTime time.Time, maxWait time.Duration) bool {
// Calculate remaining wait time based on transaction start time.
remainingTime := maxWait - time.Since(startTime)
if remainingTime <= 0 {
return false
}
// Create a context with timeout for lock acquisition.
ctx, cancel := context.WithTimeout(context.Background(), remainingTime)
defer cancel()
// Use channel for timeout control.
done := make(chan bool, 1)
go func() {
lock.Lock()
done <- true
}()
// Wait for either lock acquisition or timeout.
select {
case <-done:
return true
case <-ctx.Done():
return false
}
}