486 lines
17 KiB
Go
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
|
|
}
|
|
}
|