Repository: casbin/casbin
Pull request #1230 (2023-04-13 09:07:59)

feat: add GetAllowedObjectConditions() API
a part of https://github.com/casbin/gorm-adapter/issues/195

casbin-bot:  @tangyang9464 @JalinWang please review

Patch:
diff --git a/errors/rbac_errors.go b/errors/rbac_errors.go
index 53c3151..7eca045 100644
--- a/errors/rbac_errors.go
+++ b/errors/rbac_errors.go
@@ -23,4 +23,8 @@ var (
 	ERR_LINK_NOT_FOUND            = errors.New("error: link between name1 and name2 does not exist")
 	ERR_USE_DOMAIN_PARAMETER      = errors.New("error: useDomain should be 1 parameter")
 	INVALID_FIELDVAULES_PARAMETER = errors.New("fieldValues requires at least one parameter")
+
+	// GetAllowedObjectConditions errors
+	ERR_OBJ_CONDITION   = errors.New("need to meet the prefix required by the object condition")
+	ERR_EMPTY_CONDITION = errors.New("GetAllowedObjectConditions have an empty condition")
 )
diff --git a/examples/object_conditions_model.conf b/examples/object_conditions_model.conf
new file mode 100644
index 0000000..55aa185
--- /dev/null
+++ b/examples/object_conditions_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, sub_rule, act
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = some(where (p.eft == allow))
+
+[matchers]
+m = g(r.sub, p.sub) && eval(p.sub_rule) && r.act == p.act
\ No newline at end of file
diff --git a/examples/object_conditions_policy.csv b/examples/object_conditions_policy.csv
new file mode 100644
index 0000000..7dad5c8
--- /dev/null
+++ b/examples/object_conditions_policy.csv
@@ -0,0 +1,5 @@
+p, alice, r.obj.price < 25, read
+p, admin, r.obj.category_id = 2, read
+p, bob, r.obj.author = bob, write
+
+g, alice, admin
\ No newline at end of file
diff --git a/persist/cache/cache_sync.go b/persist/cache/cache_sync.go
index d083a03..816e12d 100644
--- a/persist/cache/cache_sync.go
+++ b/persist/cache/cache_sync.go
@@ -29,9 +29,9 @@ func (c *SyncCache) Set(key string, value bool, extra ...interface{}) error {
 	if len(extra) > 0 {
 		ttl = extra[0].(time.Duration)
 	}
-	(*c).Lock()
-	defer (*c).Unlock()
-	(*c).cache[key] = cacheItem{
+	c.Lock()
+	defer c.Unlock()
+	c.cache[key] = cacheItem{
 		value:     value,
 		expiresAt: time.Now().Add(ttl),
 		ttl:       ttl,
@@ -40,16 +40,16 @@ func (c *SyncCache) Set(key string, value bool, extra ...interface{}) error {
 }
 
 func (c *SyncCache) Get(key string) (bool, error) {
-	(*c).RLock()
-	res, ok := (*c).cache[key]
-	(*c).RUnlock()
+	c.RLock()
+	res, ok := c.cache[key]
+	c.RUnlock()
 	if !ok {
 		return false, ErrNoSuchKey
 	} else {
 		if res.ttl > 0 && time.Now().After(res.expiresAt) {
-			(*c).Lock()
-			defer (*c).Unlock()
-			delete((*c).cache, key)
+			c.Lock()
+			defer c.Unlock()
+			delete(c.cache, key)
 			return false, ErrNoSuchKey
 		}
 		return res.value, nil
@@ -57,24 +57,23 @@ func (c *SyncCache) Get(key string) (bool, error) {
 }
 
 func (c *SyncCache) Delete(key string) error {
-	(*c).RLock()
-	_, ok := (*c).cache[key]
-	(*c).RUnlock()
+	c.RLock()
+	_, ok := c.cache[key]
+	c.RUnlock()
 	if !ok {
 		return ErrNoSuchKey
 	} else {
-		(*c).Lock()
-		defer (*c).Unlock()
-		delete((*c).cache, key)
+		c.Lock()
+		defer c.Unlock()
+		delete(c.cache, key)
 		return nil
 	}
 }
 
 func (c *SyncCache) Clear() error {
-	*c = SyncCache{
-		make(DefaultCache),
-		sync.RWMutex{},
-	}
+	c.Lock()
+	c.cache = make(DefaultCache)
+	c.Unlock()
 	return nil
 }
 
diff --git a/rbac_api.go b/rbac_api.go
index 7a44311..d4f8ff7 100644
--- a/rbac_api.go
+++ b/rbac_api.go
@@ -15,6 +15,8 @@
 package casbin
 
 import (
+	"strings"
+
 	"github.com/casbin/casbin/v2/constant"
 	"github.com/casbin/casbin/v2/errors"
 	"github.com/casbin/casbin/v2/util"
@@ -414,3 +416,39 @@ func deepCopyPolicy(src []string) []string {
 	copy(newRule, src)
 	return newRule
 }
+
+// GetAllowedObjectConditions returns a string array of object conditions that the user can access.
+// For example: conditions, err := e.GetAllowedObjectConditions("alice", "read", "r.obj.")
+// Note:
+//
+// 0. prefix: You can customize the prefix of the object conditions, and "r.obj." is commonly used as a prefix.
+// After removing the prefix, the remaining part is the condition of the object.
+// If there is an obj policy that does not meet the prefix requirement, an errors.ERR_OBJ_CONDITION will be returned.
+//
+// 1. If the 'objectConditions' array is empty, return errors.ERR_EMPTY_CONDITION
+// This error is returned because some data adapters' ORM return full table data by default
+// when they receive an empty condition, which tends to behave contrary to expectations.(e.g. GORM)
+// If you are using an adapter that does not behave like this, you can choose to ignore this error.
+func (e *Enforcer) GetAllowedObjectConditions(user string, action string, prefix string) ([]string, error) {
+	permissions, err := e.GetImplicitPermissionsForUser(user)
+	if err != nil {
+		return nil, err
+	}
+
+	var objectConditions []string
+	for _, policy := range permissions {
+		// policy {sub, obj, act}
+		if policy[2] == action {
+			if !strings.HasPrefix(policy[1], prefix) {
+				return nil, errors.ERR_OBJ_CONDITION
+			}
+			objectConditions = append(objectConditions, strings.TrimPrefix(policy[1], prefix))
+		}
+	}
+
+	if len(objectConditions) == 0 {
+		return nil, errors.ERR_EMPTY_CONDITION
+	}
+
+	return objectConditions, nil
+}


Test patch:
diff --git a/enforcer_cached_synced_test.go b/enforcer_cached_synced_test.go
index 9738d8c..06c84c4 100644
--- a/enforcer_cached_synced_test.go
+++ b/enforcer_cached_synced_test.go
@@ -17,6 +17,7 @@ package casbin
 import (
 	"sync"
 	"testing"
+	"time"
 )
 
 func testSyncEnforceCache(t *testing.T, e *SyncedCachedEnforcer, sub string, obj interface{}, act string, res bool) {
@@ -28,6 +29,7 @@ func testSyncEnforceCache(t *testing.T, e *SyncedCachedEnforcer, sub string, obj
 
 func TestSyncCache(t *testing.T) {
 	e, _ := NewSyncedCachedEnforcer("examples/basic_model.conf", "examples/basic_policy.csv")
+	e.expireTime = time.Millisecond
 	// The cache is enabled by default for NewCachedEnforcer.
 	g := sync.WaitGroup{}
 	goThread := 1000
@@ -36,6 +38,9 @@ func TestSyncCache(t *testing.T) {
 		go func() {
 			_, _ = e.AddPolicy("alice", "data2", "read")
 			testSyncEnforceCache(t, e, "alice", "data2", "read", true)
+			if e.InvalidateCache() != nil {
+				panic("never reached")
+			}
 			g.Done()
 		}()
 	}
@@ -43,6 +48,9 @@ func TestSyncCache(t *testing.T) {
 	_, _ = e.RemovePolicy("alice", "data2", "read")
 
 	testSyncEnforceCache(t, e, "alice", "data1", "read", true)
+	time.Sleep(time.Millisecond * 2) // coverage for expire
+	testSyncEnforceCache(t, e, "alice", "data1", "read", true)
+
 	testSyncEnforceCache(t, e, "alice", "data1", "write", false)
 	testSyncEnforceCache(t, e, "alice", "data2", "read", false)
 	testSyncEnforceCache(t, e, "alice", "data2", "write", false)
diff --git a/rbac_api_test.go b/rbac_api_test.go
index e604572..7c101ca 100644
--- a/rbac_api_test.go
+++ b/rbac_api_test.go
@@ -15,10 +15,11 @@
 package casbin
 
 import (
-	"github.com/casbin/casbin/v2/constant"
+	"log"
 	"sort"
 	"testing"
 
+	"github.com/casbin/casbin/v2/constant"
 	"github.com/casbin/casbin/v2/errors"
 	"github.com/casbin/casbin/v2/util"
 )
@@ -542,3 +543,52 @@ func TestCustomizedFieldIndex(t *testing.T) {
 	}
 	testEnforce(t, e, "bob", "data2", "write", false)
 }
+
+func testGetAllowedObjectConditions(t *testing.T, e *Enforcer, user string, act string, prefix string, res []string, expectedErr error) {
+	myRes, actualErr := e.GetAllowedObjectConditions(user, act, prefix)
+
+	if actualErr != expectedErr {
+		t.Error("actual Err: ", actualErr, ", supposed to be ", expectedErr)
+	}
+	if actualErr == nil {
+		log.Print("Policy: ", myRes)
+		if !util.ArrayEquals(res, myRes) {
+			t.Error("Policy: ", myRes, ", supposed to be ", res)
+		}
+	}
+}
+
+func TestGetAllowedObjectConditions(t *testing.T) {
+	e, _ := NewEnforcer("examples/object_conditions_model.conf", "examples/object_conditions_policy.csv")
+	testGetAllowedObjectConditions(t, e, "alice", "read", "r.obj.", []string{"price < 25", "category_id = 2"}, nil)
+	testGetAllowedObjectConditions(t, e, "admin", "read", "r.obj.", []string{"category_id = 2"}, nil)
+	testGetAllowedObjectConditions(t, e, "bob", "write", "r.obj.", []string{"author = bob"}, nil)
+
+	// test ERR_EMPTY_CONDITION
+	testGetAllowedObjectConditions(t, e, "alice", "write", "r.obj.", []string{}, errors.ERR_EMPTY_CONDITION)
+	testGetAllowedObjectConditions(t, e, "bob", "read", "r.obj.", []string{}, errors.ERR_EMPTY_CONDITION)
+
+	// test ERR_OBJ_CONDITION
+	// should : e.AddPolicy("alice", "r.obj.price > 50", "read")
+	ok, _ := e.AddPolicy("alice", "price > 50", "read")
+	if ok {
+		testGetAllowedObjectConditions(t, e, "alice", "read", "r.obj.", []string{}, errors.ERR_OBJ_CONDITION)
+	}
+
+	// test prefix
+	e.ClearPolicy()
+	err := e.GetRoleManager().DeleteLink("alice", "admin")
+	if err != nil {
+		panic(err)
+	}
+	ok, _ = e.AddPolicies([][]string{
+		{"alice", "r.book.price < 25", "read"},
+		{"admin", "r.book.category_id = 2", "read"},
+		{"bob", "r.book.author = bob", "write"},
+	})
+	if ok {
+		testGetAllowedObjectConditions(t, e, "alice", "read", "r.book.", []string{"price < 25"}, nil)
+		testGetAllowedObjectConditions(t, e, "admin", "read", "r.book.", []string{"category_id = 2"}, nil)
+		testGetAllowedObjectConditions(t, e, "bob", "write", "r.book.", []string{"author = bob"}, nil)
+	}
+}