Skip to content

Commit 42d3262

Browse files
authored
fix(authZ): propagate new policies across replicas (#484)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent ae39ae6 commit 42d3262

File tree

7 files changed

+121
-34
lines changed

7 files changed

+121
-34
lines changed

app/controlplane/internal/authz/authz.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,18 @@
1717
package authz
1818

1919
import (
20+
"context"
2021
"errors"
2122
"fmt"
2223

2324
_ "embed"
2425

26+
psqlwatcher "github.com/IguteChung/casbin-psql-watcher"
2527
"github.com/casbin/casbin/v2"
2628
"github.com/casbin/casbin/v2/model"
2729
"github.com/casbin/casbin/v2/persist"
2830
fileadapter "github.com/casbin/casbin/v2/persist/file-adapter"
31+
2932
entadapter "github.com/casbin/ent-adapter"
3033
"github.com/chainloop-dev/chainloop/app/controlplane/internal/conf"
3134
)
@@ -123,6 +126,25 @@ func NewDatabaseEnforcer(c *conf.Data_Database) (*Enforcer, error) {
123126
return nil, fmt.Errorf("failed to create enforcer: %w", err)
124127
}
125128

129+
// watch for policy changes in database and update enforcer
130+
w, err := psqlwatcher.NewWatcherWithConnString(context.Background(), c.Source, psqlwatcher.Option{})
131+
if err != nil {
132+
return nil, fmt.Errorf("failed to create watcher: %w", err)
133+
}
134+
135+
if err = e.SetWatcher(w); err != nil {
136+
return nil, fmt.Errorf("failed to set watcher: %w", err)
137+
}
138+
139+
if err = w.SetUpdateCallback(func(string) {
140+
// When there is a change in the policy, we load the in-memory policy for the current enforcer
141+
if err := e.LoadPolicy(); err != nil {
142+
fmt.Printf("failed to load policy: %v", err)
143+
}
144+
}); err != nil {
145+
return nil, fmt.Errorf("failed to set update callback: %w", err)
146+
}
147+
126148
return e, nil
127149
}
128150

app/controlplane/internal/authz/authz_test.go

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,18 @@
1414
// limitations under the License.
1515

1616
// Authorization package
17-
package authz
17+
package authz_test
1818

1919
import (
2020
"fmt"
2121
"io"
2222
"os"
2323
"testing"
24+
"time"
2425

26+
"github.com/cenkalti/backoff/v4"
27+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/authz"
28+
"github.com/chainloop-dev/chainloop/app/controlplane/internal/biz/testhelpers"
2529
"github.com/google/uuid"
2630
"github.com/stretchr/testify/assert"
2731
"github.com/stretchr/testify/require"
@@ -30,8 +34,8 @@ import (
3034
func TestAddPolicies(t *testing.T) {
3135
testcases := []struct {
3236
name string
33-
subject *SubjectAPIToken
34-
policies []*Policy
37+
subject *authz.SubjectAPIToken
38+
policies []*authz.Policy
3539
wantErr bool
3640
wantNumberPolicies int
3741
}{
@@ -41,38 +45,38 @@ func TestAddPolicies(t *testing.T) {
4145
},
4246
{
4347
name: "no subject",
44-
policies: []*Policy{
45-
PolicyWorkflowContractList,
48+
policies: []*authz.Policy{
49+
authz.PolicyWorkflowContractList,
4650
},
4751
wantErr: true,
4852
},
4953
{
5054
name: "no policies",
51-
subject: &SubjectAPIToken{ID: uuid.NewString()},
55+
subject: &authz.SubjectAPIToken{ID: uuid.NewString()},
5256
wantErr: true,
5357
},
5458
{
5559
name: "adds two policies",
56-
subject: &SubjectAPIToken{ID: uuid.NewString()},
57-
policies: []*Policy{
58-
PolicyWorkflowContractList,
59-
PolicyReferrerRead,
60+
subject: &authz.SubjectAPIToken{ID: uuid.NewString()},
61+
policies: []*authz.Policy{
62+
authz.PolicyWorkflowContractList,
63+
authz.PolicyReferrerRead,
6064
},
6165
wantNumberPolicies: 2,
6266
},
6367
{
6468
name: "handles duplicated policies",
65-
subject: &SubjectAPIToken{
69+
subject: &authz.SubjectAPIToken{
6670
ID: uuid.NewString(),
6771
},
68-
policies: []*Policy{
69-
PolicyWorkflowContractList,
70-
PolicyWorkflowContractRead,
71-
PolicyWorkflowContractUpdate,
72-
PolicyWorkflowContractList,
73-
PolicyArtifactDownload,
74-
PolicyWorkflowContractUpdate,
75-
PolicyArtifactDownload,
72+
policies: []*authz.Policy{
73+
authz.PolicyWorkflowContractList,
74+
authz.PolicyWorkflowContractRead,
75+
authz.PolicyWorkflowContractUpdate,
76+
authz.PolicyWorkflowContractList,
77+
authz.PolicyArtifactDownload,
78+
authz.PolicyWorkflowContractUpdate,
79+
authz.PolicyArtifactDownload,
7680
},
7781
wantNumberPolicies: 4,
7882
},
@@ -103,22 +107,22 @@ func TestAddPolicies(t *testing.T) {
103107
}
104108

105109
func TestAddPoliciesDuplication(t *testing.T) {
106-
want := []*Policy{
107-
PolicyWorkflowContractList,
108-
PolicyWorkflowContractRead,
110+
want := []*authz.Policy{
111+
authz.PolicyWorkflowContractList,
112+
authz.PolicyWorkflowContractRead,
109113
}
110114

111115
enforcer, closer := testEnforcer(t)
112116
defer closer.Close()
113-
sub := &SubjectAPIToken{ID: uuid.NewString()}
117+
sub := &authz.SubjectAPIToken{ID: uuid.NewString()}
114118

115119
err := enforcer.AddPolicies(sub, want...)
116120
require.NoError(t, err)
117121
got := enforcer.GetFilteredPolicy(0, sub.String())
118122
assert.Len(t, got, 2)
119123

120124
// Update the list of policies we want to add by appending an extra one
121-
want = append(want, PolicyWorkflowContractUpdate)
125+
want = append(want, authz.PolicyWorkflowContractUpdate)
122126
// AddPolicies only add the policies that are not already present preventing duplication
123127
err = enforcer.AddPolicies(sub, want...)
124128
assert.NoError(t, err)
@@ -127,15 +131,15 @@ func TestAddPoliciesDuplication(t *testing.T) {
127131
}
128132

129133
func TestClearPolicies(t *testing.T) {
130-
want := []*Policy{
131-
PolicyWorkflowContractList,
132-
PolicyWorkflowContractRead,
134+
want := []*authz.Policy{
135+
authz.PolicyWorkflowContractList,
136+
authz.PolicyWorkflowContractRead,
133137
}
134138

135139
enforcer, closer := testEnforcer(t)
136140
defer closer.Close()
137-
sub := &SubjectAPIToken{ID: uuid.NewString()}
138-
sub2 := &SubjectAPIToken{ID: uuid.NewString()}
141+
sub := &authz.SubjectAPIToken{ID: uuid.NewString()}
142+
sub2 := &authz.SubjectAPIToken{ID: uuid.NewString()}
139143

140144
// Create policies for two different subjects
141145
err := enforcer.AddPolicies(sub, want...)
@@ -160,13 +164,68 @@ func TestClearPolicies(t *testing.T) {
160164
assert.Len(t, got, 2)
161165
}
162166

163-
func testEnforcer(t *testing.T) (*Enforcer, io.Closer) {
167+
func testEnforcer(t *testing.T) (*authz.Enforcer, io.Closer) {
164168
f, err := os.CreateTemp(t.TempDir(), "policy*.csv")
165169
if err != nil {
166170
require.FailNow(t, err.Error())
167171
}
168172

169-
enforcer, err := NewFiletypeEnforcer(f.Name())
173+
enforcer, err := authz.NewFiletypeEnforcer(f.Name())
170174
require.NoError(t, err)
171175
return enforcer, f
172176
}
177+
178+
func TestMultiReplicaPropagation(t *testing.T) {
179+
// Create two enforcers that share the same database
180+
db := testhelpers.NewTestDatabase(t)
181+
defer db.Close(t)
182+
183+
enforcerA, err := authz.NewDatabaseEnforcer(testhelpers.NewConfData(db, t).Database)
184+
require.NoError(t, err)
185+
enforcerB, err := authz.NewDatabaseEnforcer(testhelpers.NewConfData(db, t).Database)
186+
require.NoError(t, err)
187+
188+
// Subject and policies to add
189+
sub := &authz.SubjectAPIToken{ID: uuid.NewString()}
190+
want := []*authz.Policy{authz.PolicyWorkflowContractList, authz.PolicyWorkflowContractRead}
191+
192+
// Create policies in one enforcer
193+
err = enforcerA.AddPolicies(sub, want...)
194+
require.NoError(t, err)
195+
196+
// Make sure it propagates to the other one
197+
got := enforcerA.GetFilteredPolicy(0, sub.String())
198+
assert.Len(t, got, 2)
199+
200+
// it might take a bit for the policies to propagate to the other enforcer
201+
err = fnWithRetry(func() error {
202+
got = enforcerB.GetFilteredPolicy(0, sub.String())
203+
if len(got) == 2 {
204+
return nil
205+
}
206+
return fmt.Errorf("policies not propagated yet")
207+
})
208+
require.NoError(t, err)
209+
assert.Len(t, got, 2)
210+
211+
// Then delete them from the second one and check propagation again
212+
require.NoError(t, enforcerB.ClearPolicies(sub))
213+
assert.Len(t, enforcerB.GetFilteredPolicy(0, sub.String()), 0)
214+
215+
// Make sure it propagates to the other one
216+
err = fnWithRetry(func() error {
217+
got = enforcerA.GetFilteredPolicy(0, sub.String())
218+
if len(got) == 0 {
219+
return nil
220+
}
221+
222+
return fmt.Errorf("policies not propagated yet")
223+
})
224+
require.NoError(t, err)
225+
assert.Len(t, enforcerA.GetFilteredPolicy(0, sub.String()), 0)
226+
}
227+
228+
func fnWithRetry(f func() error) error {
229+
// Max 1 seconds
230+
return backoff.Retry(f, backoff.WithMaxRetries(backoff.NewConstantBackOff(100*time.Millisecond), 10))
231+
}

app/controlplane/internal/biz/testhelpers/database.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func (db *TestDatabase) ConnectionString(t *testing.T) string {
179179
return fmt.Sprintf("postgres://postgres:[email protected]:%d/postgres", db.Port(t))
180180
}
181181

182-
func newConfData(db *TestDatabase, t *testing.T) *conf.Data {
182+
func NewConfData(db *TestDatabase, t *testing.T) *conf.Data {
183183
return &conf.Data{Database: &conf.Data_Database{Driver: "pgx", Source: db.ConnectionString(t)}}
184184
}
185185

app/controlplane/internal/biz/testhelpers/wire.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func WireTestData(*TestDatabase, *testing.T, log.Logger, credentials.ReaderWrite
4444
wire.Value(&conf.ReferrerSharedIndex{}),
4545
wire.Struct(new(TestingUseCases), "*"),
4646
wire.Struct(new(TestingRepos), "*"),
47-
newConfData,
47+
NewConfData,
4848
authz.NewDatabaseEnforcer,
4949
wire.FieldsOf(new(*conf.Data), "Database"),
5050
),

app/controlplane/internal/biz/testhelpers/wire_gen.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ require (
116116
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
117117
github.com/jackc/pgtype v1.14.0 // indirect
118118
github.com/jackc/pgx/v4 v4.18.1 // indirect
119+
github.com/jackc/puddle/v2 v2.2.1 // indirect
119120
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
120121
github.com/jmespath/go-jmespath v0.4.0 // indirect
121122
github.com/kevinburke/ssh_config v1.2.0 // indirect
@@ -151,6 +152,7 @@ require (
151152
cloud.google.com/go/iam v1.1.4 // indirect
152153
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.12.0
153154
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
155+
github.com/IguteChung/casbin-psql-watcher v1.0.0
154156
github.com/Microsoft/go-winio v0.6.1 // indirect
155157
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
156158
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ github.com/CycloneDX/cyclonedx-go v0.8.0/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7B
115115
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
116116
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
117117
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
118+
github.com/IguteChung/casbin-psql-watcher v1.0.0 h1:GO5RvdHq5WZfuKt03Frk4/SvYvikMI5V1zjSt1P1suM=
119+
github.com/IguteChung/casbin-psql-watcher v1.0.0/go.mod h1:mwQYBxiYGO05U8vogls8gf3KnOmdlc7GOLX7tS/V2TU=
118120
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
119121
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
120122
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
@@ -855,6 +857,8 @@ github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0f
855857
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
856858
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
857859
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
860+
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
861+
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
858862
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
859863
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
860864
github.com/jedib0t/go-pretty/v6 v6.4.7 h1:lwiTJr1DEkAgzljsUsORmWsVn5MQjt1BPJdPCtJ6KXE=

0 commit comments

Comments
 (0)