Repository: casbin/casbin
Pull request #1343 (2023-11-23 10:01:09)

feat: support all types for JSON request in ABAC
fix: #1342 

casbin-bot:  @tangyang9464 @JalinWang please review

Patch:
diff --git a/enforcer.go b/enforcer.go
index 7fe73f4..c622549 100644
--- a/enforcer.go
+++ b/enforcer.go
@@ -17,7 +17,6 @@ package casbin
 import (
 	"errors"
 	"fmt"
-	"regexp"
 	"runtime/debug"
 	"strings"
 	"sync"
@@ -32,7 +31,6 @@ import (
 	"github.com/casbin/casbin/v2/util"
 
 	"github.com/casbin/govaluate"
-	"github.com/tidwall/gjson"
 )
 
 // Enforcer is the main interface for authorization enforcement and policy management.
@@ -637,7 +635,17 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac
 	}
 
 	if e.acceptJsonRequest {
-		expString = requestJsonReplace(expString, rTokens, rvals)
+		// try to parse all request values from json to map[string]interface{}
+		// skip if there is an error
+		for i, rval := range rvals {
+			switch rval := rval.(type) {
+			case string:
+				mapValue, err := util.JsonToMap(rval)
+				if err == nil {
+					rvals[i] = mapValue
+				}
+			}
+		}
 	}
 
 	parameters := enforceParameters{
@@ -685,16 +693,7 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac
 					pvals)
 			}
 
-			if e.acceptJsonRequest {
-				pvalsCopy := make([]string, len(pvals))
-				copy(pvalsCopy, pvals)
-				for i, pStr := range pvalsCopy {
-					pvalsCopy[i] = requestJsonReplace(util.EscapeAssertion(pStr), rTokens, rvals)
-				}
-				parameters.pVals = pvalsCopy
-			} else {
-				parameters.pVals = pvals
-			}
+			parameters.pVals = pvals
 
 			result, err := expression.Eval(parameters)
 			// log.LogPrint("Result: ", result)
@@ -796,31 +795,6 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac
 	return result, nil
 }
 
-var requestObjectRegex = regexp.MustCompile(`r[_.][A-Za-z_0-9]+\.[A-Za-z_0-9.]+[A-Za-z_0-9]`)
-var requestObjectRegexPrefix = regexp.MustCompile(`r[_.][A-Za-z_0-9]+\.`)
-
-// requestJsonReplace used to support request parameters of type json
-// It will replace the access of the request object in matchers or policy with the actual value in the request json parameter
-// For example: request sub = `{"Owner": "alice", "Age": 30}`
-// policy: p, r.sub.Age > 18, /data1, read  ==>  p, 30 > 18, /data1, read
-// matchers: m = r.sub == r.obj.Owner  ==>  m = r.sub == "alice"
-func requestJsonReplace(str string, rTokens map[string]int, rvals []interface{}) string {
-	matches := requestObjectRegex.FindAllString(str, -1)
-	for _, matchesStr := range matches {
-		prefix := requestObjectRegexPrefix.FindString(matchesStr)
-		jsonPath := strings.TrimPrefix(matchesStr, prefix)
-		tokenIndex := rTokens[prefix[:len(prefix)-1]]
-		if jsonStr, ok := rvals[tokenIndex].(string); ok {
-			newStr := gjson.Get(jsonStr, jsonPath).String()
-			if !util.IsNumeric(newStr) {
-				newStr = `"` + newStr + `"`
-			}
-			str = strings.Replace(str, matchesStr, newStr, -1)
-		}
-	}
-	return str
-}
-
 func (e *Enforcer) getAndStoreMatcherExpression(hasEval bool, expString string, functions map[string]govaluate.ExpressionFunction) (*govaluate.EvaluableExpression, error) {
 	var expression *govaluate.EvaluableExpression
 	var err error
diff --git a/go.mod b/go.mod
index 77e9498..0496154 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,6 @@ module github.com/casbin/casbin/v2
 require (
 	github.com/casbin/govaluate v1.1.0
 	github.com/golang/mock v1.4.4
-	github.com/tidwall/gjson v1.14.4
 )
 
 go 1.13
diff --git a/util/util.go b/util/util.go
index e89e5eb..d91394e 100644
--- a/util/util.go
+++ b/util/util.go
@@ -15,6 +15,7 @@
 package util
 
 import (
+	"encoding/json"
 	"regexp"
 	"sort"
 	"strings"
@@ -25,10 +26,13 @@ var evalReg = regexp.MustCompile(`\beval\((?P<rule>[^)]*)\)`)
 
 var escapeAssertionRegex = regexp.MustCompile(`\b((r|p)[0-9]*)\.`)
 
-var numericRegex = regexp.MustCompile(`^-?\d+(?:\.\d+)?$`)
-
-func IsNumeric(s string) bool {
-	return numericRegex.MatchString(s)
+func JsonToMap(jsonStr string) (map[string]interface{}, error) {
+	result := make(map[string]interface{})
+	err := json.Unmarshal([]byte(jsonStr), &result)
+	if err != nil {
+		return result, err
+	}
+	return result, nil
 }
 
 // EscapeAssertion escapes the dots in the assertion, because the expression evaluation doesn't support such variable names.


Test patch:
diff --git a/model_test.go b/model_test.go
index 8c5264f..ab955b9 100644
--- a/model_test.go
+++ b/model_test.go
@@ -15,6 +15,7 @@
 package casbin
 
 import (
+	"encoding/json"
 	"fmt"
 	"testing"
 
@@ -474,6 +475,37 @@ func TestABACMapRequest(t *testing.T) {
 	testEnforce(t, e, "bob", data2, "write", true)
 }
 
+func TestABACTypes(t *testing.T) {
+	e, _ := NewEnforcer("examples/abac_model.conf")
+	matcher := `"moderator" IN r.sub.Roles && r.sub.Enabled == true && r.sub.Age >= 21 && r.sub.Name != "foo"`
+	e.GetModel()["m"]["m"].Value = util.RemoveComments(util.EscapeAssertion(matcher))
+
+	structRequest := struct {
+		Roles   []interface{}
+		Enabled bool
+		Age     int
+		Name    string
+	}{
+		Roles:   []interface{}{"user", "moderator"},
+		Enabled: true,
+		Age:     30,
+		Name:    "alice",
+	}
+	testEnforce(t, e, structRequest, "", "", true)
+
+	mapRequest := map[string]interface{}{
+		"Roles":   []interface{}{"user", "moderator"},
+		"Enabled": true,
+		"Age":     30,
+		"Name":    "alice",
+	}
+	testEnforce(t, e, mapRequest, nil, "", true)
+
+	e.EnableAcceptJsonRequest(true)
+	jsonRequest, _ := json.Marshal(mapRequest)
+	testEnforce(t, e, string(jsonRequest), "", "", true)
+}
+
 func TestABACJsonRequest(t *testing.T) {
 	e, _ := NewEnforcer("examples/abac_model.conf")
 	e.EnableAcceptJsonRequest(true)