Skip to content

Commit 93c6899

Browse files
committed
added test cases
1 parent 8044ac5 commit 93c6899

1 file changed

Lines changed: 170 additions & 0 deletions

File tree

accesscontrol/casbin_rbac_test.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package accesscontrol
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync"
7+
"testing"
8+
9+
"github.com/casbin/casbin/v3"
10+
casbinModel "github.com/casbin/casbin/v3/model"
11+
"github.com/casbin/casbin/v3/persist"
12+
"github.com/l3montree-dev/devguard/shared"
13+
)
14+
15+
// noopAdapter is a minimal adapter for tests. Policy rules are kept
16+
// in-memory by casbin's model layer; this adapter only satisfies the interface.
17+
type noopAdapter struct{}
18+
19+
func (noopAdapter) LoadPolicy(_ casbinModel.Model) error { return nil }
20+
func (noopAdapter) SavePolicy(_ casbinModel.Model) error { return nil }
21+
func (noopAdapter) AddPolicy(_, _ string, _ []string) error { return nil }
22+
func (noopAdapter) RemovePolicy(_, _ string, _ []string) error { return nil }
23+
func (noopAdapter) RemoveFilteredPolicy(_, _ string, _ int, _ ...string) error { return nil }
24+
func (noopAdapter) LoadPolicyCtx(_ context.Context, _ casbinModel.Model) error { return nil }
25+
func (noopAdapter) SavePolicyCtx(_ context.Context, _ casbinModel.Model) error { return nil }
26+
func (noopAdapter) AddPolicyCtx(_ context.Context, _, _ string, _ []string) error { return nil }
27+
func (noopAdapter) RemovePolicyCtx(_ context.Context, _, _ string, _ []string) error { return nil }
28+
func (noopAdapter) RemoveFilteredPolicyCtx(_ context.Context, _, _ string, _ int, _ ...string) error {
29+
return nil
30+
}
31+
32+
var _ persist.ContextAdapter = noopAdapter{}
33+
34+
// testModelText mirrors config/rbac_model.conf so tests are self-contained.
35+
const testModelText = `
36+
[request_definition]
37+
r = sub, dom, obj, act
38+
39+
[policy_definition]
40+
p = sub, dom, obj, act
41+
42+
[role_definition]
43+
g = _, _, _
44+
45+
[policy_effect]
46+
e = some(where (p.eft == allow))
47+
48+
[matchers]
49+
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
50+
`
51+
52+
func newTestEnforcer(t *testing.T) *casbin.ContextEnforcer {
53+
t.Helper()
54+
m, err := casbinModel.NewModelFromString(testModelText)
55+
if err != nil {
56+
t.Fatalf("create casbin model: %v", err)
57+
}
58+
iface, err := casbin.NewContextEnforcer(m, noopAdapter{})
59+
if err != nil {
60+
t.Fatalf("create casbin enforcer: %v", err)
61+
}
62+
return iface.(*casbin.ContextEnforcer)
63+
}
64+
65+
func newTestCasbinRBAC(t *testing.T, domain string) *casbinRBAC {
66+
t.Helper()
67+
return &casbinRBAC{domain: domain, enforcer: newTestEnforcer(t)}
68+
}
69+
70+
// TestCasbinRBAC_ConcurrentWrites is a regression test for the panic:
71+
// "fatal error: concurrent map read and map write" inside casbin's Model.RemovePolicy.
72+
// Before the casbinMu fix, concurrent goroutines (e.g. different users triggering sync
73+
// via ErrGroup) wrote to casbin's internal policy maps without any synchronisation.
74+
func TestCasbinRBAC_ConcurrentWrites(t *testing.T) {
75+
rbac := newTestCasbinRBAC(t, "org-1")
76+
77+
const goroutines = 30
78+
var wg sync.WaitGroup
79+
wg.Add(goroutines)
80+
81+
for i := 0; i < goroutines; i++ {
82+
i := i
83+
go func() {
84+
defer wg.Done()
85+
user := fmt.Sprintf("user-%d", i)
86+
project := fmt.Sprintf("project-%d", i%5)
87+
_ = rbac.GrantRoleInProject(context.Background(), user, shared.RoleMember, project)
88+
_ = rbac.RevokeRoleInProject(context.Background(), user, shared.RoleMember, project)
89+
}()
90+
}
91+
wg.Wait()
92+
}
93+
94+
func TestCasbinRBAC_ConcurrentReads(t *testing.T) {
95+
rbac := newTestCasbinRBAC(t, "org-1")
96+
97+
// Seed some data first.
98+
for i := 0; i < 5; i++ {
99+
_ = rbac.GrantRoleInProject(context.Background(), fmt.Sprintf("user-%d", i), shared.RoleMember, "project-0")
100+
}
101+
102+
const goroutines = 30
103+
var wg sync.WaitGroup
104+
wg.Add(goroutines)
105+
106+
for i := 0; i < goroutines; i++ {
107+
i := i
108+
go func() {
109+
defer wg.Done()
110+
user := fmt.Sprintf("user-%d", i%5)
111+
_ = rbac.GetAllRoles(user)
112+
_, _ = rbac.GetAllProjectsForUser(user)
113+
}()
114+
}
115+
wg.Wait()
116+
}
117+
118+
func TestCasbinRBAC_ConcurrentReadsAndWrites(t *testing.T) {
119+
rbac := newTestCasbinRBAC(t, "org-1")
120+
121+
const goroutines = 40
122+
var wg sync.WaitGroup
123+
wg.Add(goroutines)
124+
125+
for i := 0; i < goroutines; i++ {
126+
i := i
127+
go func() {
128+
defer wg.Done()
129+
user := fmt.Sprintf("user-%d", i%10)
130+
project := fmt.Sprintf("project-%d", i%3)
131+
if i%2 == 0 {
132+
_ = rbac.GrantRoleInProject(context.Background(), user, shared.RoleMember, project)
133+
} else {
134+
_ = rbac.GetAllRoles(user)
135+
_, _ = rbac.GetAllProjectsForUser(user)
136+
}
137+
}()
138+
}
139+
wg.Wait()
140+
}
141+
142+
// TestCasbinRBAC_TwoUsersConcurrentOrgSync mirrors the exact scenario from the panic:
143+
// two users trigger RefreshExternalEntityProviderProjects for the same org simultaneously.
144+
// singleflight deduplicates per org+user, so both goroutines run concurrently and both
145+
// call casbin write operations on the shared enforcer.
146+
func TestCasbinRBAC_TwoUsersConcurrentOrgSync(t *testing.T) {
147+
// Both users share the same enforcer (same org, different singleflight keys).
148+
sharedEnforcer := newTestEnforcer(t)
149+
150+
user1rbac := &casbinRBAC{domain: "org-1", enforcer: sharedEnforcer}
151+
user2rbac := &casbinRBAC{domain: "org-1", enforcer: sharedEnforcer}
152+
153+
projects := []string{"proj-a", "proj-b", "proj-c", "proj-d", "proj-e"}
154+
155+
var wg sync.WaitGroup
156+
for _, rbac := range []*casbinRBAC{user1rbac, user2rbac} {
157+
rbac := rbac
158+
wg.Add(1)
159+
go func() {
160+
defer wg.Done()
161+
// Simulate what syncProjectsAndAssets does: grant + read roles per project.
162+
for _, project := range projects {
163+
_ = rbac.GrantRoleInProject(context.Background(), "user", shared.RoleMember, project)
164+
_ = rbac.GetAllRoles("user")
165+
_ = rbac.RevokeAllRolesInProjectForUser(context.Background(), "user", project)
166+
}
167+
}()
168+
}
169+
wg.Wait()
170+
}

0 commit comments

Comments
 (0)