// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package connectca import ( "context" "errors" "testing" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/hashicorp/go-hclog" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/acl/resolver" "github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/grpc-external/testutils" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/proto-public/pbconnectca" ) func TestSign_ConnectDisabled(t *testing.T) { server := NewServer(Config{ConnectEnabled: false}) _, err := server.Sign(context.Background(), &pbconnectca.SignRequest{}) require.Error(t, err) require.Equal(t, codes.FailedPrecondition.String(), status.Code(err).String()) require.Contains(t, status.Convert(err).Message(), "Connect") } func TestSign_Validation(t *testing.T) { aclResolver := &MockACLResolver{} aclResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything). Return(testutils.ACLsDisabled(t), nil) server := NewServer(Config{ Logger: hclog.NewNullLogger(), ACLResolver: aclResolver, ForwardRPC: noopForwardRPC, ConnectEnabled: true, }) testCases := map[string]struct { csr, err string }{ "no csr": { csr: "", err: "CSR is required", }, "invalid csr": { csr: "bogus", err: "no PEM-encoded data found", }, } for desc, tc := range testCases { t.Run(desc, func(t *testing.T) { _, err := server.Sign(context.Background(), &pbconnectca.SignRequest{ Csr: tc.csr, }) require.Error(t, err) require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) require.Equal(t, tc.err, status.Convert(err).Message()) }) } } func TestSign_Unauthenticated(t *testing.T) { aclResolver := &MockACLResolver{} aclResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything). Return(resolver.Result{}, acl.ErrNotFound) server := NewServer(Config{ Logger: hclog.NewNullLogger(), ACLResolver: aclResolver, ForwardRPC: noopForwardRPC, ConnectEnabled: true, }) csr, _ := connect.TestCSR(t, connect.TestSpiffeIDService(t, "web")) _, err := server.Sign(context.Background(), &pbconnectca.SignRequest{ Csr: csr, }) require.Error(t, err) require.Equal(t, codes.Unauthenticated.String(), status.Code(err).String()) } func TestSign_PermissionDenied(t *testing.T) { aclResolver := &MockACLResolver{} aclResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything). Return(testutils.ACLsDisabled(t), nil) caManager := &MockCAManager{} caManager.On("AuthorizeAndSignCertificate", mock.Anything, mock.Anything). Return(nil, acl.ErrPermissionDenied) server := NewServer(Config{ Logger: hclog.NewNullLogger(), ACLResolver: aclResolver, CAManager: caManager, ForwardRPC: noopForwardRPC, ConnectEnabled: true, }) csr, _ := connect.TestCSR(t, connect.TestSpiffeIDService(t, "web")) _, err := server.Sign(context.Background(), &pbconnectca.SignRequest{ Csr: csr, }) require.Error(t, err) require.Equal(t, codes.PermissionDenied.String(), status.Code(err).String()) } func TestSign_InvalidCSR(t *testing.T) { aclResolver := &MockACLResolver{} aclResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything). Return(testutils.ACLsDisabled(t), nil) caManager := &MockCAManager{} caManager.On("AuthorizeAndSignCertificate", mock.Anything, mock.Anything). Return(nil, connect.InvalidCSRError("nope")) server := NewServer(Config{ Logger: hclog.NewNullLogger(), ACLResolver: aclResolver, CAManager: caManager, ForwardRPC: noopForwardRPC, ConnectEnabled: true, }) csr, _ := connect.TestCSR(t, connect.TestSpiffeIDService(t, "web")) _, err := server.Sign(context.Background(), &pbconnectca.SignRequest{ Csr: csr, }) require.Error(t, err) require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) } func TestSign_RateLimited(t *testing.T) { aclResolver := &MockACLResolver{} aclResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything). Return(testutils.ACLsDisabled(t), nil) caManager := &MockCAManager{} caManager.On("AuthorizeAndSignCertificate", mock.Anything, mock.Anything). Return(nil, errors.New("Rate limit reached, try again later")) server := NewServer(Config{ Logger: hclog.NewNullLogger(), ACLResolver: aclResolver, CAManager: caManager, ForwardRPC: noopForwardRPC, ConnectEnabled: true, }) csr, _ := connect.TestCSR(t, connect.TestSpiffeIDService(t, "web")) _, err := server.Sign(context.Background(), &pbconnectca.SignRequest{ Csr: csr, }) require.Error(t, err) require.Equal(t, codes.ResourceExhausted.String(), status.Code(err).String()) } func TestSign_InternalError(t *testing.T) { aclResolver := &MockACLResolver{} aclResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything). Return(testutils.ACLsDisabled(t), nil) caManager := &MockCAManager{} caManager.On("AuthorizeAndSignCertificate", mock.Anything, mock.Anything). Return(nil, errors.New("something went very wrong")) server := NewServer(Config{ Logger: hclog.NewNullLogger(), ACLResolver: aclResolver, CAManager: caManager, ForwardRPC: noopForwardRPC, ConnectEnabled: true, }) csr, _ := connect.TestCSR(t, connect.TestSpiffeIDService(t, "web")) _, err := server.Sign(context.Background(), &pbconnectca.SignRequest{ Csr: csr, }) require.Error(t, err) require.Equal(t, codes.Internal.String(), status.Code(err).String()) } func TestSign_Success(t *testing.T) { aclResolver := &MockACLResolver{} aclResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything). Return(testutils.ACLsDisabled(t), nil) caManager := &MockCAManager{} caManager.On("AuthorizeAndSignCertificate", mock.Anything, mock.Anything). Return(&structs.IssuedCert{CertPEM: "this is the PEM"}, nil) server := NewServer(Config{ Logger: hclog.NewNullLogger(), ACLResolver: aclResolver, CAManager: caManager, ForwardRPC: noopForwardRPC, ConnectEnabled: true, }) csr, _ := connect.TestCSR(t, connect.TestSpiffeIDService(t, "web")) rsp, err := server.Sign(context.Background(), &pbconnectca.SignRequest{ Csr: csr, }) require.NoError(t, err) require.Equal(t, "this is the PEM", rsp.CertPem) } func TestSign_RPCForwarding(t *testing.T) { aclResolver := &MockACLResolver{} aclResolver.On("ResolveTokenAndDefaultMeta", mock.Anything, mock.Anything, mock.Anything). Return(testutils.ACLsDisabled(t), nil) caManager := &MockCAManager{} caManager.On("AuthorizeAndSignCertificate", mock.Anything, mock.Anything). Return(&structs.IssuedCert{CertPEM: "leader response"}, nil) leader := NewServer(Config{ Logger: hclog.NewNullLogger(), ACLResolver: aclResolver, CAManager: caManager, ForwardRPC: noopForwardRPC, ConnectEnabled: true, }) //nolint:staticcheck leaderConn, err := grpc.Dial(testutils.RunTestServer(t, leader).String(), grpc.WithInsecure()) require.NoError(t, err) follower := NewServer(Config{ Logger: hclog.NewNullLogger(), ForwardRPC: func(_ structs.RPCInfo, fn func(*grpc.ClientConn) error) (bool, error) { return true, fn(leaderConn) }, ConnectEnabled: true, }) csr, _ := connect.TestCSR(t, connect.TestSpiffeIDService(t, "web")) rsp, err := follower.Sign(context.Background(), &pbconnectca.SignRequest{ Csr: csr, }) require.NoError(t, err) require.Equal(t, "leader response", rsp.CertPem) }