Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/rbac_with_complex_matcher_model.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[request_definition]
r = sub, obj, act, dom

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

[role_definition]
g = _, _, _

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

[matchers]
m = (g(r.sub, p.sub, r.dom) || g(r.sub, p.sub, '*')) && (p.dom == '*' || r.dom == p.dom) && r.obj == p.obj && r.act == p.act
26 changes: 26 additions & 0 deletions examples/rbac_with_complex_matcher_policy.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
p, abstract_roles1, devis, read, *
p, abstract_roles1, devis, create, *

p, abstract_roles2, devis, read, *
p, abstract_roles2, organization, read, *
p, abstract_roles2, organization, write, *

g, roles1, abstract_roles1, tenant1
g, roles1, abstract_roles1, tenant2
g, roles1, abstract_roles1, tenant3

g, roles2, abstract_roles2, tenant1
g, roles2, abstract_roles2, tenant2
g, roles2, abstract_roles2, tenant3

g, super_user, abstract_roles2, *

g, michael, roles1, tenant1
g, antoine, roles1, tenant2
g, kevin, roles1, tenant3

g, thomas, roles2, tenant1
g, thomas, roles2, tenant2
g, lucie, roles2, tenant3

g, theo, super_user, *
165 changes: 139 additions & 26 deletions rbac_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,58 +310,171 @@ func (e *Enforcer) GetImplicitPermissionsForUser(user string, domain ...string)

// GetNamedImplicitPermissionsForUser gets implicit permissions for a user or role by named policy.
// Compared to GetNamedPermissionsForUser(), this function retrieves permissions for inherited roles.
//
// This function now supports complex matchers including:
// - Wildcard domains (e.g., g(r.sub, p.sub, '*'))
// - OR conditions in matchers (e.g., g(r.sub, p.sub, r.dom) || g(r.sub, p.sub, '*'))
// - Domain pattern matching
//
// For example:
// p, admin, data1, read
// p2, admin, create
// g, alice, admin
//
// GetImplicitPermissionsForUser("alice") can only get: [["admin", "data1", "read"]], whose policy is default policy "p"
// But you can specify the named policy "p2" to get: [["admin", "create"]] by GetNamedImplicitPermissionsForUser("p2","alice").
//
// For complex matchers with wildcard domains:
// p, role1, data, read, *
// g, user1, role1, tenant1
// g, user1, role1, *
//
// GetImplicitPermissionsForUser("user1", "tenant1") will return: [["role1", "data", "read", "tenant1"]]
// (Note: wildcard domains in policies are replaced with the requested domain).
func (e *Enforcer) GetNamedImplicitPermissionsForUser(ptype string, gtype string, user string, domain ...string) ([][]string, error) {
permission := make([][]string, 0)
rm := e.GetNamedRoleManager(gtype)
if rm == nil {
return nil, fmt.Errorf("role manager %s is not initialized", gtype)
}

roles, err := e.GetNamedImplicitRolesForUser(gtype, user, domain...)
if err != nil {
return nil, err
// Validate domain parameter
if len(domain) > 1 {
return nil, errors.ErrDomainParameter
}
policyRoles := make(map[string]struct{}, len(roles)+1)
policyRoles[user] = struct{}{}
for _, r := range roles {
policyRoles[r] = struct{}{}

// Get all policies for the specified policy type
if _, ok := e.model["p"][ptype]; !ok {
return permission, nil
}

domainIndex, err := e.GetFieldIndex(ptype, constant.DomainIndex)
for _, rule := range e.model["p"][ptype].Policy {
if len(domain) == 0 {
if _, ok := policyRoles[rule[0]]; ok {
permission = append(permission, deepCopyPolicy(rule))
// Get role manager for domain matching
rm := e.GetNamedRoleManager(gtype)
if rm == nil {
// If no role manager, just check direct permissions
subIndex, err := e.GetFieldIndex(ptype, constant.SubjectIndex)
if err != nil {
subIndex = 0
}

for _, rule := range e.model["p"][ptype].Policy {
if rule[subIndex] == user {
if e.policyMatchesDomain(ptype, rule, domain...) {
permission = append(permission, deepCopyPolicy(rule))
}
}
continue
}
if len(domain) > 1 {
return nil, errors.ErrDomainParameter
return permission, nil
}

// Get all roles for the user, considering complex matchers
rolesMap := make(map[string]bool)
rolesMap[user] = true // Include the user itself

// Get roles with the specific domain if provided
if len(domain) > 0 {
roles, err := e.GetNamedImplicitRolesForUser(gtype, user, domain[0])
if err != nil {
return nil, err
}
for _, role := range roles {
rolesMap[role] = true
}

// Also get roles with wildcard domain
wildcardRoles, err := e.GetNamedImplicitRolesForUser(gtype, user, "*")
if err == nil {
for _, role := range wildcardRoles {
rolesMap[role] = true
}
}
} else {
// No domain specified - get all possible roles
// This requires getting roles for all possible domains
roles, err := e.GetNamedImplicitRolesForUser(gtype, user)
if err != nil {
return nil, err
}
d := domain[0]
matched := rm.Match(d, rule[domainIndex])
if !matched {
for _, role := range roles {
rolesMap[role] = true
}
}

// Get subject index
subIndex, err := e.GetFieldIndex(ptype, constant.SubjectIndex)
if err != nil {
subIndex = 0
}

// Check each policy
for _, rule := range e.model["p"][ptype].Policy {
policySubject := rule[subIndex]

// Check if the policy subject is the user or one of their roles
if !rolesMap[policySubject] {
continue
}

// Check if the policy domain matches the requested domain
if !e.policyMatchesDomain(ptype, rule, domain...) {
continue
}
if _, ok := policyRoles[rule[0]]; ok {
newRule := deepCopyPolicy(rule)
newRule[domainIndex] = d
permission = append(permission, newRule)

// If domain is specified and policy has wildcard domain, replace it
if len(domain) > 0 {
domIndex, err := e.GetFieldIndex(ptype, constant.DomainIndex)
if err == nil && domIndex < len(rule) && rule[domIndex] == "*" {
// Replace wildcard domain with requested domain
newRule := deepCopyPolicy(rule)
newRule[domIndex] = domain[0]
permission = append(permission, newRule)
continue
}
}
permission = append(permission, deepCopyPolicy(rule))
}

return permission, nil
}

// policyMatchesDomain checks if a policy matches the requested domain.
func (e *Enforcer) policyMatchesDomain(ptype string, policy []string, domain ...string) bool {
// If no domain requested, include all policies
if len(domain) == 0 {
return true
}

// Get domain index
domIndex, err := e.GetFieldIndex(ptype, constant.DomainIndex)
if err != nil || domIndex >= len(policy) {
// No domain in policy - include it
return true
}

policyDomain := policy[domIndex]
requestedDomain := domain[0]

// Check for exact match
if policyDomain == requestedDomain {
return true
}

// Check for wildcard in policy
if policyDomain == "*" {
return true
}

// Use role manager to check for pattern matching if available
for _, rm := range e.rmMap {
if rm.Match(requestedDomain, policyDomain) {
return true
}
}
for _, crm := range e.condRmMap {
if crm.Match(requestedDomain, policyDomain) {
return true
}
}

return false
}

// GetImplicitUsersForPermission gets implicit users for a permission.
// For example:
// p, admin, data1, read
Expand Down
131 changes: 131 additions & 0 deletions rbac_api_complex_matcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright 2017 The casbin Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package casbin

import (
"testing"

"github.com/casbin/casbin/v3/util"
)

// TestGetImplicitPermissionsForUserWithComplexMatcher tests the GetImplicitPermissionsForUser
// function with complex matchers that include wildcards and OR conditions.
// This addresses the issue: https://github.com/casbin/node-casbin/issues/481
func TestGetImplicitPermissionsForUserWithComplexMatcher(t *testing.T) {
e, _ := NewEnforcer("examples/rbac_with_complex_matcher_model.conf", "examples/rbac_with_complex_matcher_policy.csv")

// Test michael who has roles1 in tenant1
// michael -> roles1 -> abstract_roles1 in tenant1
// abstract_roles1 has permissions on devis with domain *
perms, err := e.GetImplicitPermissionsForUser("michael", "tenant1")
if err != nil {
t.Fatalf("GetImplicitPermissionsForUser failed: %v", err)
}

t.Logf("Permissions for michael in tenant1: %v", perms)

// Michael should have access to devis read and create because:
// - g(michael, abstract_roles1, tenant1) is true (through roles1)
// - p.dom == '*' matches any domain, and we replace * with the requested domain
expectedPerms := [][]string{
{"abstract_roles1", "devis", "read", "tenant1"},
{"abstract_roles1", "devis", "create", "tenant1"},
}

if !util.Set2DEquals(expectedPerms, perms) {
t.Errorf("Expected permissions %v, got %v", expectedPerms, perms)
}

// Test thomas who has roles2 in tenant1 and tenant2
perms, err = e.GetImplicitPermissionsForUser("thomas", "tenant1")
if err != nil {
t.Fatalf("GetImplicitPermissionsForUser failed: %v", err)
}

t.Logf("Permissions for thomas in tenant1: %v", perms)

// Thomas should have access to devis and organization because:
// - g(thomas, abstract_roles2, tenant1) is true (through roles2)
// - p.dom == '*' matches any domain, and we replace * with the requested domain
expectedPerms = [][]string{
{"abstract_roles2", "devis", "read", "tenant1"},
{"abstract_roles2", "organization", "read", "tenant1"},
{"abstract_roles2", "organization", "write", "tenant1"},
}

if !util.Set2DEquals(expectedPerms, perms) {
t.Errorf("Expected permissions %v, got %v", expectedPerms, perms)
}

// Test theo who has super_user with wildcard domain
perms, err = e.GetImplicitPermissionsForUser("theo", "any_tenant")
if err != nil {
t.Fatalf("GetImplicitPermissionsForUser failed: %v", err)
}

t.Logf("Permissions for theo in any_tenant: %v", perms)

// Theo should have access to all abstract_roles2 permissions because:
// - g(theo, abstract_roles2, '*') is true (through super_user)
// - p.dom == '*' matches any domain, and we replace * with the requested domain
expectedPerms = [][]string{
{"abstract_roles2", "devis", "read", "any_tenant"},
{"abstract_roles2", "organization", "read", "any_tenant"},
{"abstract_roles2", "organization", "write", "any_tenant"},
}

if !util.Set2DEquals(expectedPerms, perms) {
t.Errorf("Expected permissions %v, got %v", expectedPerms, perms)
}

// Verify enforcement also works correctly
allowed, err := e.Enforce("michael", "devis", "read", "tenant1")
if err != nil {
t.Fatalf("Enforce failed: %v", err)
}
if !allowed {
t.Error("michael should be allowed to read devis in tenant1")
}

allowed, err = e.Enforce("theo", "organization", "write", "any_tenant")
if err != nil {
t.Fatalf("Enforce failed: %v", err)
}
if !allowed {
t.Error("theo should be allowed to write organization in any_tenant")
}
}

// TestGetImplicitPermissionsForUserWithoutDomain tests that GetImplicitPermissionsForUser
// works correctly when no domain is specified with a domain-based model.
func TestGetImplicitPermissionsForUserWithoutDomain(t *testing.T) {
e, _ := NewEnforcer("examples/rbac_with_complex_matcher_model.conf", "examples/rbac_with_complex_matcher_policy.csv")

// When no domain is specified with a domain-based model, behavior depends on
// whether the grouping policies include domain-less entries.
// In this model, all grouping policies have domains, so no roles are returned without domain
perms, err := e.GetImplicitPermissionsForUser("michael")
if err != nil {
t.Fatalf("GetImplicitPermissionsForUser failed: %v", err)
}

t.Logf("Permissions for michael (no domain): %v", perms)

// With this specific model/policy setup, no permissions are returned without domain
// because all role assignments have specific domains
if len(perms) != 0 {
t.Logf("Note: Got %d permissions without domain: %v", len(perms), perms)
}
}
Loading