Skip to content

Commit 3633dc6

Browse files
vault: batch smoke coverage and retry stale allowlists (#21717)
* vault: batch cross-namespace smoke coverage * vault: retry stale allowlist reads * vault: fix request authorizer lint
1 parent e1debcf commit 3633dc6

3 files changed

Lines changed: 250 additions & 129 deletions

File tree

core/capabilities/vault/request_authorizer.go

Lines changed: 58 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,14 @@ type requestAuthorizer struct {
2121
workflowRegistrySyncer workflowsyncerv2.WorkflowRegistrySyncer
2222
replayGuard *DigestReplayGuard
2323
lggr logger.Logger
24+
sleep func(time.Duration)
2425
}
2526

27+
const (
28+
allowlistReadRetryCount = 3
29+
allowlistReadRetryInterval = 3 * time.Second
30+
)
31+
2632
// AuthorizeRequest authorizes a request based on the request digest and the allowlisted requests.
2733
// It does NOT check if the request method is allowed.
2834
func (r *requestAuthorizer) AuthorizeRequest(ctx context.Context, req jsonrpc.Request[json.RawMessage]) (isAuthorized bool, owner string, err error) {
@@ -43,20 +49,8 @@ func (r *requestAuthorizer) AuthorizeRequest(ctx context.Context, req jsonrpc.Re
4349
r.lggr.Errorw("AuthorizeRequest workflowRegistrySyncer is nil", "method", req.Method, "requestID", req.ID)
4450
return false, "", errors.New("internal error: workflowRegistrySyncer is nil")
4551
}
46-
allowedRequests := r.workflowRegistrySyncer.GetAllowlistedRequests(ctx)
47-
allowedRequestsStrs := make([]string, 0, len(allowedRequests))
48-
for _, rr := range allowedRequests {
49-
allowedReqStr := fmt.Sprintf("Owner: %s, RequestDigest: %s, ExpiryTimestamp: %d", rr.Owner.Hex(), hex.EncodeToString(rr.RequestDigest[:]), rr.ExpiryTimestamp)
50-
allowedRequestsStrs = append(allowedRequestsStrs, allowedReqStr)
51-
}
52-
r.lggr.Infow("AuthorizeRequest GetAllowlistedRequests", "method", req.Method, "requestID", req.ID, "allowedRequests", allowedRequestsStrs)
53-
allowlistedRequest := r.fetchAllowlistedItem(allowedRequests, requestDigestBytes32)
52+
allowlistedRequest, _ := r.fetchAllowlistedItemWithRetry(ctx, req.Method, req.ID, requestDigest, requestDigestBytes32)
5453
if allowlistedRequest == nil {
55-
r.lggr.Infow("AuthorizeRequest fetchAllowlistedItem request not allowlisted",
56-
"method", req.Method,
57-
"requestID", req.ID,
58-
"digestHexStr", requestDigest,
59-
"allowedRequestsStrs", allowedRequestsStrs)
6054
return false, "", errors.New("request not allowlisted")
6155
}
6256

@@ -76,6 +70,56 @@ func (r *requestAuthorizer) AuthorizeRequest(ctx context.Context, req jsonrpc.Re
7670
return true, allowlistedRequest.Owner.Hex(), nil
7771
}
7872

73+
func (r *requestAuthorizer) fetchAllowlistedItemWithRetry(ctx context.Context, method string, requestID interface{}, requestDigest string, digest [32]byte) (*workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest, []string) {
74+
var allowedRequestsStrs []string
75+
for attempt := 0; attempt <= allowlistReadRetryCount; attempt++ {
76+
allowedRequests := r.workflowRegistrySyncer.GetAllowlistedRequests(ctx)
77+
allowedRequestsStrs = make([]string, 0, len(allowedRequests))
78+
for _, rr := range allowedRequests {
79+
allowedReqStr := fmt.Sprintf("Owner: %s, RequestDigest: %s, ExpiryTimestamp: %d", rr.Owner.Hex(), hex.EncodeToString(rr.RequestDigest[:]), rr.ExpiryTimestamp)
80+
allowedRequestsStrs = append(allowedRequestsStrs, allowedReqStr)
81+
}
82+
r.lggr.Infow("AuthorizeRequest GetAllowlistedRequests", "method", method, "requestID", requestID, "attempt", attempt+1, "allowedRequests", allowedRequestsStrs)
83+
84+
allowlistedRequest := r.fetchAllowlistedItem(allowedRequests, digest)
85+
if allowlistedRequest != nil {
86+
return allowlistedRequest, allowedRequestsStrs
87+
}
88+
89+
if attempt == allowlistReadRetryCount {
90+
break
91+
}
92+
93+
r.lggr.Warnw("AuthorizeRequest request not found in allowlist, retrying",
94+
"method", method,
95+
"requestID", requestID,
96+
"digestHexStr", requestDigest,
97+
"attempt", attempt+1,
98+
"retryInterval", allowlistReadRetryInterval,
99+
"allowedRequestsStrs", allowedRequestsStrs)
100+
101+
select {
102+
case <-ctx.Done():
103+
r.lggr.Warnw("AuthorizeRequest allowlist retry canceled",
104+
"method", method,
105+
"requestID", requestID,
106+
"digestHexStr", requestDigest,
107+
"attempt", attempt+1)
108+
return nil, allowedRequestsStrs
109+
default:
110+
}
111+
112+
r.sleep(allowlistReadRetryInterval)
113+
}
114+
115+
r.lggr.Infow("AuthorizeRequest fetchAllowlistedItem request not allowlisted",
116+
"method", method,
117+
"requestID", requestID,
118+
"digestHexStr", requestDigest,
119+
"allowedRequestsStrs", allowedRequestsStrs)
120+
return nil, allowedRequestsStrs
121+
}
122+
79123
func (r *requestAuthorizer) fetchAllowlistedItem(allowListedRequests []workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest, digest [32]byte) *workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest {
80124
for _, item := range allowListedRequests {
81125
if item.RequestDigest == digest {
@@ -90,5 +134,6 @@ func NewRequestAuthorizer(lggr logger.Logger, workflowRegistrySyncer workflowsyn
90134
workflowRegistrySyncer: workflowRegistrySyncer,
91135
lggr: logger.Named(lggr, "VaultRequestAuthorizer"),
92136
replayGuard: NewDigestReplayGuard(),
137+
sleep: time.Sleep,
93138
}
94139
}

core/capabilities/vault/request_authorizer_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package vault
22

33
import (
4+
"context"
45
"encoding/hex"
56
"encoding/json"
67
"testing"
@@ -202,3 +203,97 @@ func testAuthForRequests(t *testing.T, allowlistedRequest, notAllowlistedRequest
202203
require.False(t, isAuthorized)
203204
require.ErrorContains(t, err, "not allowlisted")
204205
}
206+
207+
func TestRequestAuthorizer_RetriesAllowlistReadsUntilDigestAppears(t *testing.T) {
208+
lggr := logger.TestLogger(t)
209+
owner := common.Address{1, 2, 3}
210+
req := makeListSecretsRequest(t, "123", "b")
211+
212+
digest, err := req.Digest()
213+
require.NoError(t, err)
214+
digestBytes, err := hex.DecodeString(digest)
215+
require.NoError(t, err)
216+
217+
allowlisted := []workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest{
218+
{
219+
RequestDigest: [32]byte(digestBytes),
220+
Owner: owner,
221+
ExpiryTimestamp: uint32(time.Now().UTC().Unix() + 100), //nolint:gosec // test fixture expiry is bounded and safe here
222+
},
223+
}
224+
225+
mockSyncer := syncerv2mocks.NewWorkflowRegistrySyncer(t)
226+
mockSyncer.On("GetAllowlistedRequests", mock.Anything).Return([]workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest{}).Once()
227+
mockSyncer.On("GetAllowlistedRequests", mock.Anything).Return([]workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest{}).Once()
228+
mockSyncer.On("GetAllowlistedRequests", mock.Anything).Return(allowlisted).Once()
229+
230+
auth := NewRequestAuthorizer(lggr, mockSyncer)
231+
sleepCalls := 0
232+
auth.sleep = func(d time.Duration) {
233+
require.Equal(t, allowlistReadRetryInterval, d)
234+
sleepCalls++
235+
}
236+
237+
isAuthorized, gotOwner, err := auth.AuthorizeRequest(t.Context(), req)
238+
require.True(t, isAuthorized, err)
239+
require.NoError(t, err)
240+
require.Equal(t, owner.Hex(), gotOwner)
241+
require.Equal(t, 2, sleepCalls)
242+
}
243+
244+
func TestRequestAuthorizer_FailsAfterAllowlistReadRetries(t *testing.T) {
245+
lggr := logger.TestLogger(t)
246+
req := makeListSecretsRequest(t, "123", "b")
247+
248+
mockSyncer := syncerv2mocks.NewWorkflowRegistrySyncer(t)
249+
mockSyncer.On("GetAllowlistedRequests", mock.Anything).Return([]workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest{}).Times(allowlistReadRetryCount + 1)
250+
251+
auth := NewRequestAuthorizer(lggr, mockSyncer)
252+
sleepCalls := 0
253+
auth.sleep = func(d time.Duration) {
254+
require.Equal(t, allowlistReadRetryInterval, d)
255+
sleepCalls++
256+
}
257+
258+
isAuthorized, _, err := auth.AuthorizeRequest(t.Context(), req)
259+
require.False(t, isAuthorized)
260+
require.ErrorContains(t, err, "not allowlisted")
261+
require.Equal(t, allowlistReadRetryCount, sleepCalls)
262+
}
263+
264+
func TestRequestAuthorizer_StopsRetriesWhenContextCanceled(t *testing.T) {
265+
lggr := logger.TestLogger(t)
266+
req := makeListSecretsRequest(t, "123", "b")
267+
268+
ctx, cancel := context.WithCancel(t.Context())
269+
cancel()
270+
271+
mockSyncer := syncerv2mocks.NewWorkflowRegistrySyncer(t)
272+
mockSyncer.On("GetAllowlistedRequests", mock.Anything).Return([]workflow_registry_wrapper_v2.WorkflowRegistryOwnerAllowlistedRequest{}).Once()
273+
274+
auth := NewRequestAuthorizer(lggr, mockSyncer)
275+
sleepCalls := 0
276+
auth.sleep = func(time.Duration) {
277+
sleepCalls++
278+
}
279+
280+
isAuthorized, _, err := auth.AuthorizeRequest(ctx, req)
281+
require.False(t, isAuthorized)
282+
require.ErrorContains(t, err, "not allowlisted")
283+
require.Zero(t, sleepCalls)
284+
}
285+
286+
func makeListSecretsRequest(t *testing.T, id, namespace string) jsonrpc.Request[json.RawMessage] {
287+
t.Helper()
288+
289+
params, err := json.Marshal(vaultcommon.ListSecretIdentifiersRequest{
290+
Namespace: namespace,
291+
})
292+
require.NoError(t, err)
293+
294+
return jsonrpc.Request[json.RawMessage]{
295+
ID: id,
296+
Method: vaulttypes.MethodSecretsList,
297+
Params: (*json.RawMessage)(&params),
298+
}
299+
}

0 commit comments

Comments
 (0)