Skip to content

Commit dce75de

Browse files
committed
authz: Add onPolicyUpdate callback to file watcher.
Signed-off-by: Keith Collister <kcollister@google.com>
1 parent 18a8d3d commit dce75de

2 files changed

Lines changed: 79 additions & 9 deletions

File tree

authz/grpc_authz_server_interceptors.go

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -92,24 +92,41 @@ func (i *StaticInterceptor) StreamInterceptor(srv any, ss grpc.ServerStream, _ *
9292
// FileWatcherInterceptor contains details used to make authorization decisions
9393
// by watching a file path that contains authorization policy in JSON format.
9494
type FileWatcherInterceptor struct {
95+
options FileWatcherOptions
9596
internalInterceptor unsafe.Pointer // *StaticInterceptor
96-
policyFile string
9797
policyContents []byte
98-
refreshDuration time.Duration
9998
cancel context.CancelFunc
10099
}
101100

101+
// FileWatcherOptions contains configuration options for the
102+
// FileWatcherInterceptor.
103+
type FileWatcherOptions struct {
104+
// PolicyFile contains a JSON string of the authorization policy.
105+
PolicyFile string
106+
// RefreshDuration is the delay between policy refreshes.
107+
RefreshDuration time.Duration
108+
// OnPolicyUpdate is a callback to be invoked when a policy is
109+
// loaded/updated. The loaded policy string is passed as an argument.
110+
OnPolicyUpdate func(string)
111+
}
112+
102113
// NewFileWatcher returns a new FileWatcherInterceptor from a policy file
103114
// that contains JSON string of authorization policy and a refresh duration to
104115
// specify the amount of time between policy refreshes.
105116
func NewFileWatcher(file string, duration time.Duration) (*FileWatcherInterceptor, error) {
106-
if file == "" {
117+
return NewFileWatcherWithOptions(FileWatcherOptions{PolicyFile: file, RefreshDuration: duration, OnPolicyUpdate: nil})
118+
}
119+
120+
// NewFileWatcherWithOptions returns a new FileWatcherInterceptor from a set of
121+
// options.
122+
func NewFileWatcherWithOptions(options FileWatcherOptions) (*FileWatcherInterceptor, error) {
123+
if options.PolicyFile == "" {
107124
return nil, fmt.Errorf("authorization policy file path is empty")
108125
}
109-
if duration <= time.Duration(0) {
110-
return nil, fmt.Errorf("requires refresh interval(%v) greater than 0s", duration)
126+
if options.RefreshDuration <= time.Duration(0) {
127+
return nil, fmt.Errorf("requires refresh interval(%v) greater than 0s", options.RefreshDuration)
111128
}
112-
i := &FileWatcherInterceptor{policyFile: file, refreshDuration: duration}
129+
i := &FileWatcherInterceptor{options: options}
113130
if err := i.updateInternalInterceptor(); err != nil {
114131
return nil, err
115132
}
@@ -121,7 +138,7 @@ func NewFileWatcher(file string, duration time.Duration) (*FileWatcherIntercepto
121138
}
122139

123140
func (i *FileWatcherInterceptor) run(ctx context.Context) {
124-
ticker := time.NewTicker(i.refreshDuration)
141+
ticker := time.NewTicker(i.options.RefreshDuration)
125142
for {
126143
if err := i.updateInternalInterceptor(); err != nil {
127144
logger.Warningf("authorization policy reload status err: %v", err)
@@ -140,9 +157,9 @@ func (i *FileWatcherInterceptor) run(ctx context.Context) {
140157
// constructor, if there is an error in reading the file or parsing the policy, the
141158
// previous internalInterceptors will not be replaced.
142159
func (i *FileWatcherInterceptor) updateInternalInterceptor() error {
143-
policyContents, err := os.ReadFile(i.policyFile)
160+
policyContents, err := os.ReadFile(i.options.PolicyFile)
144161
if err != nil {
145-
return fmt.Errorf("policyFile(%s) read failed: %v", i.policyFile, err)
162+
return fmt.Errorf("policyFile(%s) read failed: %v", i.options.PolicyFile, err)
146163
}
147164
if bytes.Equal(i.policyContents, policyContents) {
148165
return nil
@@ -155,6 +172,9 @@ func (i *FileWatcherInterceptor) updateInternalInterceptor() error {
155172
}
156173
atomic.StorePointer(&i.internalInterceptor, unsafe.Pointer(interceptor))
157174
logger.Infof("authorization policy reload status: successfully loaded new policy %v", policyContentsString)
175+
if i.options.OnPolicyUpdate != nil {
176+
i.options.OnPolicyUpdate(policyContentsString)
177+
}
158178
return nil
159179
}
160180

authz/grpc_authz_server_interceptors_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package authz_test
2020

2121
import (
22+
"context"
2223
"fmt"
2324
"os"
2425
"path"
@@ -28,6 +29,8 @@ import (
2829
"google.golang.org/grpc/authz"
2930
)
3031

32+
const defaultTestTimeout = 10 * time.Second
33+
3134
func createTmpPolicyFile(t *testing.T, dirSuffix string, policy []byte) string {
3235
t.Helper()
3336

@@ -118,3 +121,50 @@ func (s) TestNewFileWatcher(t *testing.T) {
118121
})
119122
}
120123
}
124+
125+
func (s) TestOnPolicyUpdate(t *testing.T) {
126+
ctx, cancel := context.WithTimeout(t.Context(), defaultTestTimeout)
127+
defer cancel()
128+
129+
updates := make(chan string, 10)
130+
onPolicyUpdate := func(s string) {
131+
updates <- s
132+
}
133+
134+
content := `{"name": "foo1", "allow_rules":[{"name":"bar"}]}`
135+
file := createTmpPolicyFile(t, "onpolicyupdate", []byte(content))
136+
i, err := authz.NewFileWatcherWithOptions(authz.FileWatcherOptions{PolicyFile: file, RefreshDuration: time.Millisecond, OnPolicyUpdate: onPolicyUpdate})
137+
if err != nil {
138+
t.Fatalf("NewFileWatcherWithCallback() returned err: %v", err)
139+
}
140+
defer i.Close()
141+
142+
select {
143+
case <-ctx.Done():
144+
t.Fatalf("timeout waiting for policy update")
145+
case update := <-updates:
146+
if update != content {
147+
t.Fatalf("unexpected contents on first load of policy file: got=%v, want=%v", update, content)
148+
}
149+
}
150+
151+
// Tweak the file, expect an update.
152+
content = `{"name": "foo2", "allow_rules":[{"name":"bar"}]}`
153+
if err := os.WriteFile(file, []byte(content), os.ModePerm); err != nil {
154+
t.Fatalf("os.WriteFile(%q) failed: %v", file, err)
155+
}
156+
157+
select {
158+
case <-ctx.Done():
159+
t.Fatalf("timeout waiting for policy update")
160+
case update := <-updates:
161+
if update != content {
162+
t.Fatalf("unexpected contents after policy file changed: got=%v, want=%v", update, content)
163+
}
164+
}
165+
166+
close(updates)
167+
if len(updates) != 0 {
168+
t.Fatalf("expected exactly 2 updates in channel")
169+
}
170+
}

0 commit comments

Comments
 (0)