mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: refresh dynamic parameters on secret changes (#24786)
Publishes user secret create, update, and delete events and subscribes dynamic parameter websockets to authorized owner secret changes. Secret changes trigger fresh renders with monotonic response IDs, with backend tests covering subscription authorization and websocket refresh behavior.
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package coderd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"cdr.dev/slog/v3/sloggers/slogtest"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
)
|
||||
|
||||
func TestNextDynamicParametersResponseID(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
lastResponseID int
|
||||
requestID int
|
||||
want int
|
||||
}{
|
||||
{
|
||||
name: "request ID advances response ID",
|
||||
lastResponseID: 1,
|
||||
requestID: 4,
|
||||
want: 4,
|
||||
},
|
||||
{
|
||||
name: "request ID collision advances response ID",
|
||||
lastResponseID: 4,
|
||||
requestID: 4,
|
||||
want: 5,
|
||||
},
|
||||
{
|
||||
name: "stale request ID advances response ID",
|
||||
lastResponseID: 4,
|
||||
requestID: 2,
|
||||
want: 5,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := nextDynamicParametersResponseID(tt.lastResponseID, tt.requestID)
|
||||
require.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanSubscribeUserSecretEventsRequiresSecretRead(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ownerID := uuid.New()
|
||||
actor := rbac.Subject{ID: uuid.NewString()}
|
||||
|
||||
t.Run("allowed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auth := &recordingAuthorizer{}
|
||||
api := &API{
|
||||
Options: &Options{
|
||||
Logger: testutil.Logger(t),
|
||||
},
|
||||
HTTPAuth: &HTTPAuthorizer{
|
||||
Authorizer: auth,
|
||||
Logger: testutil.Logger(t),
|
||||
},
|
||||
}
|
||||
ctx := dbauthz.As(t.Context(), actor) //nolint:gocritic // Testing authorization from the request context.
|
||||
|
||||
require.True(t, api.canSubscribeUserSecretEvents(ctx, ownerID))
|
||||
require.Len(t, auth.calls, 1)
|
||||
require.Equal(t, actor, auth.calls[0].Actor)
|
||||
require.Equal(t, policy.ActionRead, auth.calls[0].Action)
|
||||
require.Equal(t, rbac.ResourceUserSecret.Type, auth.calls[0].Object.Type)
|
||||
require.Equal(t, ownerID.String(), auth.calls[0].Object.Owner)
|
||||
})
|
||||
|
||||
t.Run("denied", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auth := &recordingAuthorizer{err: xerrors.New("denied")}
|
||||
api := &API{
|
||||
Options: &Options{
|
||||
Logger: testutil.Logger(t),
|
||||
},
|
||||
HTTPAuth: &HTTPAuthorizer{
|
||||
Authorizer: auth,
|
||||
Logger: testutil.Logger(t),
|
||||
},
|
||||
}
|
||||
ctx := dbauthz.As(t.Context(), actor) //nolint:gocritic // Testing authorization from the request context.
|
||||
|
||||
require.False(t, api.canSubscribeUserSecretEvents(ctx, ownerID))
|
||||
require.Len(t, auth.calls, 1)
|
||||
})
|
||||
|
||||
t.Run("no actor", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
auth := &recordingAuthorizer{}
|
||||
logger := slogtest.Make(t, &slogtest.Options{
|
||||
IgnoredErrorIs: []error{},
|
||||
IgnoreErrorFn: func(entry slog.SinkEntry) bool {
|
||||
return entry.Message == "no authorization actor for user secret event subscription"
|
||||
},
|
||||
})
|
||||
api := &API{
|
||||
Options: &Options{
|
||||
Logger: logger,
|
||||
},
|
||||
HTTPAuth: &HTTPAuthorizer{
|
||||
Authorizer: auth,
|
||||
Logger: logger,
|
||||
},
|
||||
}
|
||||
|
||||
require.False(t, api.canSubscribeUserSecretEvents(context.Background(), ownerID))
|
||||
require.Empty(t, auth.calls)
|
||||
})
|
||||
}
|
||||
|
||||
type recordingAuthorizer struct {
|
||||
err error
|
||||
calls []rbac.AuthCall
|
||||
}
|
||||
|
||||
func (a *recordingAuthorizer) Authorize(_ context.Context, subject rbac.Subject, action policy.Action, object rbac.Object) error {
|
||||
a.calls = append(a.calls, rbac.AuthCall{
|
||||
Actor: subject,
|
||||
Action: action,
|
||||
Object: object,
|
||||
})
|
||||
return a.err
|
||||
}
|
||||
|
||||
func (*recordingAuthorizer) Prepare(context.Context, rbac.Subject, policy.Action, string) (rbac.PreparedAuthorized, error) {
|
||||
//nolint:nilnil // Prepare is unused by these tests.
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user