diff --git a/enforcer.go b/enforcer.go index ff8c5431f..f82a2c5d7 100644 --- a/enforcer.go +++ b/enforcer.go @@ -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. @@ -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 { @@ -777,8 +780,8 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac if hasEval { functions["eval"] = generateEvalFunction(functions, ¶meters) } - 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 } @@ -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 { @@ -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 @@ -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 } @@ -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 @@ -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)) @@ -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) } } diff --git a/enforcer_interface.go b/enforcer_interface.go index 733653187..cea7dab49 100644 --- a/enforcer_interface.go +++ b/enforcer_interface.go @@ -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{} @@ -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) diff --git a/enforcer_synced.go b/enforcer_synced.go index 89bbe5dae..6263c3f58 100644 --- a/enforcer_synced.go +++ b/enforcer_synced.go @@ -19,8 +19,6 @@ import ( "sync/atomic" "time" - "github.com/casbin/govaluate" - "github.com/casbin/casbin/v3/persist" "github.com/casbin/casbin/v3/rbac" ) @@ -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) diff --git a/go.mod b/go.mod index c46f727d3..331183102 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index 2f3a1c775..3c2d6b86f 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/management_api.go b/management_api.go index 7a8f768ee..fc47c44f6 100644 --- a/management_api.go +++ b/management_api.go @@ -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. @@ -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 } @@ -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 @@ -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) } diff --git a/model/function.go b/model/function.go index 956c94b9d..a1db70f42 100644 --- a/model/function.go +++ b/model/function.go @@ -18,7 +18,6 @@ import ( "sync" "github.com/casbin/casbin/v3/util" - "github.com/casbin/govaluate" ) // FunctionMap represents the collection of Function. @@ -26,10 +25,8 @@ 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) } @@ -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 }) diff --git a/util/builtin_operators.go b/util/builtin_operators.go index 37d9cb5bf..6a99cf774 100644 --- a/util/builtin_operators.go +++ b/util/builtin_operators.go @@ -26,8 +26,6 @@ import ( "github.com/bmatcuk/doublestar/v4" "github.com/casbin/casbin/v3/rbac" - - "github.com/casbin/govaluate" ) var ( @@ -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) @@ -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) diff --git a/util/util.go b/util/util.go index b72823a4c..0387d36ea 100644 --- a/util/util.go +++ b/util/util.go @@ -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{}{}