Files
teleport/tool/tctl/common/auth_command_test.go
T
rosstimothy 9d066364a8 Migrate GetCertAuthorities to gRPC (#22985)
Adds a new `GetCertAuthorities` unary RPC to
`teleport.v1.trust.TrustService` and deprecates the equivalent
HTTP endpoint. All associated interfaces have been updated to remove
the marshal options as well.
2023-03-14 14:45:01 +00:00

998 lines
29 KiB
Go

// Copyright 2021 Gravitational, Inc
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package common
import (
"bytes"
"context"
"crypto/x509/pkix"
"os"
"path/filepath"
"testing"
"time"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/client/identityfile"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/kube/kubeconfig"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
)
func TestAuthSignKubeconfig(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
ClusterName: "example.com",
})
if err != nil {
t.Fatal(err)
}
remoteCluster, err := types.NewRemoteCluster("leaf.example.com")
if err != nil {
t.Fatal(err)
}
_, cert, err := tlsca.GenerateSelfSignedCA(pkix.Name{CommonName: "example.com"}, nil, time.Minute)
require.NoError(t, err)
ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
Type: types.HostCA,
ClusterName: "example.com",
ActiveKeys: types.CAKeySet{
SSH: []*types.SSHKeyPair{{PublicKey: []byte("SSH CA cert")}},
TLS: []*types.TLSKeyPair{{Cert: cert}},
},
})
require.NoError(t, err)
separatedCluster := &mockClient{
clusterName: clusterName,
remoteClusters: []types.RemoteCluster{remoteCluster},
userCerts: &proto.Certs{
SSH: []byte("SSH cert"),
TLS: cert,
TLSCACerts: [][]byte{
cert,
},
},
cas: []types.CertAuthority{ca},
proxies: []types.Server{
&types.ServerV2{
Kind: types.KindNode,
Version: types.V2,
Metadata: types.Metadata{
Name: "proxy",
},
Spec: types.ServerSpecV2{
PublicAddr: "proxy-from-api.example.com:3080",
},
},
},
}
multiplexCluster := *separatedCluster
multiplexCluster.networkConfig = &types.ClusterNetworkingConfigV2{}
multiplexCluster.networkConfig.SetProxyListenerMode(types.ProxyListenerMode_Multiplex)
tests := []struct {
desc string
ac AuthCommand
client *mockClient
wantAddr string
wantCluster string
assertErr require.ErrorAssertionFunc
}{
{
desc: "valid --proxy URL with valid URL scheme",
client: separatedCluster,
ac: AuthCommand{
output: filepath.Join(tmpDir, "kubeconfig"),
outputFormat: identityfile.FormatKubernetes,
signOverwrite: true,
proxyAddr: "https://proxy-from-flag.example.com",
},
wantAddr: "https://proxy-from-flag.example.com",
assertErr: require.NoError,
},
{
desc: "valid --proxy URL with invalid URL scheme",
client: separatedCluster,
ac: AuthCommand{
output: filepath.Join(tmpDir, "kubeconfig"),
outputFormat: identityfile.FormatKubernetes,
signOverwrite: true,
proxyAddr: "file://proxy-from-flag.example.com",
},
assertErr: func(t require.TestingT, err error, _ ...interface{}) {
require.Error(t, err)
require.Equal(t, err.Error(), "expected --proxy URL with http or https scheme")
},
},
{
desc: "valid --proxy URL without URL scheme",
client: separatedCluster,
ac: AuthCommand{
output: filepath.Join(tmpDir, "kubeconfig"),
outputFormat: identityfile.FormatKubernetes,
signOverwrite: true,
proxyAddr: "proxy-from-flag.example.com",
},
wantAddr: "https://proxy-from-flag.example.com",
assertErr: require.NoError,
},
{
desc: "invalid --proxy URL",
client: separatedCluster,
ac: AuthCommand{
output: filepath.Join(tmpDir, "kubeconfig"),
outputFormat: identityfile.FormatKubernetes,
signOverwrite: true,
proxyAddr: "1https://proxy-from-flag.example.com",
},
assertErr: func(t require.TestingT, err error, _ ...interface{}) {
require.Error(t, err)
require.Contains(t, err.Error(), "specified --proxy URL is invalid")
},
},
{
desc: "k8s proxy running locally with public_addr",
client: separatedCluster,
ac: AuthCommand{
output: filepath.Join(tmpDir, "kubeconfig"),
outputFormat: identityfile.FormatKubernetes,
signOverwrite: true,
config: &servicecfg.Config{Proxy: servicecfg.ProxyConfig{Kube: servicecfg.KubeProxyConfig{
Enabled: true,
PublicAddrs: []utils.NetAddr{{Addr: "proxy-from-config.example.com:3026"}},
}}},
},
wantAddr: "https://proxy-from-config.example.com:3026",
assertErr: require.NoError,
},
{
desc: "k8s proxy running locally without public_addr",
client: separatedCluster,
ac: AuthCommand{
output: filepath.Join(tmpDir, "kubeconfig"),
outputFormat: identityfile.FormatKubernetes,
signOverwrite: true,
config: &servicecfg.Config{Proxy: servicecfg.ProxyConfig{
Kube: servicecfg.KubeProxyConfig{
Enabled: true,
},
PublicAddrs: []utils.NetAddr{{Addr: "proxy-from-config.example.com:3080"}},
}},
},
wantAddr: "https://proxy-from-config.example.com:3026",
assertErr: require.NoError,
},
{
desc: "k8s proxy from cluster info",
client: separatedCluster,
ac: AuthCommand{
output: filepath.Join(tmpDir, "kubeconfig"),
outputFormat: identityfile.FormatKubernetes,
signOverwrite: true,
config: &servicecfg.Config{Proxy: servicecfg.ProxyConfig{
Kube: servicecfg.KubeProxyConfig{
Enabled: false,
},
}},
},
wantAddr: "https://proxy-from-api.example.com:3026",
assertErr: require.NoError,
},
{
desc: "--kube-cluster specified with valid cluster",
client: separatedCluster,
ac: AuthCommand{
output: filepath.Join(tmpDir, "kubeconfig"),
outputFormat: identityfile.FormatKubernetes,
signOverwrite: true,
leafCluster: remoteCluster.GetMetadata().Name,
config: &servicecfg.Config{Proxy: servicecfg.ProxyConfig{
Kube: servicecfg.KubeProxyConfig{
Enabled: false,
},
}},
},
wantCluster: remoteCluster.GetMetadata().Name,
assertErr: require.NoError,
},
{
desc: "--kube-cluster specified with invalid cluster",
client: separatedCluster,
ac: AuthCommand{
output: filepath.Join(tmpDir, "kubeconfig"),
outputFormat: identityfile.FormatKubernetes,
signOverwrite: true,
leafCluster: "doesnotexist.example.com",
config: &servicecfg.Config{Proxy: servicecfg.ProxyConfig{
Kube: servicecfg.KubeProxyConfig{
Enabled: false,
},
}},
},
assertErr: func(t require.TestingT, err error, _ ...interface{}) {
require.Error(t, err)
require.Equal(t, err.Error(), "couldn't find leaf cluster named \"doesnotexist.example.com\"")
},
},
{
desc: "k8s proxy running locally in multiplex mode without public_addr",
client: &multiplexCluster,
ac: AuthCommand{
output: filepath.Join(tmpDir, "kubeconfig"),
outputFormat: identityfile.FormatKubernetes,
signOverwrite: true,
config: &servicecfg.Config{
Auth: servicecfg.AuthConfig{
NetworkingConfig: &types.ClusterNetworkingConfigV2{
Spec: types.ClusterNetworkingConfigSpecV2{
ProxyListenerMode: types.ProxyListenerMode_Multiplex,
},
},
},
Proxy: servicecfg.ProxyConfig{Kube: servicecfg.KubeProxyConfig{
Enabled: true,
}, PublicAddrs: []utils.NetAddr{{Addr: "proxy-from-config.example.com:3080"}}},
},
},
wantAddr: "https://proxy-from-config.example.com:3080",
assertErr: require.NoError,
},
{
desc: "k8s proxy from cluster info",
client: &multiplexCluster,
ac: AuthCommand{
output: filepath.Join(tmpDir, "kubeconfig"),
outputFormat: identityfile.FormatKubernetes,
signOverwrite: true,
config: &servicecfg.Config{Proxy: servicecfg.ProxyConfig{
Kube: servicecfg.KubeProxyConfig{
Enabled: false,
},
}},
},
wantAddr: "https://proxy-from-api.example.com:3080",
assertErr: require.NoError,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
// Generate kubeconfig.
err := tt.ac.generateUserKeys(context.Background(), tt.client)
tt.assertErr(t, err)
// Validate kubeconfig contents.
kc, err := kubeconfig.Load(tt.ac.output)
if err != nil {
t.Fatalf("loading generated kubeconfig: %v", err)
}
currentCtx, ok := kc.Contexts[kc.CurrentContext]
if !ok {
t.Fatalf("currentContext %q not present in kubeconfig", kc.CurrentContext)
}
gotCert := kc.AuthInfos[currentCtx.AuthInfo].ClientCertificateData
if !bytes.Equal(gotCert, tt.client.userCerts.TLS) {
t.Errorf("got client cert: %q, want %q", gotCert, tt.client.userCerts.TLS)
}
gotCA := kc.Clusters[currentCtx.Cluster].CertificateAuthorityData
wantCA := ca.GetActiveKeys().TLS[0].Cert
if !bytes.Equal(gotCA, wantCA) {
t.Errorf("got CA cert: %q, want %q", gotCA, wantCA)
}
gotServerAddr := kc.Clusters[currentCtx.Cluster].Server
if tt.wantAddr != "" && gotServerAddr != tt.wantAddr {
t.Errorf("got server address: %q, want %q", gotServerAddr, tt.wantAddr)
}
if tt.wantCluster != "" && kc.CurrentContext != tt.wantCluster {
t.Errorf("got cluster: %q, want %q", kc.CurrentContext, tt.wantCluster)
}
})
}
}
type mockClient struct {
auth.ClientI
clusterName types.ClusterName
userCerts *proto.Certs
userCertsReq *proto.UserCertsRequest
dbCertsReq *proto.DatabaseCertRequest
dbCerts *proto.DatabaseCertResponse
cas []types.CertAuthority
proxies []types.Server
remoteClusters []types.RemoteCluster
kubeServers []types.KubeServer
appServices []types.AppServer
dbServices []types.DatabaseServer
appSession types.WebSession
networkConfig types.ClusterNetworkingConfig
crl []byte
}
func (c *mockClient) GetClusterName(...services.MarshalOption) (types.ClusterName, error) {
return c.clusterName, nil
}
func (c *mockClient) GetClusterNetworkingConfig(ctx context.Context, opts ...services.MarshalOption) (types.ClusterNetworkingConfig, error) {
if c.networkConfig == nil {
return &types.ClusterNetworkingConfigV2{}, nil
}
return c.networkConfig, nil
}
func (c *mockClient) GenerateUserCerts(ctx context.Context, userCertsReq proto.UserCertsRequest) (*proto.Certs, error) {
c.userCertsReq = &userCertsReq
return c.userCerts, nil
}
func (c *mockClient) GetCertAuthority(ctx context.Context, id types.CertAuthID, loadSigningKeys bool) (types.CertAuthority, error) {
for _, v := range c.cas {
if v.GetType() == id.Type && v.GetClusterName() == id.DomainName {
return v, nil
}
}
return nil, trace.NotFound("not found")
}
func (c *mockClient) GetCertAuthorities(context.Context, types.CertAuthType, bool) ([]types.CertAuthority, error) {
return c.cas, nil
}
func (c *mockClient) GetProxies() ([]types.Server, error) {
return c.proxies, nil
}
func (c *mockClient) GetRemoteClusters(opts ...services.MarshalOption) ([]types.RemoteCluster, error) {
return c.remoteClusters, nil
}
func (c *mockClient) GetKubernetesServers(context.Context) ([]types.KubeServer, error) {
return c.kubeServers, nil
}
func (c *mockClient) GenerateDatabaseCert(ctx context.Context, req *proto.DatabaseCertRequest) (*proto.DatabaseCertResponse, error) {
c.dbCertsReq = req
return c.dbCerts, nil
}
func (c *mockClient) GetApplicationServers(context.Context, string) ([]types.AppServer, error) {
return c.appServices, nil
}
func (c *mockClient) CreateAppSession(ctx context.Context, req types.CreateAppSessionRequest) (types.WebSession, error) {
return c.appSession, nil
}
func (c *mockClient) GetDatabaseServers(context.Context, string, ...services.MarshalOption) ([]types.DatabaseServer, error) {
return c.dbServices, nil
}
func (c *mockClient) GenerateCertAuthorityCRL(context.Context, types.CertAuthType) ([]byte, error) {
return c.crl, nil
}
func TestCheckKubeCluster(t *testing.T) {
const teleportCluster = "local-teleport"
clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
ClusterName: teleportCluster,
})
require.NoError(t, err)
client := &mockClient{
clusterName: clusterName,
}
tests := []struct {
desc string
kubeCluster string
leafCluster string
outputFormat identityfile.Format
registeredClusters []*types.KubernetesClusterV3
want string
assertErr require.ErrorAssertionFunc
}{
{
desc: "non-k8s output format",
outputFormat: identityfile.FormatFile,
assertErr: require.NoError,
},
{
desc: "local cluster, valid kube cluster",
kubeCluster: "foo",
leafCluster: teleportCluster,
registeredClusters: []*types.KubernetesClusterV3{{Metadata: types.Metadata{Name: "foo"}}},
outputFormat: identityfile.FormatKubernetes,
want: "foo",
assertErr: require.NoError,
},
{
desc: "local cluster, empty kube cluster",
kubeCluster: "",
leafCluster: teleportCluster,
registeredClusters: []*types.KubernetesClusterV3{{Metadata: types.Metadata{Name: "foo"}}},
outputFormat: identityfile.FormatKubernetes,
want: "foo",
assertErr: require.NoError,
},
{
desc: "local cluster, empty kube cluster, no registered kube clusters",
kubeCluster: "",
leafCluster: teleportCluster,
registeredClusters: []*types.KubernetesClusterV3{},
outputFormat: identityfile.FormatKubernetes,
want: "",
assertErr: require.NoError,
},
{
desc: "local cluster, invalid kube cluster",
kubeCluster: "bar",
leafCluster: teleportCluster,
registeredClusters: []*types.KubernetesClusterV3{{Metadata: types.Metadata{Name: "foo"}}},
outputFormat: identityfile.FormatKubernetes,
assertErr: require.Error,
},
{
desc: "remote cluster, empty kube cluster",
kubeCluster: "",
leafCluster: "remote-teleport",
registeredClusters: []*types.KubernetesClusterV3{{Metadata: types.Metadata{Name: "foo"}}},
outputFormat: identityfile.FormatKubernetes,
want: "",
assertErr: require.NoError,
},
{
desc: "remote cluster, non-empty kube cluster",
kubeCluster: "bar",
leafCluster: "remote-teleport",
registeredClusters: []*types.KubernetesClusterV3{{Metadata: types.Metadata{Name: "foo"}}},
outputFormat: identityfile.FormatKubernetes,
want: "bar",
assertErr: require.NoError,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
client.kubeServers = []types.KubeServer{}
for _, kube := range tt.registeredClusters {
client.kubeServers = append(client.kubeServers, &types.KubernetesServerV3{
Metadata: types.Metadata{
Name: kube.GetName(),
},
Spec: types.KubernetesServerSpecV3{
Hostname: "host",
Cluster: kube,
},
})
}
a := &AuthCommand{
kubeCluster: tt.kubeCluster,
leafCluster: tt.leafCluster,
outputFormat: tt.outputFormat,
}
err := a.checkKubeCluster(context.Background(), client)
tt.assertErr(t, err)
require.Equal(t, tt.want, a.kubeCluster)
})
}
}
// TestGenerateDatabaseKeys verifies cert/key pair generation for databases.
func TestGenerateDatabaseKeys(t *testing.T) {
clusterName, err := services.NewClusterNameWithRandomID(
types.ClusterNameSpecV2{
ClusterName: "example.com",
})
require.NoError(t, err)
certBytes := []byte("TLS cert")
caBytes := []byte("CA cert")
authClient := &mockClient{
clusterName: clusterName,
dbCerts: &proto.DatabaseCertResponse{
Cert: certBytes,
CACerts: [][]byte{caBytes},
},
}
key, err := client.GenerateRSAKey()
require.NoError(t, err)
tests := []struct {
name string
inFormat identityfile.Format
inHost string
inOutDir string
inOutFile string
outSubject pkix.Name
outServerNames []string
outKeyFile string
outCertFile string
outCAFile string
outKey []byte
outCert []byte
outCA []byte
genKeyErrMsg string
}{
{
name: "database certificate",
inFormat: identityfile.FormatDatabase,
inHost: "postgres.example.com",
inOutDir: t.TempDir(),
inOutFile: "db",
outSubject: pkix.Name{CommonName: "postgres.example.com"},
outServerNames: []string{"postgres.example.com"},
outKeyFile: "db.key",
outCertFile: "db.crt",
outCAFile: "db.cas",
outKey: key.PrivateKeyPEM(),
outCert: certBytes,
outCA: caBytes,
},
{
name: "database certificate multiple SANs",
inFormat: identityfile.FormatDatabase,
inHost: "mysql.external.net,mysql.internal.net,192.168.1.1",
inOutDir: t.TempDir(),
inOutFile: "db",
outSubject: pkix.Name{CommonName: "mysql.external.net"},
outServerNames: []string{"mysql.external.net", "mysql.internal.net", "192.168.1.1"},
outKeyFile: "db.key",
outCertFile: "db.crt",
outCAFile: "db.cas",
outKey: key.PrivateKeyPEM(),
outCert: certBytes,
outCA: caBytes,
},
{
name: "mongodb certificate",
inFormat: identityfile.FormatMongo,
inHost: "mongo.example.com",
inOutDir: t.TempDir(),
inOutFile: "mongo",
outSubject: pkix.Name{CommonName: "mongo.example.com", Organization: []string{"example.com"}},
outServerNames: []string{"mongo.example.com"},
outCertFile: "mongo.crt",
outCAFile: "mongo.cas",
outCert: append(certBytes, key.PrivateKeyPEM()...),
outCA: caBytes,
},
{
name: "cockroachdb certificate",
inFormat: identityfile.FormatCockroach,
inHost: "localhost,roach1",
inOutDir: t.TempDir(),
outSubject: pkix.Name{CommonName: "node"},
outServerNames: []string{"node", "localhost", "roach1"}, // "node" principal should always be added
outKeyFile: "node.key",
outCertFile: "node.crt",
outCAFile: "ca.crt",
outKey: key.PrivateKeyPEM(),
outCert: certBytes,
outCA: caBytes,
},
{
name: "redis certificate",
inFormat: identityfile.FormatRedis,
inHost: "localhost,redis1,172.0.0.1",
inOutDir: t.TempDir(),
inOutFile: "db",
outSubject: pkix.Name{CommonName: "localhost"},
outServerNames: []string{"localhost", "redis1", "172.0.0.1"},
outKeyFile: "db.key",
outCertFile: "db.crt",
outCAFile: "db.cas",
outKey: key.PrivateKeyPEM(),
outCert: certBytes,
outCA: caBytes,
},
{
name: "missing host",
inFormat: identityfile.FormatRedis,
inOutDir: t.TempDir(),
inHost: "", // missing host
inOutFile: "db",
genKeyErrMsg: "at least one hostname must be specified",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ac := AuthCommand{
output: filepath.Join(test.inOutDir, test.inOutFile),
outputFormat: test.inFormat,
signOverwrite: true,
genHost: test.inHost,
genTTL: time.Hour,
}
err = ac.generateDatabaseKeysForKey(context.Background(), authClient, key)
if test.genKeyErrMsg == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
require.Contains(t, err.Error(), test.genKeyErrMsg)
return
}
require.NotNil(t, authClient.dbCertsReq)
csr, err := tlsca.ParseCertificateRequestPEM(authClient.dbCertsReq.CSR)
require.NoError(t, err)
require.Equal(t, test.outSubject.String(), csr.Subject.String())
require.Equal(t, test.outServerNames, authClient.dbCertsReq.ServerNames)
require.Equal(t, test.outServerNames[0], authClient.dbCertsReq.ServerName)
if len(test.outKey) > 0 {
keyBytes, err := os.ReadFile(filepath.Join(test.inOutDir, test.outKeyFile))
require.NoError(t, err)
require.Equal(t, test.outKey, keyBytes, "keys match")
}
if len(test.outCert) > 0 {
certBytes, err := os.ReadFile(filepath.Join(test.inOutDir, test.outCertFile))
require.NoError(t, err)
require.Equal(t, test.outCert, certBytes, "certificates match")
}
if len(test.outCA) > 0 {
caBytes, err := os.ReadFile(filepath.Join(test.inOutDir, test.outCAFile))
require.NoError(t, err)
require.Equal(t, test.outCA, caBytes, "CA certificates match")
}
})
}
}
// TestGenerateAppCertificates verifies cert/key pair generation for applications.
func TestGenerateAppCertificates(t *testing.T) {
const appName = "app-1"
const clusterNameStr = "example.com"
const publicAddr = "https://app-1.example.com"
const sessionID = "foobar"
clusterName, err := services.NewClusterNameWithRandomID(
types.ClusterNameSpecV2{
ClusterName: clusterNameStr,
})
require.NoError(t, err)
authClient := &mockClient{
clusterName: clusterName,
userCerts: &proto.Certs{
SSH: []byte("SSH cert"),
TLS: []byte("TLS cert"),
},
appServices: []types.AppServer{
&types.AppServerV3{
Metadata: types.Metadata{
Name: appName,
},
Spec: types.AppServerSpecV3{
App: &types.AppV3{
Spec: types.AppSpecV3{
PublicAddr: publicAddr,
},
},
},
},
},
appSession: &types.WebSessionV2{
Metadata: types.Metadata{
Name: sessionID,
},
},
}
tests := []struct {
name string
outDir string
outFileBase string
appName string
assertErr require.ErrorAssertionFunc
}{
{
name: "app happy path",
outDir: t.TempDir(),
outFileBase: "app-1",
appName: "app-1",
assertErr: require.NoError,
},
{
name: "app non-existent",
outDir: t.TempDir(),
outFileBase: "app-2",
appName: "app-2",
assertErr: func(t require.TestingT, err error, _ ...interface{}) {
require.Error(t, err)
require.True(t, trace.IsNotFound(err))
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
output := filepath.Join(tc.outDir, tc.outFileBase)
ac := AuthCommand{
output: output,
outputFormat: identityfile.FormatTLS,
signOverwrite: true,
genTTL: time.Hour,
appName: tc.appName,
}
err = ac.generateUserKeys(context.Background(), authClient)
tc.assertErr(t, err)
if err != nil {
return
}
expectedRouteToApp := proto.RouteToApp{
Name: tc.appName,
SessionID: sessionID,
PublicAddr: publicAddr,
ClusterName: clusterNameStr,
}
require.Equal(t, proto.UserCertsRequest_App, authClient.userCertsReq.Usage)
require.Equal(t, expectedRouteToApp, authClient.userCertsReq.RouteToApp)
certBytes, err := os.ReadFile(filepath.Join(tc.outDir, tc.outFileBase+".crt"))
require.NoError(t, err)
require.Equal(t, authClient.userCerts.TLS, certBytes, "certificates match")
})
}
}
func TestGenerateDatabaseUserCertificates(t *testing.T) {
ctx := context.Background()
tests := map[string]struct {
clusterName string
dbService string
dbName string
dbUser string
expectedDbProtocol string
dbServices []types.DatabaseServer
expectedErr error
}{
"DatabaseExists": {
clusterName: "example.com",
dbService: "db-1",
expectedDbProtocol: defaults.ProtocolPostgres,
dbServices: []types.DatabaseServer{
&types.DatabaseServerV3{
Metadata: types.Metadata{
Name: "db-1",
},
Spec: types.DatabaseServerSpecV3{
Hostname: "example.com",
Database: &types.DatabaseV3{
Spec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolPostgres,
},
},
},
},
},
},
"DatabaseWithUserExists": {
clusterName: "example.com",
dbService: "db-user-1",
dbUser: "mongo-user",
expectedDbProtocol: defaults.ProtocolMongoDB,
dbServices: []types.DatabaseServer{
&types.DatabaseServerV3{
Metadata: types.Metadata{
Name: "db-user-1",
},
Spec: types.DatabaseServerSpecV3{
Hostname: "example.com",
Database: &types.DatabaseV3{
Spec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolMongoDB,
},
},
},
},
},
},
"DatabaseWithDatabaseNameExists": {
clusterName: "example.com",
dbService: "db-user-1",
dbName: "root-database",
expectedDbProtocol: defaults.ProtocolMongoDB,
dbServices: []types.DatabaseServer{
&types.DatabaseServerV3{
Metadata: types.Metadata{
Name: "db-user-1",
},
Spec: types.DatabaseServerSpecV3{
Hostname: "example.com",
Database: &types.DatabaseV3{
Spec: types.DatabaseSpecV3{
Protocol: defaults.ProtocolMongoDB,
},
},
},
},
},
},
"DatabaseNotFound": {
clusterName: "example.com",
dbService: "db-2",
dbServices: []types.DatabaseServer{},
expectedErr: trace.NotFound(""),
},
}
for name, test := range tests {
t.Run(name, func(t *testing.T) {
clusterName, err := services.NewClusterNameWithRandomID(
types.ClusterNameSpecV2{
ClusterName: test.clusterName,
})
require.NoError(t, err)
authClient := &mockClient{
clusterName: clusterName,
userCerts: &proto.Certs{
SSH: []byte("SSH cert"),
TLS: []byte("TLS cert"),
},
dbServices: test.dbServices,
}
certsDir := t.TempDir()
output := filepath.Join(certsDir, test.dbService)
ac := AuthCommand{
output: output,
outputFormat: identityfile.FormatTLS,
signOverwrite: true,
genTTL: time.Hour,
dbService: test.dbService,
dbName: test.dbName,
dbUser: test.dbUser,
}
err = ac.generateUserKeys(ctx, authClient)
if test.expectedErr != nil {
require.Error(t, err)
require.IsType(t, test.expectedErr, err)
return
}
require.NoError(t, err)
expectedRouteToDatabase := proto.RouteToDatabase{
ServiceName: test.dbService,
Protocol: test.expectedDbProtocol,
Database: test.dbName,
Username: test.dbUser,
}
require.Equal(t, proto.UserCertsRequest_Database, authClient.userCertsReq.Usage)
require.Equal(t, expectedRouteToDatabase, authClient.userCertsReq.RouteToDatabase)
certBytes, err := os.ReadFile(filepath.Join(certsDir, test.dbService+".crt"))
require.NoError(t, err)
require.Equal(t, authClient.userCerts.TLS, certBytes, "certificates match")
})
}
}
func TestGenerateAndSignKeys(t *testing.T) {
clusterName, err := services.NewClusterNameWithRandomID(
types.ClusterNameSpecV2{
ClusterName: "example.com",
})
require.NoError(t, err)
_, cert, err := tlsca.GenerateSelfSignedCA(pkix.Name{CommonName: "example.com"}, nil, time.Minute)
require.NoError(t, err)
firstCA, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
Type: types.DatabaseCA,
ClusterName: "example.com",
ActiveKeys: types.CAKeySet{
SSH: []*types.SSHKeyPair{{PublicKey: []byte("SSH CA cert")}},
TLS: []*types.TLSKeyPair{{Cert: cert}},
},
})
require.NoError(t, err)
secondCA, err := types.NewCertAuthority(types.CertAuthoritySpecV2{
Type: types.DatabaseCA,
ClusterName: "leaf.example.com",
ActiveKeys: types.CAKeySet{
SSH: []*types.SSHKeyPair{{PublicKey: []byte("SSH CA cert")}},
TLS: []*types.TLSKeyPair{{Cert: cert}},
},
})
require.NoError(t, err)
certBytes := []byte("TLS cert")
caBytes := []byte("CA cert")
authClient := &mockClient{
clusterName: clusterName,
dbCerts: &proto.DatabaseCertResponse{
Cert: certBytes,
CACerts: [][]byte{caBytes},
},
cas: []types.CertAuthority{firstCA, secondCA},
}
tests := []struct {
name string
inFormat identityfile.Format
inHost string
inOutDir string
inOutFile string
}{
{
name: "snowflake format",
inFormat: identityfile.FormatSnowflake,
inOutDir: t.TempDir(),
inOutFile: "server",
},
{
name: "db format",
inFormat: identityfile.FormatDatabase,
inOutDir: t.TempDir(),
inOutFile: "server",
inHost: "localhost",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ac := AuthCommand{
output: filepath.Join(test.inOutDir, test.inOutFile),
outputFormat: test.inFormat,
signOverwrite: true,
genHost: test.inHost,
genTTL: time.Hour,
}
err = ac.GenerateAndSignKeys(context.Background(), authClient)
require.NoError(t, err)
})
}
}
func TestGenerateCRLForCA(t *testing.T) {
ctx := context.Background()
for _, caType := range allowedCRLCertificateTypes {
t.Run(caType, func(t *testing.T) {
ac := AuthCommand{caType: caType}
authClient := &mockClient{crl: []byte{}}
require.NoError(t, ac.GenerateCRLForCA(ctx, authClient))
})
}
t.Run("InvalidCAType", func(t *testing.T) {
ac := AuthCommand{caType: "wrong-ca"}
authClient := &mockClient{crl: []byte{}}
require.Error(t, ac.GenerateCRLForCA(ctx, authClient))
})
}