This addresses all found vet/race fixes in community edition. --------- Co-authored-by: Damian Debkowski <damian.debkowski@hashicorp.com>
496 lines
17 KiB
Go
496 lines
17 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/hashicorp/boundary/internal/errors"
|
|
pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services"
|
|
"github.com/hashicorp/nodeenrollment"
|
|
"github.com/hashicorp/nodeenrollment/registration"
|
|
"github.com/hashicorp/nodeenrollment/rotation"
|
|
nodeefile "github.com/hashicorp/nodeenrollment/storage/file"
|
|
"github.com/hashicorp/nodeenrollment/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/testing/protocmp"
|
|
)
|
|
|
|
func Test_RegisterUpstreamMessageHandler(t *testing.T) {
|
|
// IMPORTANT: cannot run with t.Parallel() because it operates on the
|
|
// handlers pkg state.
|
|
testCtx := context.Background()
|
|
tests := []struct {
|
|
name string
|
|
msgType pbs.MsgType
|
|
h UpstreamMessageHandler
|
|
withUpstreamMsgTypeSpecifier bool
|
|
wantErr bool
|
|
wantErrMatch *errors.Template
|
|
wantErrContains string
|
|
}{
|
|
{
|
|
name: "missing-msg-type",
|
|
h: &TestMockUpstreamMessageHandler{},
|
|
wantErr: true,
|
|
wantErrMatch: errors.T(errors.InvalidParameter),
|
|
wantErrContains: "missing msg type",
|
|
},
|
|
{
|
|
name: "missing-handler",
|
|
msgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
wantErr: true,
|
|
wantErrMatch: errors.T(errors.InvalidParameter),
|
|
wantErrContains: "missing handler",
|
|
},
|
|
{
|
|
name: "success",
|
|
msgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
h: &TestMockUpstreamMessageHandler{},
|
|
},
|
|
{
|
|
name: "success-with-type-specifier",
|
|
msgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
h: &TestMockUpstreamMessageHandler{},
|
|
withUpstreamMsgTypeSpecifier: true,
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
cp := new(sync.Map)
|
|
upstreamMessageHandler.Range(func(k, v interface{}) bool {
|
|
cp.Store(k, v)
|
|
return true
|
|
})
|
|
err := RegisterUpstreamMessageHandler(testCtx, tc.msgType, tc.h)
|
|
t.Cleanup(func() {
|
|
upstreamMessageHandler = cp
|
|
})
|
|
if tc.wantErr {
|
|
require.Error(err)
|
|
if tc.wantErrMatch != nil {
|
|
assert.Truef(errors.Match(tc.wantErrMatch, err), "unexpected error: %q", err.Error())
|
|
}
|
|
if tc.wantErrContains != "" {
|
|
assert.Contains(err.Error(), tc.wantErrContains)
|
|
}
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
_, ok := getUpstreamMessageHandler(testCtx, tc.msgType)
|
|
assert.True(ok)
|
|
|
|
if tc.withUpstreamMsgTypeSpecifier {
|
|
_, ok := getUpstreamMessageTypeSpecifier(testCtx, tc.msgType)
|
|
assert.True(ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_controllerUpstreamMessageServiceServer_UpstreamMessage(t *testing.T) {
|
|
// IMPORTANT: cannot run with t.Parallel() because it operates on the
|
|
// handlers pkg state.
|
|
|
|
testCtx := context.Background()
|
|
|
|
// Get an initial set of authorized node credentials
|
|
initStorage, err := nodeefile.New(testCtx)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { assert.NoError(t, initStorage.Cleanup(testCtx)) })
|
|
_, err = rotation.RotateRootCertificates(testCtx, initStorage)
|
|
require.NoError(t, err)
|
|
initNodeCreds, err := types.NewNodeCredentials(testCtx, initStorage)
|
|
require.NoError(t, err)
|
|
req, err := initNodeCreds.CreateFetchNodeCredentialsRequest(testCtx)
|
|
require.NoError(t, err)
|
|
_, err = registration.AuthorizeNode(testCtx, initStorage, req)
|
|
require.NoError(t, err)
|
|
fetchResp, err := registration.FetchNodeCredentials(testCtx, initStorage, req)
|
|
require.NoError(t, err)
|
|
initNodeCreds, err = initNodeCreds.HandleFetchNodeCredentialsResponse(testCtx, initStorage, fetchResp)
|
|
require.NoError(t, err)
|
|
initKeyId, err := nodeenrollment.KeyIdFromPkix(initNodeCreds.CertificatePublicKeyPkix)
|
|
require.NoError(t, err)
|
|
nodeInfo, err := types.LoadNodeInformation(testCtx, initStorage, initKeyId)
|
|
require.NoError(t, err)
|
|
// define a test controller
|
|
testController, err := NewControllerUpstreamMessageServiceServer(testCtx, initStorage)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, testController)
|
|
|
|
tests := []struct {
|
|
name string
|
|
controllerServer pbs.UpstreamMessageServiceServer
|
|
setupHandlers func(t *testing.T)
|
|
req *pbs.UpstreamMessageRequest
|
|
wantErr bool
|
|
wantErrContains string
|
|
}{
|
|
{
|
|
name: "success-echo-unencrypted",
|
|
controllerServer: testController,
|
|
req: &pbs.UpstreamMessageRequest{
|
|
OriginatingWorkerKeyId: initKeyId,
|
|
MsgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
Message: func() *pbs.UpstreamMessageRequest_Pt {
|
|
pt, err := proto.Marshal(&pbs.EchoUpstreamMessageRequest{Msg: "ping"})
|
|
require.NoError(t, err)
|
|
return &pbs.UpstreamMessageRequest_Pt{Pt: pt}
|
|
}(),
|
|
},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, &TestMockUpstreamMessageHandler{}),
|
|
},
|
|
{
|
|
name: "unimplemented",
|
|
controllerServer: testController,
|
|
req: &pbs.UpstreamMessageRequest{
|
|
OriginatingWorkerKeyId: initKeyId,
|
|
MsgType: pbs.MsgType_MSG_TYPE_UNSPECIFIED,
|
|
Message: func() *pbs.UpstreamMessageRequest_Pt {
|
|
return &pbs.UpstreamMessageRequest_Pt{Pt: []byte("Unimplemented.")}
|
|
}(),
|
|
},
|
|
wantErr: true,
|
|
wantErrContains: "msg type \"MSG_TYPE_UNSPECIFIED\" is unsupported",
|
|
},
|
|
{
|
|
name: "missing-originating-worker-id",
|
|
controllerServer: testController,
|
|
req: &pbs.UpstreamMessageRequest{
|
|
MsgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
Message: func() *pbs.UpstreamMessageRequest_Pt {
|
|
pt, err := proto.Marshal(&pbs.EchoUpstreamMessageRequest{Msg: "ping"})
|
|
require.NoError(t, err)
|
|
return &pbs.UpstreamMessageRequest_Pt{Pt: pt}
|
|
}(),
|
|
},
|
|
wantErr: true,
|
|
wantErrContains: "missing originating worker id",
|
|
},
|
|
{
|
|
name: "unsupported-msg-type",
|
|
controllerServer: testController,
|
|
req: &pbs.UpstreamMessageRequest{
|
|
OriginatingWorkerKeyId: initKeyId,
|
|
MsgType: pbs.MsgType_MSG_TYPE_UNSPECIFIED,
|
|
Message: func() *pbs.UpstreamMessageRequest_Pt {
|
|
pt, err := proto.Marshal(&pbs.EchoUpstreamMessageRequest{Msg: "ping"})
|
|
require.NoError(t, err)
|
|
return &pbs.UpstreamMessageRequest_Pt{Pt: pt}
|
|
}(),
|
|
},
|
|
wantErr: true,
|
|
wantErrContains: "msg type \"MSG_TYPE_UNSPECIFIED\" is unsupported",
|
|
},
|
|
{
|
|
name: "invalid-originating-worker-id",
|
|
controllerServer: testController,
|
|
req: &pbs.UpstreamMessageRequest{
|
|
OriginatingWorkerKeyId: "bad-id",
|
|
MsgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
Message: func() *pbs.UpstreamMessageRequest_Ct {
|
|
r := &pbs.EchoUpstreamMessageRequest{Msg: "ping"}
|
|
ct, err := nodeenrollment.EncryptMessage(testCtx, r, nodeInfo)
|
|
require.NoError(t, err)
|
|
return &pbs.UpstreamMessageRequest_Ct{Ct: ct}
|
|
}(),
|
|
},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, &TestMockEncryptedUpstreamMessageHandler{}),
|
|
wantErr: true,
|
|
wantErrContains: "error loading node information",
|
|
},
|
|
{
|
|
name: "invalid-unencrypted-msg",
|
|
controllerServer: testController,
|
|
req: &pbs.UpstreamMessageRequest{
|
|
OriginatingWorkerKeyId: initKeyId,
|
|
MsgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
Message: &pbs.UpstreamMessageRequest_Pt{Pt: []byte("not-a-proto")},
|
|
},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, &TestMockUpstreamMessageHandler{}),
|
|
wantErr: true,
|
|
wantErrContains: "error marshaling request message",
|
|
},
|
|
{
|
|
name: "invalid-encrypted-msg",
|
|
controllerServer: testController,
|
|
req: &pbs.UpstreamMessageRequest{
|
|
OriginatingWorkerKeyId: initKeyId,
|
|
MsgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
Message: &pbs.UpstreamMessageRequest_Ct{Ct: []byte("not-an-encrypted-proto")},
|
|
},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, &TestMockEncryptedUpstreamMessageHandler{}),
|
|
wantErr: true,
|
|
wantErrContains: "error decrypting request message",
|
|
},
|
|
{
|
|
name: "message-handler-not-implemented",
|
|
controllerServer: testController,
|
|
req: &pbs.UpstreamMessageRequest{
|
|
OriginatingWorkerKeyId: initKeyId,
|
|
MsgType: -1,
|
|
Message: &pbs.UpstreamMessageRequest_Ct{Ct: []byte("not-an-encrypted-proto")},
|
|
},
|
|
wantErr: true,
|
|
wantErrContains: "Unimplemented desc = upstream message handler for \"-1\" is not implemented",
|
|
},
|
|
{
|
|
name: "success-echo-unencrypted",
|
|
controllerServer: testController,
|
|
req: &pbs.UpstreamMessageRequest{
|
|
OriginatingWorkerKeyId: initKeyId,
|
|
MsgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
Message: func() *pbs.UpstreamMessageRequest_Pt {
|
|
pt, err := proto.Marshal(&pbs.EchoUpstreamMessageRequest{Msg: "ping"})
|
|
require.NoError(t, err)
|
|
return &pbs.UpstreamMessageRequest_Pt{Pt: pt}
|
|
}(),
|
|
},
|
|
// intentionally, no setupHandlers
|
|
wantErr: true,
|
|
wantErrContains: "upstream message handler for \"MSG_TYPE_ECHO\" is not implemented",
|
|
},
|
|
{
|
|
name: "fail-echo-unencrypted",
|
|
controllerServer: testController,
|
|
req: &pbs.UpstreamMessageRequest{
|
|
OriginatingWorkerKeyId: initKeyId,
|
|
MsgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
Message: func() *pbs.UpstreamMessageRequest_Pt {
|
|
pt, err := proto.Marshal(&pbs.EchoUpstreamMessageRequest{Msg: "ping"})
|
|
require.NoError(t, err)
|
|
return &pbs.UpstreamMessageRequest_Pt{Pt: pt}
|
|
}(),
|
|
},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, &TestMockUpstreamMessageHandler{WantHandlerErr: fmt.Errorf("fail-echo-unencrypted")}),
|
|
wantErr: true,
|
|
wantErrContains: "rpc error: code = Unknown desc = fail-echo-unencrypted",
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
controllerClient := pbs.NewUpstreamMessageServiceClient(grpcCCToServer(t, testCtx, tc.controllerServer))
|
|
if tc.setupHandlers != nil {
|
|
tc.setupHandlers(t)
|
|
}
|
|
|
|
gotResp, err := controllerClient.UpstreamMessage(testCtx, tc.req)
|
|
if tc.wantErr {
|
|
require.Error(err)
|
|
assert.Nil(gotResp)
|
|
assert.Contains(err.Error(), tc.wantErrContains)
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
require.NotNil(gotResp)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_SendUpstreamMessage(t *testing.T) {
|
|
// IMPORTANT: cannot run with t.Parallel() because it operates on the
|
|
// handlers pkg state.
|
|
testCtx := context.Background()
|
|
workerClientProducer, workerNodeInfo, originatingWorkerId := TestUpstreamService(t)
|
|
testHandler := &TestMockUpstreamMessageHandler{}
|
|
testEncryptedHandler := &TestMockEncryptedUpstreamMessageHandler{}
|
|
|
|
tests := []struct {
|
|
name string
|
|
setupHandlers func(t *testing.T)
|
|
clientProducer UpstreamMessageServiceClientProducer
|
|
originatingWorkerId string
|
|
req proto.Message
|
|
opt []Option
|
|
wantResp proto.Message
|
|
wantErr bool
|
|
wantErrContains string
|
|
}{
|
|
{
|
|
name: "success-echo-unencrypted",
|
|
clientProducer: workerClientProducer,
|
|
originatingWorkerId: originatingWorkerId,
|
|
req: &pbs.EchoUpstreamMessageRequest{Msg: "ping"},
|
|
wantResp: &pbs.EchoUpstreamMessageResponse{Msg: "ping"},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, testHandler),
|
|
},
|
|
{
|
|
name: "success-echo-encrypted",
|
|
clientProducer: workerClientProducer,
|
|
originatingWorkerId: originatingWorkerId,
|
|
req: &pbs.EchoUpstreamMessageRequest{Msg: "ping"},
|
|
opt: []Option{WithKeyProducer(workerNodeInfo)},
|
|
wantResp: &pbs.EchoUpstreamMessageResponse{Msg: "ping"},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, testEncryptedHandler),
|
|
},
|
|
{
|
|
name: "missing-client-producer",
|
|
originatingWorkerId: originatingWorkerId,
|
|
req: &pbs.EchoUpstreamMessageRequest{Msg: "ping"},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, testHandler),
|
|
wantErr: true,
|
|
wantErrContains: "missing client producer",
|
|
},
|
|
{
|
|
name: "missing-originating-worker-id",
|
|
clientProducer: workerClientProducer,
|
|
req: &pbs.EchoUpstreamMessageRequest{Msg: "ping"},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, testHandler),
|
|
wantErr: true,
|
|
wantErrContains: "missing originating worker key id",
|
|
},
|
|
{
|
|
name: "missing-req",
|
|
clientProducer: workerClientProducer,
|
|
originatingWorkerId: originatingWorkerId,
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, testHandler),
|
|
wantErr: true,
|
|
wantErrContains: "missing message",
|
|
},
|
|
{
|
|
name: "invalid-msg-type",
|
|
clientProducer: workerClientProducer,
|
|
originatingWorkerId: originatingWorkerId,
|
|
req: &pbs.Connection{},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, testHandler),
|
|
wantErr: true,
|
|
wantErrContains: "\"\" is an unknown msg type",
|
|
},
|
|
{
|
|
name: "no-registered-handler",
|
|
clientProducer: workerClientProducer,
|
|
originatingWorkerId: originatingWorkerId,
|
|
req: &pbs.EchoUpstreamMessageRequest{Msg: "ping"},
|
|
wantErr: true,
|
|
wantErrContains: "upstream message handler for \"MSG_TYPE_ECHO\" is not implemented",
|
|
},
|
|
{
|
|
name: "no-registered-handler",
|
|
clientProducer: workerClientProducer,
|
|
originatingWorkerId: originatingWorkerId,
|
|
req: &pbs.EchoUpstreamMessageRequest{Msg: "ping"},
|
|
wantErr: true,
|
|
wantErrContains: "upstream message handler for \"MSG_TYPE_ECHO\" is not implemented",
|
|
},
|
|
{
|
|
name: "missing-node-info",
|
|
clientProducer: workerClientProducer,
|
|
originatingWorkerId: originatingWorkerId,
|
|
req: &pbs.EchoUpstreamMessageRequest{Msg: "ping"},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, testEncryptedHandler),
|
|
wantErr: true,
|
|
wantErrContains: "missing node information required for encrypting unwrap keys message",
|
|
},
|
|
{
|
|
name: "client-producer-err",
|
|
clientProducer: func(context.Context) (pbs.UpstreamMessageServiceClient, error) {
|
|
return nil, fmt.Errorf("client-producer-err")
|
|
},
|
|
originatingWorkerId: originatingWorkerId,
|
|
req: &pbs.EchoUpstreamMessageRequest{Msg: "ping"},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, testHandler),
|
|
wantErr: true,
|
|
wantErrContains: "error getting a client producer",
|
|
},
|
|
{
|
|
name: "upstream-err",
|
|
clientProducer: workerClientProducer,
|
|
originatingWorkerId: originatingWorkerId,
|
|
req: &pbs.EchoUpstreamMessageRequest{Msg: "ping"},
|
|
setupHandlers: TestRegisterHandlerFn(t, pbs.MsgType_MSG_TYPE_ECHO, &TestMockUpstreamMessageHandler{WantHandlerErr: fmt.Errorf("upstream-err")}),
|
|
wantErr: true,
|
|
wantErrContains: "error from upstream client",
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
|
|
if tc.setupHandlers != nil {
|
|
tc.setupHandlers(t)
|
|
}
|
|
gotResp, err := SendUpstreamMessage(testCtx, tc.clientProducer, tc.originatingWorkerId, tc.req, tc.opt...)
|
|
if tc.wantErr {
|
|
require.Error(err)
|
|
assert.Nil(gotResp)
|
|
assert.Contains(err.Error(), tc.wantErrContains)
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
require.NotNil(gotResp)
|
|
assert.Empty(cmp.Diff(gotResp, tc.wantResp, protocmp.Transform()))
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_RegisterUpstreamMessageTypeSpecifier(t *testing.T) {
|
|
// IMPORTANT: cannot run with t.Parallel() because it operates on the
|
|
// handlers pkg state.
|
|
testCtx := context.Background()
|
|
tests := []struct {
|
|
name string
|
|
msgType pbs.MsgType
|
|
s UpstreamMessageTypeSpecifier
|
|
wantErr bool
|
|
wantErrMatch *errors.Template
|
|
wantErrContains string
|
|
}{
|
|
{
|
|
name: "missing-msg-type",
|
|
s: &TestMockUpstreamMessageHandler{},
|
|
wantErr: true,
|
|
wantErrMatch: errors.T(errors.InvalidParameter),
|
|
wantErrContains: "missing msg type",
|
|
},
|
|
{
|
|
name: "missing-type-specifier",
|
|
msgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
wantErr: true,
|
|
wantErrMatch: errors.T(errors.InvalidParameter),
|
|
wantErrContains: "missing type specifier",
|
|
},
|
|
{
|
|
name: "success",
|
|
msgType: pbs.MsgType_MSG_TYPE_ECHO,
|
|
s: &TestMockUpstreamMessageHandler{},
|
|
},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
cp := new(sync.Map)
|
|
upstreamMessageTypeSpecifier.Range(func(k, v interface{}) bool {
|
|
cp.Store(k, v)
|
|
return true
|
|
})
|
|
err := registerUpstreamMessageTypeSpecifier(testCtx, tc.msgType, tc.s)
|
|
t.Cleanup(func() {
|
|
upstreamMessageHandler = cp
|
|
})
|
|
if tc.wantErr {
|
|
require.Error(err)
|
|
if tc.wantErrMatch != nil {
|
|
assert.Truef(errors.Match(tc.wantErrMatch, err), "unexpected error: %q", err.Error())
|
|
}
|
|
if tc.wantErrContains != "" {
|
|
assert.Contains(err.Error(), tc.wantErrContains)
|
|
}
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
_, ok := getUpstreamMessageTypeSpecifier(testCtx, tc.msgType)
|
|
assert.True(ok)
|
|
})
|
|
}
|
|
}
|