Repository: casbin/casbin

Issue:
[Question] Performing RBAC checks against many resources (ie. API 'list' endpoint, database SELECT)
**Want to prioritize this issue? Try:**

[![issuehunt-to-marktext](https://github.com/BoostIO/issuehunt-materials/raw/master/v1/issuehunt-button-v1.svg)](https://issuehunt.io/r/casbin/casbin)

------

**What's your scenario? What do you want to achieve?**
I feel like I have a good handle on RBAC for individual resources, as this can quite easily be handled by a HTTP middleware (ie. check resource access and early-exit if denied).

However, for an API endpoint that performs a 'list' (and this is usually coupled with a DB SELECT query), I need some way of checking each resource.

The 2 approaches that stand out to me with the current API (that I'm aware of) are:

  1. Query the DB and perform an RBAC check against the resources (via. `BatchEnforce`) and filter the results. However, this skews the number of results if the user is relying on pagination as the  DB could return their provided `limit` of 50, but the application would filter this down to 20 that they have access to.

  To accommodate this, the application will probably need to query for a lot more than needed and filter down the results, and then re-query if there's still a number missing, tracking where to start from in the DB query. 

  This is pretty unwieldy, but it might be something that could be abstracted & re-used, and feels like streaming chunks to be validated from the DB.

  2. Fetch the user's permissions (`GetImplicitPermissionsForUser`) and then build an appropriate DB filter from their permissions (`WHERE` clause).

  But this could also return thousands (or more) permissions depending upon the permission model/number of resources and again means some kind of app-side filtering. Additionally, I believe this would return permissions that aren't relevant (ie. not for that resource) so they'd have to be filtered out unless there's an API that can also do this (eg. something like `GetUserPermissionsForObject(user, "location/*", domain)`?

  I would also have to special-case a wildcard permission object (eg. `location/*`) to remove the filter - But I think I can first check with `HasPermissionForUser` to see if they have a wildcard permission and then just query without a filter 🤔.

**Your model:**

```ini
[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, dom, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[role_definition]
g = _, _, _ 

[matchers]
m = g(r.sub, p.sub, r.dom) && \
    globMatch(r.dom, p.dom) && \
    globMatch(r.obj, p.obj) && \
    (r.act == p.act || p.act == "admin")

```

**Your policy:**

```
#---------------------------------------------------------------------------------------------------------------------------------------------------------------
# name                      domain                                         resource path  action
#---------------------------------------------------------------------------------------------------------------------------------------------------------------
p, role/chronicle.creator, chronicle/_,                                    chronicle/*, create

p, role/superadmin,        chronicle/*,                                    */*,         *

# chronicle/2d01bc41-0e28-4bab-9253-1fef397b1f1f
p, role/chronicle.admin,   chronicle/2d01bc41-0e28-4bab-9253-1fef397b1f1f, chronicle/2d01bc41-0e28-4bab-9253-1fef397b1f1f, create
p, role/location.admin,    chronicle/2d01bc41-0e28-4bab-9253-1fef397b1f1f, location/*,  *

#---------------------------------------------------------------------------------------------------------------------------------------------------------------
# user id                                        role                      domain
#---------------------------------------------------------------------------------------------------------------------------------------------------------------
# system user
g, system/00000000-0000-0000-0000-000000000000, role:superadmin,        chronicle/*

# test user
g, user/d90888c2-0a29-4da7-9c9e-03ba384c8c12,   role:chronicle.creator, chronicle/_
g, user/d90888c2-0a29-4da7-9c9e-03ba384c8c12,   role:chronicle.admin,   chronicle/2d01bc41-0e28-4bab-9253-1fef397b1f1f
g, user/d90888c2-0a29-4da7-9c9e-03ba384c8c12,   role:location.admin,    chronicle/2d01bc41-0e28-4bab-9253-1fef397b1f1f
```

Discussion:
@yxrxy: You can use GetImplicitObjectPatternsForUser (new API), which directly returns the resource patterns that a user can access under a specific domain and action. For your user it returns ["location/*", "chronicle/2d01bc41-0a29-4bab-9253-1fef397b1f1f"]. Then you can directly build SQL WHERE clauses from these patterns, ensuring accurate pagination.

Solution patch:
diff --git a/rbac_api.go b/rbac_api.go
index 3cf4b2d..27c3bf4 100644
--- a/rbac_api.go
+++ b/rbac_api.go
@@ -650,3 +650,81 @@ func (e *Enforcer) GetImplicitUsersForResourceByDomain(resource string, domain s
 	res := removeDuplicatePermissions(permissions)
 	return res, nil
 }
+
+// GetImplicitObjectPatternsForUser returns all object patterns (with wildcards) that a user has for a given domain and action.
+// For example:
+// p, admin, chronicle/123, location/*, read
+// p, user, chronicle/456, location/789, read
+// g, alice, admin
+// g, bob, user
+//
+// GetImplicitObjectPatternsForUser("alice", "chronicle/123", "read") will return ["location/*"].
+// GetImplicitObjectPatternsForUser("bob", "chronicle/456", "read") will return ["location/789"].
+func (e *Enforcer) GetImplicitObjectPatternsForUser(user string, domain string, action string) ([]string, error) {
+	roles, err := e.GetImplicitRolesForUser(user, domain)
+	if err != nil {
+		return nil, err
+	}
+
+	subjects := append([]string{user}, roles...)
+	subjectIndex, _ := e.GetFieldIndex("p", constant.SubjectIndex)
+	domainIndex, _ := e.GetFieldIndex("p", constant.DomainIndex)
+	objectIndex, _ := e.GetFieldIndex("p", constant.ObjectIndex)
+	actionIndex, _ := e.GetFieldIndex("p", constant.ActionIndex)
+
+	patterns := make(map[string]struct{})
+	for _, rule := range e.model["p"]["p"].Policy {
+		sub := rule[subjectIndex]
+		matched := false
+		for _, subject := range subjects {
+			if sub == subject {
+				matched = true
+				break
+			}
+		}
+		if !matched {
+			continue
+		}
+
+		if !e.matchDomain(domainIndex, domain, rule) {
+			continue
+		}
+
+		ruleAction := rule[actionIndex]
+		if ruleAction != action && ruleAction != "*" {
+			continue
+		}
+
+		obj := rule[objectIndex]
+		patterns[obj] = struct{}{}
+	}
+
+	result := make([]string, 0, len(patterns))
+	for pattern := range patterns {
+		result = append(result, pattern)
+	}
+
+	return result, nil
+}
+
+// matchDomain checks if the domain matches the rule domain using pattern matching.
+func (e *Enforcer) matchDomain(domainIndex int, domain string, rule []string) bool {
+	if domainIndex < 0 || domain == "" {
+		return true
+	}
+	ruleDomain := rule[domainIndex]
+	if ruleDomain == domain {
+		return true
+	}
+	for _, rm := range e.rmMap {
+		if rm.Match(domain, ruleDomain) {
+			return true
+		}
+	}
+	for _, crm := range e.condRmMap {
+		if crm.Match(domain, ruleDomain) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/rbac_api_synced.go b/rbac_api_synced.go
index 7b10e56..8767bb6 100644
--- a/rbac_api_synced.go
+++ b/rbac_api_synced.go
@@ -201,3 +201,18 @@ func (e *SyncedEnforcer) GetImplicitUsersForPermission(permission ...string) ([]
 	defer e.m.RUnlock()
 	return e.Enforcer.GetImplicitUsersForPermission(permission...)
 }
+
+// GetImplicitObjectPatternsForUser returns all object patterns (with wildcards) that a user has for a given domain and action.
+// For example:
+// p, admin, chronicle/123, location/*, read
+// p, user, chronicle/456, location/789, read
+// g, alice, admin
+// g, bob, user
+//
+// GetImplicitObjectPatternsForUser("alice", "chronicle/123", "read") will return ["location/*"].
+// GetImplicitObjectPatternsForUser("bob", "chronicle/456", "read") will return ["location/789"].
+func (e *SyncedEnforcer) GetImplicitObjectPatternsForUser(user string, domain string, action string) ([]string, error) {
+	e.m.RLock()
+	defer e.m.RUnlock()
+	return e.Enforcer.GetImplicitObjectPatternsForUser(user, domain, action)
+}



Test patch:
diff --git a/rbac_api_test.go b/rbac_api_test.go
index 8ec1bf3..7b59487 100644
--- a/rbac_api_test.go
+++ b/rbac_api_test.go
@@ -844,3 +844,40 @@ func TestMaxHierarchyLevelConsistency(t *testing.T) {
 		})
 	}
 }
+
+func testGetImplicitObjectPatternsForUser(t *testing.T, e *Enforcer, user string, domain string, action string, res []string) {
+	t.Helper()
+	myRes, err := e.GetImplicitObjectPatternsForUser(user, domain, action)
+	if err != nil {
+		t.Error("Implicit object patterns for ", user, " under domain ", domain, " with action ", action, " could not be fetched: ", err.Error())
+	}
+	t.Log("Implicit object patterns for ", user, " under domain ", domain, " with action ", action, ": ", myRes)
+
+	if !util.SetEquals(res, myRes) {
+		t.Error("Implicit object patterns for ", user, " under domain ", domain, " with action ", action, ": ", myRes, ", supposed to be ", res)
+	}
+}
+
+func TestGetImplicitObjectPatternsForUser(t *testing.T) {
+	// Test with domain pattern model
+	e, _ := NewEnforcer("examples/rbac_with_domain_pattern_model.conf", "examples/rbac_with_domain_pattern_policy.csv")
+	e.AddNamedDomainMatchingFunc("g", "KeyMatch", util.KeyMatch)
+
+	// Test case 1: admin user with wildcard domain access
+	testGetImplicitObjectPatternsForUser(t, e, "admin", "domain1", "read", []string{"data1", "data3"})
+	testGetImplicitObjectPatternsForUser(t, e, "admin", "domain1", "write", []string{"data1"})
+
+	// Test case 2: alice user inheriting admin role in domain2
+	testGetImplicitObjectPatternsForUser(t, e, "alice", "domain2", "read", []string{"data2", "data3"})
+	testGetImplicitObjectPatternsForUser(t, e, "alice", "domain2", "write", []string{"data2"})
+
+	// Test case 3: bob user with specific domain access
+	testGetImplicitObjectPatternsForUser(t, e, "bob", "domain2", "read", []string{"data2", "data3"})
+	testGetImplicitObjectPatternsForUser(t, e, "bob", "domain2", "write", []string{"data2"})
+
+	// Test case 4: non-existent domain (admin has wildcard access to data3)
+	testGetImplicitObjectPatternsForUser(t, e, "admin", "non_existent", "read", []string{"data3"})
+
+	// Test case 5: non-existent action
+	testGetImplicitObjectPatternsForUser(t, e, "admin", "domain1", "non_existent", []string{})
+}