Skip to content
Open
77 changes: 64 additions & 13 deletions enforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import (
defaultrolemanager "github.com/casbin/casbin/v3/rbac/default-role-manager"
"github.com/casbin/casbin/v3/util"

"github.com/casbin/govaluate"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
)

// Enforcer is the main interface for authorization enforcement and policy management.
Expand Down Expand Up @@ -737,6 +738,8 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac
// For custom matchers provided at runtime, escape backslashes in string literals
expString = util.EscapeStringLiterals(util.RemoveComments(util.EscapeAssertion(matcher)))
}
// Convert govaluate IN operator syntax to expr syntax
expString = util.ConvertInOperatorSyntax(expString)

rTokens := make(map[string]int, len(e.model["r"][rType].Tokens))
for i, token := range e.model["r"][rType].Tokens {
Expand Down Expand Up @@ -777,8 +780,8 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac
if hasEval {
functions["eval"] = generateEvalFunction(functions, &parameters)
}
var expression *govaluate.EvaluableExpression
expression, err = e.getAndStoreMatcherExpression(hasEval, expString, functions)
var expression *vm.Program
expression, err = e.getAndStoreMatcherExpression(hasEval, expString, functions, rTokens, pTokens)
if err != nil {
return false, err
}
Expand Down Expand Up @@ -813,7 +816,13 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac

parameters.pVals = pvals

result, err := expression.Eval(parameters)
// Create environment with functions and parameters
env := parameters.ToMap()
for k, v := range functions {
env[k] = v
}

result, err := expr.Run(expression, env)
// log.LogPrint("Result: ", result)

if err != nil {
Expand Down Expand Up @@ -871,7 +880,13 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac

parameters.pVals = make([]string, len(parameters.pTokens))

result, err := expression.Eval(parameters)
// Create environment with functions and parameters
env := parameters.ToMap()
for k, v := range functions {
env[k] = v
}

result, err := expr.Run(expression, env)

if err != nil {
return false, err
Expand Down Expand Up @@ -904,15 +919,21 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac
return result, nil
}

func (e *Enforcer) getAndStoreMatcherExpression(hasEval bool, expString string, functions map[string]govaluate.ExpressionFunction) (*govaluate.EvaluableExpression, error) {
var expression *govaluate.EvaluableExpression
func (e *Enforcer) getAndStoreMatcherExpression(hasEval bool, expString string, functions map[string]interface{}, rTokens, pTokens map[string]int) (*vm.Program, error) {
var expression *vm.Program
var err error
var cachedExpression, isPresent = e.matcherMap.Load(expString)

if !hasEval && isPresent {
expression = cachedExpression.(*govaluate.EvaluableExpression)
expression = cachedExpression.(*vm.Program)
} else {
expression, err = govaluate.NewEvaluableExpressionWithFunctions(expString, functions)
// Create environment with functions
env := make(map[string]interface{})
for k, v := range functions {
env[k] = v
}
// Compile with AllowUndefinedVariables to support ABAC and dynamic parameter access
expression, err = expr.Compile(expString, expr.Env(env), expr.AllowUndefinedVariables())
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -1041,7 +1062,24 @@ type enforceParameters struct {
pVals []string
}

// implements govaluate.Parameters.
// ToMap converts enforceParameters to a map suitable for expr evaluation.
func (p enforceParameters) ToMap() map[string]interface{} {
env := make(map[string]interface{})

// Add r parameters
for token, index := range p.rTokens {
env[token] = p.rVals[index]
}

// Add p parameters
for token, index := range p.pTokens {
env[token] = p.pVals[index]
}

return env
}

// Get implements parameter access for backward compatibility.
func (p enforceParameters) Get(name string) (interface{}, error) {
if name == "" {
return nil, nil
Expand All @@ -1065,7 +1103,7 @@ func (p enforceParameters) Get(name string) (interface{}, error) {
}
}

func generateEvalFunction(functions map[string]govaluate.ExpressionFunction, parameters *enforceParameters) govaluate.ExpressionFunction {
func generateEvalFunction(functions map[string]interface{}, parameters *enforceParameters) func(args ...interface{}) (interface{}, error) {
return func(args ...interface{}) (interface{}, error) {
if len(args) != 1 {
return nil, fmt.Errorf("function eval(subrule string) expected %d arguments, but got %d", 1, len(args))
Expand All @@ -1076,10 +1114,23 @@ func generateEvalFunction(functions map[string]govaluate.ExpressionFunction, par
return nil, errors.New("argument of eval(subrule string) must be a string")
}
expression = util.EscapeAssertion(expression)
expr, err := govaluate.NewEvaluableExpressionWithFunctions(expression, functions)
// Convert IN operator syntax for compatibility
expression = util.ConvertInOperatorSyntax(expression)
// Create environment with functions for compilation
env := make(map[string]interface{})
for k, v := range functions {
env[k] = v
}
// Compile with AllowUndefinedVariables to support dynamic parameter access
program, err := expr.Compile(expression, expr.Env(env), expr.AllowUndefinedVariables())
if err != nil {
return nil, fmt.Errorf("error while parsing eval parameter: %s, %s", expression, err.Error())
}
return expr.Eval(parameters)
// Create environment with parameters and functions for evaluation
evalEnv := parameters.ToMap()
for k, v := range functions {
evalEnv[k] = v
}
return expr.Run(program, evalEnv)
}
}
3 changes: 1 addition & 2 deletions enforcer_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"github.com/casbin/casbin/v3/model"
"github.com/casbin/casbin/v3/persist"
"github.com/casbin/casbin/v3/rbac"
"github.com/casbin/govaluate"
)

var _ IEnforcer = &Enforcer{}
Expand Down Expand Up @@ -138,7 +137,7 @@ type IEnforcer interface {
RemoveNamedGroupingPolicy(ptype string, params ...interface{}) (bool, error)
RemoveNamedGroupingPolicies(ptype string, rules [][]string) (bool, error)
RemoveFilteredNamedGroupingPolicy(ptype string, fieldIndex int, fieldValues ...string) (bool, error)
AddFunction(name string, function govaluate.ExpressionFunction)
AddFunction(name string, function interface{})

UpdatePolicy(oldPolicy []string, newPolicy []string) (bool, error)
UpdatePolicies(oldPolicies [][]string, newPolicies [][]string) (bool, error)
Expand Down
4 changes: 1 addition & 3 deletions enforcer_synced.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import (
"sync/atomic"
"time"

"github.com/casbin/govaluate"

"github.com/casbin/casbin/v3/persist"
"github.com/casbin/casbin/v3/rbac"
)
Expand Down Expand Up @@ -631,7 +629,7 @@ func (e *SyncedEnforcer) RemoveFilteredNamedGroupingPolicy(ptype string, fieldIn
}

// AddFunction adds a customized function.
func (e *SyncedEnforcer) AddFunction(name string, function govaluate.ExpressionFunction) {
func (e *SyncedEnforcer) AddFunction(name string, function interface{}) {
e.m.Lock()
defer e.m.Unlock()
e.Enforcer.AddFunction(name, function)
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module github.com/casbin/casbin/v3

require (
github.com/bmatcuk/doublestar/v4 v4.6.1
github.com/casbin/govaluate v1.3.0
github.com/expr-lang/expr v1.17.7
github.com/google/uuid v1.6.0
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +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/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
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=
26 changes: 21 additions & 5 deletions management_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import (

"github.com/casbin/casbin/v3/constant"
"github.com/casbin/casbin/v3/util"
"github.com/casbin/govaluate"

"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
)

// GetAllSubjects gets the list of subjects that show up in the current policy.
Expand Down Expand Up @@ -159,10 +161,18 @@ func (e *Enforcer) GetFilteredNamedPolicyWithMatcher(ptype string, matcher strin
} else {
expString = util.RemoveComments(util.EscapeAssertion(matcher))
}
// Convert govaluate IN operator syntax to expr syntax
expString = util.ConvertInOperatorSyntax(expString)

var expression *govaluate.EvaluableExpression
var expression *vm.Program

expression, err = govaluate.NewEvaluableExpressionWithFunctions(expString, functions)
// Create environment with functions
env := make(map[string]interface{})
for k, v := range functions {
env[k] = v
}
// Compile with AllowUndefinedVariables to support dynamic parameter access
expression, err = expr.Compile(expString, expr.Env(env), expr.AllowUndefinedVariables())
if err != nil {
return res, err
}
Expand All @@ -188,7 +198,13 @@ func (e *Enforcer) GetFilteredNamedPolicyWithMatcher(ptype string, matcher strin

parameters.pVals = pvals

result, err := expression.Eval(parameters)
// Create environment with functions and parameters
evalEnv := parameters.ToMap()
for k, v := range functions {
evalEnv[k] = v
}

result, err := expr.Run(expression, evalEnv)

if err != nil {
return res, err
Expand Down Expand Up @@ -480,7 +496,7 @@ func (e *Enforcer) RemoveFilteredNamedGroupingPolicy(ptype string, fieldIndex in
}

// AddFunction adds a customized function.
func (e *Enforcer) AddFunction(name string, function govaluate.ExpressionFunction) {
func (e *Enforcer) AddFunction(name string, function interface{}) {
e.fm.AddFunction(name, function)
}

Expand Down
11 changes: 4 additions & 7 deletions model/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,15 @@ import (
"sync"

"github.com/casbin/casbin/v3/util"
"github.com/casbin/govaluate"
)

// FunctionMap represents the collection of Function.
type FunctionMap struct {
fns *sync.Map
}

// [string]govaluate.ExpressionFunction

// AddFunction adds an expression function.
func (fm *FunctionMap) AddFunction(name string, function govaluate.ExpressionFunction) {
func (fm *FunctionMap) AddFunction(name string, function interface{}) {
fm.fns.LoadOrStore(name, function)
}

Expand All @@ -54,11 +51,11 @@ func LoadFunctionMap() FunctionMap {
}

// GetFunctions return a map with all the functions.
func (fm *FunctionMap) GetFunctions() map[string]govaluate.ExpressionFunction {
ret := make(map[string]govaluate.ExpressionFunction)
func (fm *FunctionMap) GetFunctions() map[string]interface{} {
ret := make(map[string]interface{})

fm.fns.Range(func(k interface{}, v interface{}) bool {
ret[k.(string)] = v.(govaluate.ExpressionFunction)
ret[k.(string)] = v
return true
})

Expand Down
10 changes: 4 additions & 6 deletions util/builtin_operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ import (
"github.com/bmatcuk/doublestar/v4"

"github.com/casbin/casbin/v3/rbac"

"github.com/casbin/govaluate"
)

var (
Expand Down Expand Up @@ -402,10 +400,10 @@ func GlobMatchFunc(args ...interface{}) (interface{}, error) {
}

// GenerateGFunction is the factory method of the g(_, _[, _]) function.
func GenerateGFunction(rm rbac.RoleManager) govaluate.ExpressionFunction {
func GenerateGFunction(rm rbac.RoleManager) func(args ...interface{}) (interface{}, error) {
memorized := sync.Map{}
return func(args ...interface{}) (interface{}, error) {
// Like all our other govaluate functions, all args are strings.
// Like all our other expression functions, all args are strings.

// Allocate and generate a cache key from the arguments...
total := len(args)
Expand Down Expand Up @@ -445,9 +443,9 @@ func GenerateGFunction(rm rbac.RoleManager) govaluate.ExpressionFunction {
}

// GenerateConditionalGFunction is the factory method of the g(_, _[, _]) function with conditions.
func GenerateConditionalGFunction(crm rbac.ConditionalRoleManager) govaluate.ExpressionFunction {
func GenerateConditionalGFunction(crm rbac.ConditionalRoleManager) func(args ...interface{}) (interface{}, error) {
return func(args ...interface{}) (interface{}, error) {
// Like all our other govaluate functions, all args are strings.
// Like all our other expression functions, all args are strings.
var hasLink bool

name1, name2 := args[0].(string), args[1].(string)
Expand Down
16 changes: 16 additions & 0 deletions util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,22 @@ func EscapeStringLiterals(expr string) string {
return result.String()
}

// ConvertInOperatorSyntax converts govaluate's IN operator syntax to expr's syntax.
// Changes: `x in ('a', 'b')` to `x in ['a', 'b']`
// Also handles: `x IN ('a', 'b')` (case insensitive).
// And: `x IN array` to `x in array`.
func ConvertInOperatorSyntax(expression string) string {
// First, replace all IN/In/iN with lowercase 'in' (case insensitive)
// Use word boundaries to avoid replacing IN in the middle of identifiers.
reCase := regexp.MustCompile(`(?i)\bIN\b`)
expression = reCase.ReplaceAllString(expression, "in")

// Then, replace `in (...)` with `in [...]`
// This handles simple cases but may not work with deeply nested parentheses.
re := regexp.MustCompile(`\bin\s*\(([^)]+)\)`)
return re.ReplaceAllString(expression, "in [$1]")
}

func RemoveDuplicateElement(s []string) []string {
result := make([]string, 0, len(s))
temp := map[string]struct{}{}
Expand Down
Loading