diff --git a/examples/rbac_with_complex_matcher_model.conf b/examples/rbac_with_complex_matcher_model.conf new file mode 100644 index 00000000..ba39549d --- /dev/null +++ b/examples/rbac_with_complex_matcher_model.conf @@ -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 diff --git a/examples/rbac_with_complex_matcher_policy.csv b/examples/rbac_with_complex_matcher_policy.csv new file mode 100644 index 00000000..bbba695f --- /dev/null +++ b/examples/rbac_with_complex_matcher_policy.csv @@ -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, * diff --git a/rbac_api.go b/rbac_api.go index c1ca4f7a..c27c4617 100644 --- a/rbac_api.go +++ b/rbac_api.go @@ -310,6 +310,12 @@ 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 @@ -317,51 +323,158 @@ func (e *Enforcer) GetImplicitPermissionsForUser(user string, domain ...string) // // 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 diff --git a/rbac_api_complex_matcher_test.go b/rbac_api_complex_matcher_test.go new file mode 100644 index 00000000..8f92070b --- /dev/null +++ b/rbac_api_complex_matcher_test.go @@ -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) + } +}