Repository: casbin/casbin
Pull request #427 (2020-04-20 15:49:47)

Scaling ABAC Rules
Fix: https://github.com/casbin/casbin/issues/354

coveralls: 
[![Coverage Status](https://coveralls.io/builds/30199021/badge)](https://coveralls.io/builds/30199021)

Coverage decreased (-0.02%) to 71.774% when pulling **0c1d7b4aadc1330438cd4522395a583fe337c2db on dovics:ABAC** into **907df7925df1a0e7b565cb915f316278bcb5d19d on casbin:master**.

hsluoyz: Message not good:

![image](https://user-images.githubusercontent.com/3787410/79772188-60a9dd00-8362-11ea-864b-b4a447e80062.png)


Patch:
diff --git a/enforcer.go b/enforcer.go
index ec5179f..696617e 100644
--- a/enforcer.go
+++ b/enforcer.go
@@ -371,9 +371,15 @@ func (e *Enforcer) enforce(matcher string, rvals ...interface{}) (ok bool, err e
 	} else {
 		expString = matcher
 	}
-	expression, err := govaluate.NewEvaluableExpressionWithFunctions(expString, functions)
-	if err != nil {
-		return false, err
+
+	var expression *govaluate.EvaluableExpression
+	hasEval := util.HasEval(expString)
+
+	if !hasEval {
+		expression, err = govaluate.NewEvaluableExpressionWithFunctions(expString, functions)
+		if err != nil {
+			return false, err
+		}
 	}
 
 	rTokens := make(map[string]int, len(e.model["r"]["r"].Tokens))
@@ -416,6 +422,25 @@ func (e *Enforcer) enforce(matcher string, rvals ...interface{}) (ok bool, err e
 
 			parameters.pVals = pvals
 
+			if hasEval {
+				ruleNames := util.GetEvalValue(expString)
+				var expWithRule = expString
+				for _, ruleName := range ruleNames {
+					if j, ok := parameters.pTokens[ruleName]; ok {
+						rule := util.EscapeAssertion(pvals[j])
+						expWithRule = util.ReplaceEval(expWithRule, rule)
+					} else {
+						return false, errors.New("please make sure rule exists in policy when using eval() in matcher")
+					}
+
+					expression, err = govaluate.NewEvaluableExpressionWithFunctions(expWithRule, functions)
+					if err != nil {
+						return false, fmt.Errorf("p.sub_rule should satisfy the syntax of matcher: %s", err)
+					}
+				}
+
+			}
+
 			result, err := expression.Eval(parameters)
 			// log.LogPrint("Result: ", result)
 
diff --git a/examples/abac_rule_model.conf b/examples/abac_rule_model.conf
new file mode 100644
index 0000000..591dd3a
--- /dev/null
+++ b/examples/abac_rule_model.conf
@@ -0,0 +1,11 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub_rule, obj, act
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = eval(p.sub_rule) && r.obj == p.obj && r.act == p.act
\ No newline at end of file
diff --git a/examples/abac_rule_policy.csv b/examples/abac_rule_policy.csv
new file mode 100644
index 0000000..e3dbc83
--- /dev/null
+++ b/examples/abac_rule_policy.csv
@@ -0,0 +1,2 @@
+p, r.sub.Age > 18, /data1, read
+p, r.sub.Age < 60, /data2, write
\ No newline at end of file
diff --git a/util/util.go b/util/util.go
index e25dc22..7e48ff1 100644
--- a/util/util.go
+++ b/util/util.go
@@ -20,11 +20,13 @@ import (
 	"strings"
 )
 
+var evalReg *regexp.Regexp = regexp.MustCompile(`\beval\((?P<rule>[^),]*)\)`)
+
 // EscapeAssertion escapes the dots in the assertion, because the expression evaluation doesn't support such variable names.
 func EscapeAssertion(s string) string {
 	//Replace the first dot, because it can't be recognized by the regexp.
-	if (strings.HasPrefix(s, "r") || strings.HasPrefix(s, "p")) {
-		s = strings.Replace(s, ".", "_",1)
+	if strings.HasPrefix(s, "r") || strings.HasPrefix(s, "p") {
+		s = strings.Replace(s, ".", "_", 1)
 	}
 	var regex = regexp.MustCompile(`(\|| |=|\)|\(|&|<|>|,|\+|-|!|\*|\/)(r|p)\.`)
 	s = regex.ReplaceAllStringFunc(s, func(m string) string {
@@ -149,3 +151,23 @@ func SetSubtract(a []string, b []string) []string {
 	}
 	return diff
 }
+
+// HasEval determine whether matcher contains function eval
+func HasEval(s string) bool {
+	return evalReg.MatchString(s)
+}
+
+// ReplaceEval replace function eval with the value of its parameters
+func ReplaceEval(s string, rule string) string {
+	return evalReg.ReplaceAllString(s, "("+rule+")")
+}
+
+// GetEvalValue returns the parameters of function eval
+func GetEvalValue(s string) []string {
+	subMatch := evalReg.FindAllStringSubmatch(s, -1)
+	var rules []string
+	for _, rule := range subMatch {
+		rules = append(rules, rule[1])
+	}
+	return rules
+}


Test patch:
diff --git a/model_test.go b/model_test.go
index 2b14312..bd631f0 100644
--- a/model_test.go
+++ b/model_test.go
@@ -23,7 +23,7 @@ import (
 	"github.com/casbin/casbin/v2/util"
 )
 
-func testEnforce(t *testing.T, e *Enforcer, sub string, obj interface{}, act string, res bool) {
+func testEnforce(t *testing.T, e *Enforcer, sub interface{}, obj interface{}, act string, res bool) {
 	t.Helper()
 	if myRes, _ := e.Enforce(sub, obj, act); myRes != res {
 		t.Errorf("%s, %v, %s: %t, supposed to be %t", sub, obj, act, myRes, res)
@@ -528,3 +528,35 @@ func TestRBACModelInMultiLines(t *testing.T) {
 	testEnforce(t, e, "bob", "data2", "read", false)
 	testEnforce(t, e, "bob", "data2", "write", true)
 }
+
+type testSub struct {
+	Name string
+	Age  int
+}
+
+func newTestSubject(name string, age int) testSub {
+	s := testSub{}
+	s.Name = name
+	s.Age = age
+	return s
+}
+
+func TestABACPolicy(t *testing.T) {
+	e, _ := NewEnforcer("examples/abac_rule_model.conf", "examples/abac_rule_policy.csv")
+	sub1 := newTestSubject("alice", 16)
+	sub2 := newTestSubject("alice", 20)
+	sub3 := newTestSubject("alice", 65)
+
+	testEnforce(t, e, sub1, "/data1", "read", false)
+	testEnforce(t, e, sub1, "/data2", "read", false)
+	testEnforce(t, e, sub1, "/data1", "write", false)
+	testEnforce(t, e, sub1, "/data2", "write", true)
+	testEnforce(t, e, sub2, "/data1", "read", true)
+	testEnforce(t, e, sub2, "/data2", "read", false)
+	testEnforce(t, e, sub2, "/data1", "write", false)
+	testEnforce(t, e, sub2, "/data2", "write", true)
+	testEnforce(t, e, sub3, "/data1", "read", true)
+	testEnforce(t, e, sub3, "/data2", "read", false)
+	testEnforce(t, e, sub3, "/data1", "write", false)
+	testEnforce(t, e, sub3, "/data2", "write", false)
+}
diff --git a/util/util_test.go b/util/util_test.go
index 9986f11..39732cd 100644
--- a/util/util_test.go
+++ b/util/util_test.go
@@ -113,3 +113,47 @@ func TestSetEquals(t *testing.T) {
 	testSetEquals(t, []string{"a", "b", "c"}, []string{"a", "c", "b"}, true)
 	testSetEquals(t, []string{"a", "b", "c"}, []string{}, false)
 }
+
+func testContainEval(t *testing.T, s string, res bool) {
+	t.Helper()
+	myRes := HasEval(s)
+	if myRes != res {
+		t.Errorf("%s: %t, supposed to be %t", s, myRes, res)
+	}
+}
+func TestContainEval(t *testing.T) {
+	testContainEval(t, "eval() && a && b && c", true)
+	testContainEval(t, "eval) && a && b && c", false)
+	testContainEval(t, "eval)( && a && b && c", false)
+	testContainEval(t, "eval(c * (a + b)) && a && b && c", true)
+	testContainEval(t, "xeval() && a && b && c", false)
+}
+
+func testReplaceEval(t *testing.T, s string, rule string, res string) {
+	t.Helper()
+	myRes := ReplaceEval(s, rule)
+
+	if myRes != res {
+		t.Errorf("%s: %s supposed to be %s", s, myRes, res)
+	}
+}
+func TestReplaceEval(t *testing.T) {
+	testReplaceEval(t, "eval() && a && b && c", "a", "(a) && a && b && c")
+	testReplaceEval(t, "eval() && a && b && c", "(a)", "((a)) && a && b && c")
+}
+
+func testGetEvalValue(t *testing.T, s string, res []string) {
+	t.Helper()
+	myRes := GetEvalValue(s)
+
+	if !ArrayEquals(myRes, res) {
+		t.Errorf("%s: %s supposed to be %s", s, myRes, res)
+	}
+}
+
+func TestGetEvalValue(t *testing.T) {
+	testGetEvalValue(t, "eval(a) && a && b && c", []string{"a"})
+	testGetEvalValue(t, "a && eval(a) && b && c", []string{"a"})
+	testGetEvalValue(t, "eval(a) && eval(b) && a && b && c", []string{"a", "b"})
+	testGetEvalValue(t, "a && eval(a) && eval(b) && b && c", []string{"a", "b"})
+}