feat: deduplicate permission RBAC by building grouping policies in run time (#5374)

This commit is contained in:
cooronx
2026-04-08 23:01:00 +08:00
committed by GitHub
parent cef6b85389
commit 315a6bb040
4 changed files with 473 additions and 178 deletions

View File

@@ -120,18 +120,6 @@ func checkPermissionValid(permission *Permission) error {
return nil
}
groupingPolicies, err := getGroupingPolicies(permission)
if err != nil {
return err
}
if len(groupingPolicies) > 0 {
_, err = enforcer.AddGroupingPolicies(groupingPolicies)
if err != nil {
return err
}
}
return nil
}
@@ -171,11 +159,6 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
}
if affected != 0 {
err = removeGroupingPolicies(oldPermission)
if err != nil {
return false, err
}
err = removePolicies(oldPermission)
if err != nil {
return false, err
@@ -191,11 +174,6 @@ func UpdatePermission(id string, permission *Permission) (bool, error) {
// }
// }
err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
@@ -212,11 +190,6 @@ func AddPermission(permission *Permission) (bool, error) {
}
if affected != 0 {
err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
@@ -241,11 +214,6 @@ func AddPermissions(permissions []*Permission) (bool, error) {
for _, permission := range permissions {
// add using for loop
if affected != 0 {
err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
@@ -302,11 +270,6 @@ func DeletePermission(permission *Permission) (bool, error) {
}
if affected {
err = removeGroupingPolicies(permission)
if err != nil {
return false, err
}
err = removePolicies(permission)
if err != nil {
return false, err

View File

@@ -52,11 +52,9 @@ func getPermissionEnforcer(p *Permission, permissionIDs ...string) (*casbin.Enfo
}
policyFilter := xormadapter.Filter{
V5: policyFilterV5,
}
if !HasRoleDefinition(enforcer.GetModel()) {
policyFilter.Ptype = []string{"p"}
// Permission enforcers only persist p rules. Legacy g rows are rebuilt from roles at runtime.
Ptype: []string{"p"},
V5: policyFilterV5,
}
err = enforcer.LoadFilteredPolicy(policyFilter)
@@ -64,6 +62,12 @@ func getPermissionEnforcer(p *Permission, permissionIDs ...string) (*casbin.Enfo
return nil, err
}
// we can rebuild group policies in memory
err = loadRuntimeGroupingPolicies(enforcer, p, permissionIDs...)
if err != nil {
return nil, err
}
return enforcer, nil
}
@@ -141,13 +145,47 @@ func getPolicies(permission *Permission) [][]string {
return policies
}
func getRolesInRole(roleId string, visited map[string]struct{}) ([]*Role, error) {
type permissionRoleResolver struct {
rolesByOwner map[string][]*Role
roleByID map[string]*Role
}
func newPermissionRoleResolver() *permissionRoleResolver {
return &permissionRoleResolver{
rolesByOwner: map[string][]*Role{},
roleByID: map[string]*Role{},
}
}
func (r *permissionRoleResolver) getRoles(owner string) ([]*Role, error) {
if roles, ok := r.rolesByOwner[owner]; ok {
return roles, nil
}
roles, err := GetRoles(owner)
if err != nil {
return nil, err
}
r.rolesByOwner[owner] = roles
for _, role := range roles {
r.roleByID[role.GetId()] = role
}
return roles, nil
}
func (r *permissionRoleResolver) getRolesInRole(permissionOwner string, roleId string, visited map[string]struct{}) ([]*Role, error) {
if roleId == "*" {
roleId = util.GetId(permissionOwner, "*")
}
roleOwner, roleName, err := util.GetOwnerAndNameFromIdWithError(roleId)
if err != nil {
return []*Role{}, err
}
if roleName == "*" {
roles, err := GetRoles(roleOwner)
roles, err := r.getRoles(roleOwner)
if err != nil {
return []*Role{}, err
}
@@ -155,11 +193,13 @@ func getRolesInRole(roleId string, visited map[string]struct{}) ([]*Role, error)
return roles, nil
}
role, err := GetRole(roleId)
_, err = r.getRoles(roleOwner)
if err != nil {
return []*Role{}, err
}
role := r.roleByID[roleId]
if role == nil {
return []*Role{}, nil
}
@@ -168,55 +208,94 @@ func getRolesInRole(roleId string, visited map[string]struct{}) ([]*Role, error)
roles := []*Role{role}
for _, subRole := range role.Roles {
if _, ok := visited[subRole]; !ok {
r, err := getRolesInRole(subRole, visited)
subRoles, err := r.getRolesInRole(roleOwner, subRole, visited)
if err != nil {
return []*Role{}, err
}
roles = append(roles, r...)
roles = append(roles, subRoles...)
}
}
return roles, nil
}
func getGroupingPolicies(permission *Permission) ([][]string, error) {
var groupingPolicies [][]string
func getPermissionEnforcerTargets(permission *Permission, permissionIDs ...string) ([]*Permission, error) {
if len(permissionIDs) == 0 {
return []*Permission{permission}, nil
}
domainExist := len(permission.Domains) > 0
permissionId := permission.GetId()
for _, roleId := range permission.Roles {
visited := map[string]struct{}{}
if roleId == "*" {
roleId = util.GetId(permission.Owner, "*")
permissions := make([]*Permission, 0, len(permissionIDs))
visited := map[string]struct{}{}
for _, permissionID := range permissionIDs {
if _, ok := visited[permissionID]; ok {
continue
}
rolesInRole, err := getRolesInRole(roleId, visited)
targetPermission, err := GetPermission(permissionID)
if err != nil {
return nil, err
}
if targetPermission == nil {
return nil, fmt.Errorf("the permission: %s doesn't exist", permissionID)
}
for _, role := range rolesInRole {
roleId = role.GetId()
for _, subUser := range role.Users {
if domainExist {
for _, domain := range permission.Domains {
groupingPolicies = append(groupingPolicies, []string{subUser, roleId, domain, "", "", permissionId})
}
} else {
groupingPolicies = append(groupingPolicies, []string{subUser, roleId, "", "", "", permissionId})
}
permissions = append(permissions, targetPermission)
visited[permissionID] = struct{}{}
}
return permissions, nil
}
func newRuntimeGroupingPolicy(sub string, roleId string, domain string) []string {
return []string{sub, roleId, domain, "", "", ""}
}
func appendRuntimeGroupingPolicy(groupingPolicies *[][]string, visited map[string]struct{}, rule []string) {
// we can't use []string as key, so use null character
key := strings.Join(rule, "\x00")
if _, ok := visited[key]; ok {
return
}
*groupingPolicies = append(*groupingPolicies, rule)
visited[key] = struct{}{}
}
func getRuntimeGroupingPolicies(permissions []*Permission) ([][]string, error) {
var groupingPolicies [][]string
visitedPolicies := map[string]struct{}{}
roleResolver := newPermissionRoleResolver()
for _, permission := range permissions {
domainExist := len(permission.Domains) > 0
for _, roleId := range permission.Roles {
visited := map[string]struct{}{}
rolesInRole, err := roleResolver.getRolesInRole(permission.Owner, roleId, visited)
if err != nil {
return nil, err
}
for _, subRole := range role.Roles {
if domainExist {
for _, domain := range permission.Domains {
groupingPolicies = append(groupingPolicies, []string{subRole, roleId, domain, "", "", permissionId})
for _, role := range rolesInRole {
currentRoleID := role.GetId()
for _, subUser := range role.Users {
if domainExist {
for _, domain := range permission.Domains {
appendRuntimeGroupingPolicy(&groupingPolicies, visitedPolicies, newRuntimeGroupingPolicy(subUser, currentRoleID, domain))
}
} else {
appendRuntimeGroupingPolicy(&groupingPolicies, visitedPolicies, newRuntimeGroupingPolicy(subUser, currentRoleID, ""))
}
}
for _, subRole := range role.Roles {
if domainExist {
for _, domain := range permission.Domains {
appendRuntimeGroupingPolicy(&groupingPolicies, visitedPolicies, newRuntimeGroupingPolicy(subRole, currentRoleID, domain))
}
} else {
appendRuntimeGroupingPolicy(&groupingPolicies, visitedPolicies, newRuntimeGroupingPolicy(subRole, currentRoleID, ""))
}
} else {
groupingPolicies = append(groupingPolicies, []string{subRole, roleId, "", "", "", permissionId})
}
}
}
@@ -225,6 +304,35 @@ func getGroupingPolicies(permission *Permission) ([][]string, error) {
return groupingPolicies, nil
}
func loadRuntimeGroupingPolicies(enforcer *casbin.Enforcer, permission *Permission, permissionIDs ...string) error {
if !HasRoleDefinition(enforcer.GetModel()) {
return nil
}
targetPermissions, err := getPermissionEnforcerTargets(permission, permissionIDs...)
if err != nil {
return err
}
groupingPolicies, err := getRuntimeGroupingPolicies(targetPermissions)
if err != nil {
return err
}
if len(groupingPolicies) == 0 {
return nil
}
enforcer.EnableAutoSave(false)
defer enforcer.EnableAutoSave(true)
_, err = enforcer.AddGroupingPolicies(groupingPolicies)
if err != nil {
return err
}
return nil
}
func addPolicies(permission *Permission) error {
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
@@ -249,48 +357,6 @@ func removePolicies(permission *Permission) error {
return err
}
func addGroupingPolicies(permission *Permission) error {
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return err
}
groupingPolicies, err := getGroupingPolicies(permission)
if err != nil {
return err
}
if len(groupingPolicies) > 0 {
_, err = enforcer.AddGroupingPolicies(groupingPolicies)
if err != nil {
return err
}
}
return nil
}
func removeGroupingPolicies(permission *Permission) error {
enforcer, err := getPermissionEnforcer(permission)
if err != nil {
return err
}
groupingPolicies, err := getGroupingPolicies(permission)
if err != nil {
return err
}
if len(groupingPolicies) > 0 {
_, err = enforcer.RemoveGroupingPolicies(groupingPolicies)
if err != nil {
return err
}
}
return nil
}
func Enforce(permission *Permission, request []string, permissionIds ...string) (bool, error) {
enforcer, err := getPermissionEnforcer(permission, permissionIds...)
if err != nil {

View File

@@ -0,0 +1,314 @@
// Copyright 2024 The Casdoor 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.
//go:build !skipCi
package object
import (
"fmt"
"sync"
"testing"
"github.com/casdoor/casdoor/util"
)
type permissionRuleRecord struct {
Id int64 `xorm:"pk autoincr"`
Ptype string `xorm:"varchar(100) index not null default ''"`
V0 string `xorm:"varchar(100) index not null default ''"`
V1 string `xorm:"varchar(100) index not null default ''"`
V2 string `xorm:"varchar(100) index not null default ''"`
V3 string `xorm:"varchar(100) index not null default ''"`
V4 string `xorm:"varchar(100) index not null default ''"`
V5 string `xorm:"varchar(100) index not null default ''"`
}
func (permissionRuleRecord) TableName() string {
return "permission_rule"
}
var permissionRbacTestInit sync.Once
func initPermissionRbacTestDb(t *testing.T) {
t.Helper()
permissionRbacTestInit.Do(func() {
oldCreateDatabase := createDatabase
createDatabase = false
InitConfig()
createDatabase = oldCreateDatabase
})
}
func newPermissionRbacTestOwner(t *testing.T) string {
t.Helper()
initPermissionRbacTestDb(t)
owner := "rbac-dedup-" + util.GenerateId()
t.Cleanup(func() {
_, err := ormer.Engine.Where("v5 like ?", owner+"/%").Delete(&permissionRuleRecord{})
if err != nil {
t.Fatalf("failed to delete permission rules for owner %s: %v", owner, err)
}
_, err = ormer.Engine.Where("owner = ?", owner).Delete(&Permission{})
if err != nil {
t.Fatalf("failed to delete permissions for owner %s: %v", owner, err)
}
_, err = ormer.Engine.Where("owner = ?", owner).Delete(&Role{})
if err != nil {
t.Fatalf("failed to delete roles for owner %s: %v", owner, err)
}
})
return owner
}
func newTestPermission(owner string, name string, roleIDs ...string) *Permission {
return &Permission{
Owner: owner,
Name: name,
Roles: roleIDs,
Resources: []string{"data1"},
Actions: []string{"read"},
Effect: "Allow",
}
}
func getPermissionRulesByPermissionID(t *testing.T, permissionID string) []permissionRuleRecord {
t.Helper()
rules := make([]permissionRuleRecord, 0)
err := ormer.Engine.Where("v5 = ?", permissionID).Asc("id").Find(&rules)
if err != nil {
t.Fatalf("failed to query permission rules for %s: %v", permissionID, err)
}
return rules
}
func TestPermissionRuntimeGroupingIgnoresPersistedG(t *testing.T) {
owner := newPermissionRbacTestOwner(t)
role := &Role{
Owner: owner,
Name: "reader",
Users: []string{owner + "/alice"},
}
affected, err := AddRole(role)
if err != nil {
t.Fatalf("AddRole() error: %v", err)
}
if !affected {
t.Fatalf("expected AddRole to affect rows")
}
permission := newTestPermission(owner, "perm-reader", role.GetId())
affected, err = AddPermission(permission)
if err != nil {
t.Fatalf("AddPermission() error: %v", err)
}
if !affected {
t.Fatalf("expected AddPermission to affect rows")
}
rules := getPermissionRulesByPermissionID(t, permission.GetId())
if len(rules) != 1 || rules[0].Ptype != "p" {
t.Fatalf("expected exactly one persisted p rule, got %+v", rules)
}
allowed, err := Enforce(permission, []string{owner + "/alice", "data1", "read"})
if err != nil {
t.Fatalf("Enforce() for alice error: %v", err)
}
if !allowed {
t.Fatalf("expected alice to be allowed")
}
_, err = ormer.Engine.Insert(&permissionRuleRecord{
Ptype: "g",
V0: owner + "/mallory",
V1: role.GetId(),
V5: permission.GetId(),
})
if err != nil {
t.Fatalf("failed to insert legacy g rule: %v", err)
}
allowed, err = Enforce(permission, []string{owner + "/mallory", "data1", "read"})
if err != nil {
t.Fatalf("Enforce() for mallory error: %v", err)
}
if allowed {
t.Fatalf("expected legacy persisted g rule to be ignored")
}
}
func TestUpdateRoleUsesRuntimeGroupingAndOnlyRenameRewritesP(t *testing.T) {
owner := newPermissionRbacTestOwner(t)
role := &Role{
Owner: owner,
Name: "reader-old",
Users: []string{owner + "/alice"},
}
affected, err := AddRole(role)
if err != nil {
t.Fatalf("AddRole() error: %v", err)
}
if !affected {
t.Fatalf("expected AddRole to affect rows")
}
permission := newTestPermission(owner, "perm-reader", role.GetId())
affected, err = AddPermission(permission)
if err != nil {
t.Fatalf("AddPermission() error: %v", err)
}
if !affected {
t.Fatalf("expected AddPermission to affect rows")
}
rulesBefore := getPermissionRulesByPermissionID(t, permission.GetId())
updatedRole := *role
updatedRole.Users = []string{owner + "/bob"}
affected, err = UpdateRole(role.GetId(), &updatedRole)
if err != nil {
t.Fatalf("UpdateRole() for membership change error: %v", err)
}
if !affected {
t.Fatalf("expected UpdateRole membership change to affect rows")
}
rulesAfterMembershipChange := getPermissionRulesByPermissionID(t, permission.GetId())
if fmt.Sprintf("%#v", rulesBefore) != fmt.Sprintf("%#v", rulesAfterMembershipChange) {
t.Fatalf("expected membership change to keep persisted permission rules unchanged")
}
allowed, err := Enforce(permission, []string{owner + "/alice", "data1", "read"})
if err != nil {
t.Fatalf("Enforce() for alice after membership change error: %v", err)
}
if allowed {
t.Fatalf("expected alice to lose permission after membership change")
}
allowed, err = Enforce(permission, []string{owner + "/bob", "data1", "read"})
if err != nil {
t.Fatalf("Enforce() for bob after membership change error: %v", err)
}
if !allowed {
t.Fatalf("expected bob to gain permission after membership change")
}
renamedRole := updatedRole
renamedRole.Name = "reader-new"
affected, err = UpdateRole(updatedRole.GetId(), &renamedRole)
if err != nil {
t.Fatalf("UpdateRole() for rename error: %v", err)
}
if !affected {
t.Fatalf("expected UpdateRole rename to affect rows")
}
updatedPermission, err := GetPermission(permission.GetId())
if err != nil {
t.Fatalf("GetPermission() error: %v", err)
}
if len(updatedPermission.Roles) != 1 || updatedPermission.Roles[0] != renamedRole.GetId() {
t.Fatalf("expected permission role reference to be renamed")
}
rulesAfterRename := getPermissionRulesByPermissionID(t, permission.GetId())
if len(rulesAfterRename) != 1 || rulesAfterRename[0].Ptype != "p" || rulesAfterRename[0].V0 != renamedRole.GetId() {
t.Fatalf("expected rename to rebuild persisted p rule with new role id, got %+v", rulesAfterRename)
}
allowed, err = Enforce(updatedPermission, []string{owner + "/bob", "data1", "read"})
if err != nil {
t.Fatalf("Enforce() for bob after rename error: %v", err)
}
if !allowed {
t.Fatalf("expected bob to stay allowed after role rename")
}
}
// issue 5346
func TestPermissionEnforcerDeduplicatesRuntimeGroupingPoliciesAcross1000Permissions(t *testing.T) {
owner := newPermissionRbacTestOwner(t)
const (
permissionCount = 1000
userCount = 1000
)
users := make([]string, 0, userCount)
for i := range userCount {
users = append(users, fmt.Sprintf("%s/user-%04d", owner, i))
}
role := &Role{
Owner: owner,
Name: "shared-role",
Users: users,
}
affected, err := AddRole(role)
if err != nil {
t.Fatalf("AddRole() error: %v", err)
}
if !affected {
t.Fatalf("expected AddRole to affect rows")
}
permissions := make([]*Permission, 0, permissionCount)
permissionIDs := make([]string, 0, permissionCount)
for i := 0; i < permissionCount; i++ {
permission := newTestPermission(owner, fmt.Sprintf("perm-%04d", i), role.GetId())
permissions = append(permissions, permission)
permissionIDs = append(permissionIDs, permission.GetId())
}
affected, err = AddPermissions(permissions)
if err != nil {
t.Fatalf("AddPermissions() error: %v", err)
}
if !affected {
t.Fatalf("expected AddPermissions to affect rows")
}
enforcer, err := getPermissionEnforcer(permissions[0], permissionIDs...)
if err != nil {
t.Fatalf("getPermissionEnforcer() error: %v", err)
}
if len(enforcer.GetPolicy()) != permissionCount {
t.Fatalf("expected %d p rules in merged enforcer, got %d", permissionCount, len(enforcer.GetPolicy()))
}
if len(enforcer.GetGroupingPolicy()) != userCount {
t.Fatalf("expected deduplicated runtime g rules to stay at %d, got %d", userCount, len(enforcer.GetGroupingPolicy()))
}
allowed, err := enforcer.Enforce(users[userCount-1], "data1", "read")
if err != nil {
t.Fatalf("Enforce() in 1000x1000 scenario error: %v", err)
}
if !allowed {
t.Fatalf("expected last user to be allowed in 1000x1000 scenario")
}
}

View File

@@ -98,40 +98,23 @@ func UpdateRole(id string, role *Role) (bool, error) {
return false, nil
}
visited := map[string]struct{}{}
permissions, err := GetPermissionsByRole(id)
if err != nil {
return false, err
}
for _, permission := range permissions {
removeGroupingPolicies(permission)
removePolicies(permission)
visited[permission.GetId()] = struct{}{}
}
ancestorRoles, err := GetAncestorRoles(id)
if err != nil {
return false, err
}
for _, r := range ancestorRoles {
permissions, err := GetPermissionsByRole(r.GetId())
renameRole := name != role.Name
oldPermissions := []*Permission{}
if renameRole {
oldPermissions, err = GetPermissionsByRole(id)
if err != nil {
return false, err
}
for _, permission := range permissions {
permissionId := permission.GetId()
if _, ok := visited[permissionId]; !ok {
removeGroupingPolicies(permission)
visited[permissionId] = struct{}{}
for _, permission := range oldPermissions {
err = removePolicies(permission)
if err != nil {
return false, err
}
}
}
if name != role.Name {
if renameRole {
err := roleChangeTrigger(name, role.Name)
if err != nil {
return false, err
@@ -143,47 +126,16 @@ func UpdateRole(id string, role *Role) (bool, error) {
return false, err
}
visited = map[string]struct{}{}
newRoleID := role.GetId()
permissions, err = GetPermissionsByRole(newRoleID)
if err != nil {
return false, err
}
for _, permission := range permissions {
err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
err = addPolicies(permission)
if err != nil {
return false, err
}
visited[permission.GetId()] = struct{}{}
}
ancestorRoles, err = GetAncestorRoles(newRoleID)
if err != nil {
return false, err
}
for _, r := range ancestorRoles {
permissions, err := GetPermissionsByRole(r.GetId())
if renameRole && affected != 0 {
permissions, err := GetPermissionsByRole(role.GetId())
if err != nil {
return false, err
}
for _, permission := range permissions {
permissionId := permission.GetId()
if _, ok := visited[permissionId]; !ok {
err = addGroupingPolicies(permission)
if err != nil {
return false, err
}
visited[permissionId] = struct{}{}
err = addPolicies(permission)
if err != nil {
return false, err
}
}
}