File: enforcer_cached_gfunction_test.go

package info (click to toggle)
golang-github-casbin-casbin 3.10.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,756 kB
  • sloc: makefile: 14
file content (201 lines) | stat: -rw-r--r-- 7,330 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// 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"

// TestCachedGFunctionAfterAddingGroupingPolicy tests that adding new grouping policies
// at runtime correctly updates permission evaluation without requiring a restart or manual cache clearing.
// This is a regression test for the issue where GenerateGFunction's memoization cache
// caused stale permission results after adding new grouping policies.
func TestCachedGFunctionAfterAddingGroupingPolicy(t *testing.T) {
	e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
	if err != nil {
		t.Fatalf("Failed to create enforcer: %v", err)
	}

	// Initial state: bob should not have access to data2 (read)
	// bob has no roles initially
	ok, err := e.Enforce("bob", "data2", "read")
	if err != nil {
		t.Fatalf("Enforce failed: %v", err)
	}
	if ok {
		t.Error("bob should not have read access to data2 initially")
	}

	// Add a new grouping policy: bob becomes data2_admin
	// data2_admin has read and write access to data2
	_, err = e.AddGroupingPolicy("bob", "data2_admin")
	if err != nil {
		t.Fatalf("Failed to add grouping policy: %v", err)
	}

	// Now bob should have read access to data2 through the data2_admin role
	// This should work immediately without needing to clear cache or restart
	ok, err = e.Enforce("bob", "data2", "read")
	if err != nil {
		t.Fatalf("Enforce failed after adding grouping policy: %v", err)
	}
	if !ok {
		t.Error("bob should have read access to data2 after being added to data2_admin role")
	}

	// Also verify write access works
	ok, err = e.Enforce("bob", "data2", "write")
	if err != nil {
		t.Fatalf("Enforce failed: %v", err)
	}
	if !ok {
		t.Error("bob should have write access to data2 after being added to data2_admin role")
	}
}

// TestCachedGFunctionWithMultipleEnforceCalls tests that the g() function cache
// properly invalidates when grouping policies change, even after multiple enforce calls.
func TestCachedGFunctionWithMultipleEnforceCalls(t *testing.T) {
	e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
	if err != nil {
		t.Fatalf("Failed to create enforcer: %v", err)
	}

	// Make multiple enforce calls to ensure the g() function closure is cached
	for i := 0; i < 5; i++ {
		ok, enforceErr := e.Enforce("charlie", "data2", "read")
		if enforceErr != nil {
			t.Fatalf("Enforce failed on iteration %d: %v", i, enforceErr)
		}
		if ok {
			t.Errorf("charlie should not have read access to data2 on iteration %d", i)
		}
	}

	// Add grouping policy
	_, err = e.AddGroupingPolicy("charlie", "data2_admin")
	if err != nil {
		t.Fatalf("Failed to add grouping policy: %v", err)
	}

	// Immediately verify the change took effect
	ok, err := e.Enforce("charlie", "data2", "read")
	if err != nil {
		t.Fatalf("Enforce failed after adding grouping policy: %v", err)
	}
	if !ok {
		t.Error("charlie should have read access to data2 immediately after being added to data2_admin role")
	}

	// Make multiple calls to ensure it stays consistent
	for i := 0; i < 5; i++ {
		ok, enforceErr := e.Enforce("charlie", "data2", "read")
		if enforceErr != nil {
			t.Fatalf("Enforce failed on iteration %d after policy change: %v", i, enforceErr)
		}
		if !ok {
			t.Errorf("charlie should have read access to data2 on iteration %d after policy change", i)
		}
	}
}

// TestCachedGFunctionAfterRemovingGroupingPolicy tests that removing grouping policies
// also properly invalidates the g() function cache.
func TestCachedGFunctionAfterRemovingGroupingPolicy(t *testing.T) {
	e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
	if err != nil {
		t.Fatalf("Failed to create enforcer: %v", err)
	}

	// alice initially has the data2_admin role
	ok, err := e.Enforce("alice", "data2", "read")
	if err != nil {
		t.Fatalf("Enforce failed: %v", err)
	}
	if !ok {
		t.Error("alice should have read access to data2 initially")
	}

	// Remove alice from data2_admin role
	_, err = e.RemoveGroupingPolicy("alice", "data2_admin")
	if err != nil {
		t.Fatalf("Failed to remove grouping policy: %v", err)
	}

	// Now alice should not have access to data2 (she only has access to data1)
	ok, err = e.Enforce("alice", "data2", "read")
	if err != nil {
		t.Fatalf("Enforce failed after removing grouping policy: %v", err)
	}
	if ok {
		t.Error("alice should not have read access to data2 after being removed from data2_admin role")
	}

	// Verify alice still has access to data1 (direct policy)
	ok, err = e.Enforce("alice", "data1", "read")
	if err != nil {
		t.Fatalf("Enforce failed: %v", err)
	}
	if !ok {
		t.Error("alice should still have read access to data1 (direct policy)")
	}
}

// TestCachedGFunctionAfterBuildRoleLinks tests the specific scenario mentioned in the bug report:
// adding grouping policies and calling BuildRoleLinks() manually should properly invalidate the cache.
func TestCachedGFunctionAfterBuildRoleLinks(t *testing.T) {
	e, err := NewEnforcer("examples/rbac_model.conf", "examples/rbac_policy.csv")
	if err != nil {
		t.Fatalf("Failed to create enforcer: %v", err)
	}

	// First, make some enforce calls to ensure the g() function closure is created and cached
	// This will cache "bob" NOT having data2_admin role in the g() function's sync.Map
	for i := 0; i < 3; i++ {
		ok, enforceErr := e.Enforce("bob", "data2", "read")
		if enforceErr != nil {
			t.Fatalf("Enforce failed on iteration %d: %v", i, enforceErr)
		}
		if ok {
			t.Errorf("bob should not have read access to data2 on iteration %d (before adding role)", i)
		}
	}

	// Disable autoBuildRoleLinks to manually control when role links are rebuilt
	e.EnableAutoBuildRoleLinks(false)

	// Manually add the grouping policy to the model (bypassing BuildIncrementalRoleLinks)
	// This simulates the scenario where policies are loaded from database
	err = e.model.AddPolicy("g", "g", []string{"bob", "data2_admin"})
	if err != nil {
		t.Fatalf("Failed to add grouping policy to model: %v", err)
	}

	// Manually build role links as mentioned in the issue
	// This is the key part - BuildRoleLinks() should invalidate the matcher map cache
	err = e.BuildRoleLinks()
	if err != nil {
		t.Fatalf("Failed to build role links: %v", err)
	}

	// Now bob should have read access to data2 through the data2_admin role
	// This is where the bug would manifest - if BuildRoleLinks() doesn't invalidate the cache,
	// the old g() function closure with "bob->data2_admin = false" cached will still be used
	ok, err := e.Enforce("bob", "data2", "read")
	if err != nil {
		t.Fatalf("Enforce failed after BuildRoleLinks: %v", err)
	}
	if !ok {
		t.Error("bob should have read access to data2 after BuildRoleLinks() - this indicates the g() function cache was not properly invalidated")
	}
}