Repository: casbin/casbin
Pull request #1277 (2023-07-16 10:24:54)

feat: support json request for ABAC
fix: https://github.com/casbin/casbin/issues/1272

casbin-bot:  @tangyang9464 @JalinWang please review
hsluoyz: @nengc plz review

Patch:
diff --git a/enforcer.go b/enforcer.go
index bae4d27..86167e9 100644
--- a/enforcer.go
+++ b/enforcer.go
@@ -17,11 +17,11 @@ package casbin
 import (
 	"errors"
 	"fmt"
+	"regexp"
 	"runtime/debug"
 	"strings"
 	"sync"
 
-	"github.com/Knetic/govaluate"
 	"github.com/casbin/casbin/v2/effector"
 	"github.com/casbin/casbin/v2/log"
 	"github.com/casbin/casbin/v2/model"
@@ -30,6 +30,9 @@ import (
 	"github.com/casbin/casbin/v2/rbac"
 	defaultrolemanager "github.com/casbin/casbin/v2/rbac/default-role-manager"
 	"github.com/casbin/casbin/v2/util"
+
+	"github.com/Knetic/govaluate"
+	"github.com/tidwall/gjson"
 )
 
 // Enforcer is the main interface for authorization enforcement and policy management.
@@ -50,6 +53,7 @@ type Enforcer struct {
 	autoBuildRoleLinks   bool
 	autoNotifyWatcher    bool
 	autoNotifyDispatcher bool
+	acceptJsonRequest    bool
 
 	logger log.Logger
 }
@@ -476,6 +480,11 @@ func (e *Enforcer) EnableAutoBuildRoleLinks(autoBuildRoleLinks bool) {
 	e.autoBuildRoleLinks = autoBuildRoleLinks
 }
 
+// EnableAcceptJsonRequest controls whether to accept json as a request parameter
+func (e *Enforcer) EnableAcceptJsonRequest(acceptJsonRequest bool) {
+	e.acceptJsonRequest = acceptJsonRequest
+}
+
 // BuildRoleLinks manually rebuild the role inheritance relations.
 func (e *Enforcer) BuildRoleLinks() error {
 	for _, rm := range e.rmMap {
@@ -564,6 +573,10 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac
 		pTokens[token] = i
 	}
 
+	if e.acceptJsonRequest {
+		expString = requestJsonReplace(expString, rTokens, rvals)
+	}
+
 	parameters := enforceParameters{
 		rTokens: rTokens,
 		rVals:   rvals,
@@ -609,7 +622,16 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac
 					pvals)
 			}
 
-			parameters.pVals = pvals
+			if e.acceptJsonRequest {
+				pvalsCopy := make([]string, len(pvals))
+				copy(pvalsCopy, pvals)
+				for i, pStr := range pvalsCopy {
+					pvalsCopy[i] = requestJsonReplace(pStr, rTokens, rvals)
+				}
+				parameters.pVals = pvalsCopy
+			} else {
+				parameters.pVals = pvals
+			}
 
 			result, err := expression.Eval(parameters)
 			// log.LogPrint("Result: ", result)
@@ -646,9 +668,9 @@ func (e *Enforcer) enforce(matcher string, explains *[]string, rvals ...interfac
 				policyEffects[policyIndex] = effector.Allow
 			}
 
-			//if e.model["e"]["e"].Value == "priority(p_eft) || deny" {
+			// if e.model["e"]["e"].Value == "priority(p_eft) || deny" {
 			//	break
-			//}
+			// }
 
 			effect, explainIndex, err = e.eft.MergeEffects(e.model["e"][eType].Value, policyEffects, matcherResults, policyIndex, policyLen)
 			if err != nil {
@@ -711,6 +733,31 @@ 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.FindStringSubmatch(str)
+	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 62fe536..a766c6d 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/casbin/casbin/v2
 require (
 	github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible
 	github.com/golang/mock v1.4.4
+	github.com/tidwall/gjson v1.14.4
 )
 
 go 1.13
diff --git a/go.sum b/go.sum
index c88d82f..f747ee0 100644
--- a/go.sum
+++ b/go.sum
@@ -2,9 +2,15 @@ github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1
 github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
 github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
 github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
+github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
\ No newline at end of file
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
diff --git a/util/util.go b/util/util.go
index 88f4345..e89e5eb 100644
--- a/util/util.go
+++ b/util/util.go
@@ -25,6 +25,12 @@ 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)
+}
+
 // EscapeAssertion escapes the dots in the assertion, because the expression evaluation doesn't support such variable names.
 func EscapeAssertion(s string) string {
 	s = escapeAssertionRegex.ReplaceAllStringFunc(s, func(m string) string {


Test patch:
diff --git a/model_test.go b/model_test.go
index f2c3437..8ff2903 100644
--- a/model_test.go
+++ b/model_test.go
@@ -414,6 +414,50 @@ func TestABACModel(t *testing.T) {
 	testEnforce(t, e, "bob", data2, "write", true)
 }
 
+func TestABACJsonRequest(t *testing.T) {
+	e, _ := NewEnforcer("examples/abac_model.conf")
+	e.EnableAcceptJsonRequest(true)
+
+	data1Json := `{ "Name": "data1", "Owner": "alice"}`
+	data2Json := `{ "Name": "data2", "Owner": "bob"}`
+
+	testEnforce(t, e, "alice", data1Json, "read", true)
+	testEnforce(t, e, "alice", data1Json, "write", true)
+	testEnforce(t, e, "alice", data2Json, "read", false)
+	testEnforce(t, e, "alice", data2Json, "write", false)
+	testEnforce(t, e, "bob", data1Json, "read", false)
+	testEnforce(t, e, "bob", data1Json, "write", false)
+	testEnforce(t, e, "bob", data2Json, "read", true)
+	testEnforce(t, e, "bob", data2Json, "write", true)
+
+	e, _ = NewEnforcer("examples/abac_not_using_policy_model.conf", "examples/abac_rule_effect_policy.csv")
+	e.EnableAcceptJsonRequest(true)
+
+	testEnforce(t, e, "alice", data1Json, "read", true)
+	testEnforce(t, e, "alice", data1Json, "write", true)
+	testEnforce(t, e, "alice", data2Json, "read", false)
+	testEnforce(t, e, "alice", data2Json, "write", false)
+
+	e, _ = NewEnforcer("examples/abac_rule_model.conf", "examples/abac_rule_policy.csv")
+	e.EnableAcceptJsonRequest(true)
+	sub1Json := `{"Name": "alice", "Age": 16}`
+	sub2Json := `{"Name": "alice", "Age": 20}`
+	sub3Json := `{"Name": "alice", "Age": 65}`
+
+	testEnforce(t, e, sub1Json, "/data1", "read", false)
+	testEnforce(t, e, sub1Json, "/data2", "read", false)
+	testEnforce(t, e, sub1Json, "/data1", "write", false)
+	testEnforce(t, e, sub1Json, "/data2", "write", true)
+	testEnforce(t, e, sub2Json, "/data1", "read", true)
+	testEnforce(t, e, sub2Json, "/data2", "read", false)
+	testEnforce(t, e, sub2Json, "/data1", "write", false)
+	testEnforce(t, e, sub2Json, "/data2", "write", true)
+	testEnforce(t, e, sub3Json, "/data1", "read", true)
+	testEnforce(t, e, sub3Json, "/data2", "read", false)
+	testEnforce(t, e, sub3Json, "/data1", "write", false)
+	testEnforce(t, e, sub3Json, "/data2", "write", false)
+}
+
 func TestKeyMatchModel(t *testing.T) {
 	e, _ := NewEnforcer("examples/keymatch_model.conf", "examples/keymatch_policy.csv")