Files
Logan Davis 12fd87aa5f Bound Keypair: Initialize status field when creating token with --apply-on-startup (#68727)
* Write status on boundkeypair when created from apply-on-startup

* whitespace changes

* Update to not reset status

* Add check for defaulted recovery policy

* Add additional check for compare failure
2026-07-22 19:40:38 +00:00

2897 lines
106 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package auth_test
import (
"context"
"errors"
"fmt"
"maps"
"math"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"sync"
"testing"
"time"
"github.com/coreos/go-semver/semver"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
kyaml "k8s.io/apimachinery/pkg/util/yaml"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api"
"github.com/gravitational/teleport/api/constants"
backendinfov1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/backendinfo/v1"
dbobjectimportrulev1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/dbobjectimportrule/v1"
healthcheckconfigv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/healthcheckconfig/v1"
workloadidentityv1pb "github.com/gravitational/teleport/api/gen/proto/go/teleport/workloadidentity/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/backendinfo"
"github.com/gravitational/teleport/api/types/label"
"github.com/gravitational/teleport/api/types/vnet"
apisshutils "github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/entitlements"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/authcatest"
"github.com/gravitational/teleport/lib/auth/authtest"
"github.com/gravitational/teleport/lib/auth/state"
"github.com/gravitational/teleport/lib/auth/storage"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/backend/memory"
"github.com/gravitational/teleport/lib/cryptosuites"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/modules/modulestest"
"github.com/gravitational/teleport/lib/observability/tracing"
"github.com/gravitational/teleport/lib/service/servicecfg"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/srv/db/common/databaseobjectimportrule"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/proxy"
"github.com/gravitational/teleport/lib/utils/set"
)
// TestReadIdentity makes parses identity from private key and certificate
// and checks that all parameters are valid
func TestReadIdentity(t *testing.T) {
t.Parallel()
clock := clockwork.NewFakeClock()
a, err := testauthority.NewKeygen(modules.BuildOSS, clock.Now)
require.NoError(t, err)
priv, pub, err := a.GenerateKeyPair()
require.NoError(t, err)
caSigner, err := ssh.ParsePrivateKey(priv)
require.NoError(t, err)
cert, err := a.GenerateHostCert(sshca.HostCertificateRequest{
CASigner: caSigner,
PublicHostKey: pub,
HostID: "id1",
NodeName: "node-name",
TTL: 0,
Identity: sshca.Identity{
ClusterName: "example.com",
SystemRole: types.RoleNode,
},
})
require.NoError(t, err)
id, err := state.ReadSSHIdentityFromKeyPair(priv, cert)
require.NoError(t, err)
require.Equal(t, "example.com", id.ClusterName)
require.Equal(t, state.IdentityID{HostUUID: "id1.example.com", Role: types.RoleNode}, id.ID)
require.Equal(t, cert, id.CertBytes)
require.Equal(t, priv, id.KeyBytes)
// test TTL by converting the generated cert to text -> back and making sure ExpireAfter is valid
ttl := 10 * time.Second
expiryDate := clock.Now().Add(ttl)
bytes, err := a.GenerateHostCert(sshca.HostCertificateRequest{
CASigner: caSigner,
PublicHostKey: pub,
HostID: "id1",
NodeName: "node-name",
TTL: ttl,
Identity: sshca.Identity{
ClusterName: "example.com",
SystemRole: types.RoleNode,
},
})
require.NoError(t, err)
copy, err := apisshutils.ParseCertificate(bytes)
require.NoError(t, err)
require.Equal(t, uint64(expiryDate.Unix()), copy.ValidBefore)
}
func TestBadIdentity(t *testing.T) {
t.Parallel()
a, err := testauthority.NewKeygen(modules.BuildOSS, time.Now)
require.NoError(t, err)
priv, pub, err := a.GenerateKeyPair()
require.NoError(t, err)
caSigner, err := ssh.ParsePrivateKey(priv)
require.NoError(t, err)
// bad cert type
_, err = state.ReadSSHIdentityFromKeyPair(priv, pub)
require.ErrorAs(t, err, new(*trace.BadParameterError))
// missing authority domain
cert, err := a.GenerateHostCert(sshca.HostCertificateRequest{
CASigner: caSigner,
PublicHostKey: pub,
HostID: "id2",
NodeName: "",
TTL: 0,
Identity: sshca.Identity{
ClusterName: "",
SystemRole: types.RoleNode,
},
})
require.NoError(t, err)
_, err = state.ReadSSHIdentityFromKeyPair(priv, cert)
require.ErrorAs(t, err, new(*trace.BadParameterError))
// missing host uuid
cert, err = a.GenerateHostCert(sshca.HostCertificateRequest{
CASigner: caSigner,
PublicHostKey: pub,
HostID: "example.com",
NodeName: "",
TTL: 0,
Identity: sshca.Identity{
ClusterName: "",
SystemRole: types.RoleNode,
},
})
require.NoError(t, err)
_, err = state.ReadSSHIdentityFromKeyPair(priv, cert)
require.ErrorAs(t, err, new(*trace.BadParameterError))
// unrecognized role
cert, err = a.GenerateHostCert(sshca.HostCertificateRequest{
CASigner: caSigner,
PublicHostKey: pub,
HostID: "example.com",
NodeName: "",
TTL: 0,
Identity: sshca.Identity{
ClusterName: "id1",
SystemRole: "bad role",
},
})
require.NoError(t, err)
_, err = state.ReadSSHIdentityFromKeyPair(priv, cert)
require.ErrorAs(t, err, new(*trace.BadParameterError))
}
func TestSignatureAlgorithmSuite(t *testing.T) {
t.Parallel()
ctx := t.Context()
suiteName := func(suite types.SignatureAlgorithmSuite) string {
suiteName, err := suite.MarshalText()
require.NoError(t, err)
return string(suiteName)
}
assertSuitesEqual := func(t *testing.T, expected, actual types.SignatureAlgorithmSuite) {
t.Helper()
assert.Equal(t, suiteName(expected), suiteName(actual))
}
setupInitConfig := func(t *testing.T, capOrigin string, fips, hsm, cloud bool) auth.InitConfig {
cfg := setupConfig(t)
cfg.FIPS = fips
if hsm {
cfg.KeyStoreConfig = HSMTestConfig(t)
}
cfg.AuthPreference.SetOrigin(capOrigin)
if capOrigin != types.OriginDefaults {
cfg.AuthPreference.SetSignatureAlgorithmSuite(types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED)
}
// Pre-generate all CAs to keep tests fast esp. with SoftHSM.
for _, caType := range types.CertAuthTypes {
ca, err := authcatest.NewCAWithConfig(authcatest.CAConfig{
Type: caType,
ClusterName: cfg.ClusterName.GetClusterName(),
Clock: cfg.Clock,
})
require.NoError(t, err)
cfg.BootstrapResources = append(cfg.BootstrapResources, ca)
}
cfg.Modules = &modulestest.Modules{
TestFeatures: modules.Features{
Cloud: cloud,
Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{
entitlements.HSM: {Enabled: true},
},
},
}
return cfg
}
testCases := map[string]struct {
fips bool
hsm bool
cloud bool
expectDefaultSuite types.SignatureAlgorithmSuite
expectUnallowedSuites []types.SignatureAlgorithmSuite
}{
"basic": {
expectDefaultSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
},
"fips": {
fips: true,
expectDefaultSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_FIPS_V1,
expectUnallowedSuites: []types.SignatureAlgorithmSuite{
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1,
},
},
"hsm": {
hsm: true,
expectDefaultSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1,
expectUnallowedSuites: []types.SignatureAlgorithmSuite{
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
},
},
"fips and hsm": {
fips: true,
hsm: true,
expectDefaultSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_FIPS_V1,
expectUnallowedSuites: []types.SignatureAlgorithmSuite{
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1,
},
},
"cloud": {
cloud: true,
expectDefaultSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1,
expectUnallowedSuites: []types.SignatureAlgorithmSuite{
types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
},
},
}
// Test the behavior of auth server init. A default signature algorithm
// suite should never overwrite a persisted signature algorithm suite for an
// existing cluster, even if that was also a default.
t.Run("init", func(t *testing.T) {
for _, origin := range []string{types.OriginDefaults, types.OriginConfigFile} {
t.Run(origin, func(t *testing.T) {
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
// Assert that a fresh cluster with no signature_algorithm_suite
// configured gets the expected default suite, whether
// or not anything else in the cluster auth preference is set.
cfg := setupInitConfig(t, origin, tc.fips, tc.hsm, tc.cloud)
auth1, err := auth.Init(ctx, cfg)
require.NoError(t, err)
t.Cleanup(func() { auth1.Close() })
authPref, err := auth1.GetAuthPreference(ctx)
require.NoError(t, err)
assert.Equal(t, origin, authPref.GetMetadata().Labels[types.OriginLabel])
assertSuitesEqual(t, tc.expectDefaultSuite, authPref.GetSignatureAlgorithmSuite())
// Start a second auth server with the same backend and
// config, assert that the default suite remains.
auth2, err := auth.Init(ctx, cfg)
require.NoError(t, err)
t.Cleanup(func() { auth2.Close() })
authPref, err = auth2.GetAuthPreference(ctx)
require.NoError(t, err)
assert.Equal(t, origin, authPref.GetMetadata().Labels[types.OriginLabel])
assertSuitesEqual(t, tc.expectDefaultSuite, authPref.GetSignatureAlgorithmSuite())
// In the stored cluster_auth_preference, reset the
// signature_algorithm_suite to unspecified (still with
// the same origin) to mimic an older cluster with the old
// defaults, in the next step it will be "upgraded".
authPref.SetSignatureAlgorithmSuite(types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED)
_, err = auth2.UpsertAuthPreference(ctx, authPref)
require.NoError(t, err)
authPref, err = auth2.GetAuthPreference(ctx)
require.NoError(t, err)
// Sanity check it persisted.
assert.Equal(t, origin, authPref.GetMetadata().Labels[types.OriginLabel])
assertSuitesEqual(t, types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED, authPref.GetSignatureAlgorithmSuite())
// Start a third brand new auth server sharing the same
// backend and config. The new auth starting up should
// apply the new default auth preference and persist it
// to the backend, but it should not modify the existing
// signature algorithm suite even though it's
// unspecified. This is meant to test that a v16 auth
// server upgraded to v17 will still have an unspecified
// signature algorithm suite and won't get a new one
// until explicitly opting in.
auth3, err := auth.Init(ctx, cfg)
require.NoError(t, err)
t.Cleanup(func() { auth3.Close() })
authPref, err = auth3.GetAuthPreference(ctx)
require.NoError(t, err)
assert.Equal(t, origin, authPref.GetMetadata().Labels[types.OriginLabel])
assertSuitesEqual(t, types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED, authPref.GetSignatureAlgorithmSuite())
// Assert that the selected algorithm is RSA2048 when the suite is
// unspecified.
alg, err := cryptosuites.AlgorithmForKey(ctx,
cryptosuites.GetCurrentSuiteFromAuthPreference(auth3),
cryptosuites.UserTLS)
require.NoError(t, err)
assert.Equal(t, cryptosuites.RSA2048.String(), alg.String())
})
}
})
}
})
// Test that the auth preference cannot be upserted with a signature
// algorithm suite incompatible with the cluster FIPS and HSM settings.
t.Run("upsert", func(t *testing.T) {
for desc, tc := range testCases {
t.Run(desc, func(t *testing.T) {
cfg := authtest.AuthServerConfig{
Dir: t.TempDir(),
FIPS: tc.fips,
AuthPreferenceSpec: &types.AuthPreferenceSpecV2{
// Cloud requires second factor enabled.
SecondFactor: constants.SecondFactorOn,
Webauthn: &types.Webauthn{
RPID: "teleport.example.com",
},
},
Modules: &modulestest.Modules{
TestFeatures: modules.Features{
Cloud: tc.cloud,
Entitlements: map[entitlements.EntitlementKind]modules.EntitlementInfo{
entitlements.HSM: {Enabled: true},
},
},
},
}
if tc.hsm {
cfg.KeystoreConfig = HSMTestConfig(t)
}
testAuthServer, err := authtest.NewAuthServer(cfg)
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, testAuthServer.Close()) })
tlsServer, err := testAuthServer.NewTestTLSServer()
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, tlsServer.Close()) })
clt, err := tlsServer.NewClient(authtest.TestAdmin())
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, clt.Close()) })
for _, suiteValue := range types.SignatureAlgorithmSuite_value {
suite := types.SignatureAlgorithmSuite(suiteValue)
t.Run(suiteName(suite), func(t *testing.T) {
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
SignatureAlgorithmSuite: suite,
})
require.NoError(t, err)
_, err = clt.UpsertAuthPreference(ctx, authPref)
if slices.Contains(tc.expectUnallowedSuites, suite) {
var badParameterErr *trace.BadParameterError
assert.ErrorAs(t, err, &badParameterErr)
return
} else {
assert.NoError(t, err)
}
// Reset should go back to the default suite.
err = clt.ResetAuthPreference(ctx)
require.NoError(t, err)
authPref, err = clt.GetAuthPreference(ctx)
require.NoError(t, err)
assert.Equal(t, types.OriginDefaults, authPref.GetMetadata().Labels[types.OriginLabel])
assertSuitesEqual(t, tc.expectDefaultSuite, authPref.GetSignatureAlgorithmSuite())
})
}
})
}
})
}
func HSMTestConfig(t *testing.T) servicecfg.KeystoreConfig {
if cfg, ok := yubiHSMTestConfig(); ok {
t.Log("Running test with YubiHSM")
return cfg
}
if cfg, ok := cloudHSMTestConfig(); ok {
t.Log("Running test with AWS CloudHSM")
return cfg
}
if cfg, ok := awsKMSTestConfig(); ok {
t.Log("Running test with AWS KMS")
return cfg
}
if cfg, ok := gcpKMSTestConfig(); ok {
t.Log("Running test with GCP KMS")
return cfg
}
if cfg, ok := softHSMTestConfig(t); ok {
t.Log("Running test with SoftHSM")
return cfg
}
t.Skip("No HSM available for test")
return servicecfg.KeystoreConfig{}
}
func yubiHSMTestConfig() (servicecfg.KeystoreConfig, bool) {
yubiHSMPath := os.Getenv("TELEPORT_TEST_YUBIHSM_PKCS11_PATH")
yubiHSMPin := os.Getenv("TELEPORT_TEST_YUBIHSM_PIN")
if yubiHSMPath == "" || yubiHSMPin == "" {
return servicecfg.KeystoreConfig{}, false
}
slotNumber := 0
return servicecfg.KeystoreConfig{
PKCS11: servicecfg.PKCS11Config{
Path: yubiHSMPath,
SlotNumber: &slotNumber,
PIN: yubiHSMPin,
},
}, true
}
func cloudHSMTestConfig() (servicecfg.KeystoreConfig, bool) {
cloudHSMPin := os.Getenv("TELEPORT_TEST_CLOUDHSM_PIN")
if cloudHSMPin == "" {
return servicecfg.KeystoreConfig{}, false
}
return servicecfg.KeystoreConfig{
PKCS11: servicecfg.PKCS11Config{
Path: "/opt/cloudhsm/lib/libcloudhsm_pkcs11.so",
TokenLabel: "cavium",
PIN: cloudHSMPin,
},
}, true
}
func awsKMSTestConfig() (servicecfg.KeystoreConfig, bool) {
awsKMSAccount := os.Getenv("TELEPORT_TEST_AWS_KMS_ACCOUNT")
awsKMSRegion := os.Getenv("TELEPORT_TEST_AWS_KMS_REGION")
if awsKMSAccount == "" || awsKMSRegion == "" {
return servicecfg.KeystoreConfig{}, false
}
return servicecfg.KeystoreConfig{
AWSKMS: &servicecfg.AWSKMSConfig{
AWSAccount: awsKMSAccount,
AWSRegion: awsKMSRegion,
},
}, true
}
func gcpKMSTestConfig() (servicecfg.KeystoreConfig, bool) {
gcpKeyring := os.Getenv("TELEPORT_TEST_GCP_KMS_KEYRING")
if gcpKeyring == "" {
return servicecfg.KeystoreConfig{}, false
}
return servicecfg.KeystoreConfig{
GCPKMS: servicecfg.GCPKMSConfig{
KeyRing: gcpKeyring,
ProtectionLevel: "SOFTWARE",
},
}, true
}
var (
cachedSoftHSMConfig *servicecfg.KeystoreConfig
cachedSoftHSMConfigMutex sync.Mutex
)
// softHSMTestConfig is for use in tests only and creates a test SOFTHSM2 token.
// This should be used for all tests which need to use SoftHSM because the
// library can only be initialized once and SOFTHSM2_PATH and SOFTHSM2_CONF
// cannot be changed. New tokens added after the library has been initialized
// will not be found by the library.
//
// A new token will be used for each `go test` invocation, but it's difficult
// to create a separate token for each test because because new tokens
// added after the library has been initialized will not be found by the
// library. It's also difficult to clean up the token because tests for all
// packages are run in parallel there is not a good time to safely
// delete the token or the entire token directory. Each test should clean up
// all keys that it creates because SoftHSM2 gets really slow when there are
// many keys for a given token.
func softHSMTestConfig(t *testing.T) (servicecfg.KeystoreConfig, bool) {
path := os.Getenv("SOFTHSM2_PATH")
if path == "" {
return servicecfg.KeystoreConfig{}, false
}
cachedSoftHSMConfigMutex.Lock()
defer cachedSoftHSMConfigMutex.Unlock()
if cachedSoftHSMConfig != nil {
return *cachedSoftHSMConfig, true
}
if os.Getenv("SOFTHSM2_CONF") == "" {
// create tokendir
tokenDir, err := os.MkdirTemp("", "tokens")
require.NoError(t, err)
// create config file
configFile, err := os.CreateTemp("", "softhsm2.conf")
require.NoError(t, err)
// write config file
_, err = fmt.Fprintf(configFile, "directories.tokendir = %s\nobjectstore.backend = file\nlog.level = DEBUG\n", tokenDir)
require.NoError(t, err)
require.NoError(t, configFile.Close())
// set env
os.Setenv("SOFTHSM2_CONF", configFile.Name())
}
// create test token (max length is 32 chars)
tokenLabel := strings.ReplaceAll(uuid.NewString(), "-", "")
cmd := exec.Command("softhsm2-util", "--init-token", "--free", "--label", tokenLabel, "--so-pin", "password", "--pin", "password")
t.Logf("Running command: %q", cmd)
if err := cmd.Run(); err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
require.NoError(t, exitErr, "error creating test softhsm token: %s", string(exitErr.Stderr))
}
require.NoError(t, err, "error attempting to run softhsm2-util")
}
cachedSoftHSMConfig = &servicecfg.KeystoreConfig{
PKCS11: servicecfg.PKCS11Config{
Path: path,
TokenLabel: tokenLabel,
PIN: "password",
},
}
return *cachedSoftHSMConfig, true
}
type testDynamicallyConfigurableParams struct {
withDefaults, withConfigFile, withAnotherConfigFile func(*testing.T, *auth.InitConfig) types.ResourceWithOrigin
setDynamic func(*testing.T, *auth.Server)
getStored func(*testing.T, *auth.Server) types.ResourceWithOrigin
}
func testDynamicallyConfigurable(t *testing.T, p testDynamicallyConfigurableParams) {
initAuthServer := func(t *testing.T, conf auth.InitConfig) *auth.Server {
authServer, err := auth.Init(context.Background(), conf)
require.NoError(t, err)
t.Cleanup(func() { authServer.Close() })
return authServer
}
t.Run("start with config file, reinit with defaults", func(t *testing.T) {
t.Parallel()
conf := setupConfig(t)
// Simulate a server with a config-file resource.
configFileRes := p.withConfigFile(t, &conf)
authServer := initAuthServer(t, conf)
stored := p.getStored(t, authServer)
require.Equal(t, types.OriginConfigFile, stored.Origin())
require.Empty(t, resourceDiff(configFileRes, stored))
// Reinitialize with the default resource.
defaultRes := p.withDefaults(t, &conf)
authServer = initAuthServer(t, conf)
// Verify the stored resource is now labeled as originating from defaults.
stored = p.getStored(t, authServer)
require.Equal(t, types.OriginDefaults, stored.Origin())
require.Empty(t, resourceDiff(defaultRes, stored))
})
t.Run("start with dynamic, reinit with defaults", func(t *testing.T) {
t.Parallel()
conf := setupConfig(t)
// Simulate a server with dynamic configuration.
authServer := initAuthServer(t, conf)
p.setDynamic(t, authServer)
dynamic := p.getStored(t, authServer)
require.Equal(t, types.OriginDynamic, dynamic.Origin())
// Attempt to reinitialize with the default resource should be a no-op.
p.withDefaults(t, &conf)
authServer = initAuthServer(t, conf)
// Verify the stored resource remains unchanged.
stored := p.getStored(t, authServer)
require.Equal(t, types.OriginDynamic, stored.Origin())
require.Empty(t, resourceDiff(dynamic, stored))
})
t.Run("start with dynamic, reinit with config file", func(t *testing.T) {
t.Parallel()
conf := setupConfig(t)
// Simulate a server with dynamic configuration.
authServer := initAuthServer(t, conf)
p.setDynamic(t, authServer)
dynamic := p.getStored(t, authServer)
require.Equal(t, types.OriginDynamic, dynamic.Origin())
// Reinitialize with a config-file resource.
configFileRes := p.withConfigFile(t, &conf)
authServer = initAuthServer(t, conf)
// Verify the stored resource is updated.
stored := p.getStored(t, authServer)
require.Equal(t, types.OriginConfigFile, stored.Origin())
require.Empty(t, resourceDiff(configFileRes, stored))
})
t.Run("start with defaults, reinit with config file", func(t *testing.T) {
t.Parallel()
conf := setupConfig(t)
// Simulate a server with the default resource.
defaultRes := p.withDefaults(t, &conf)
authServer := initAuthServer(t, conf)
stored := p.getStored(t, authServer)
require.Equal(t, types.OriginDefaults, stored.Origin())
require.Empty(t, resourceDiff(defaultRes, stored))
// Reinitialize with a config-file resource.
configFileRes := p.withConfigFile(t, &conf)
authServer = initAuthServer(t, conf)
// Verify the stored resource is updated.
stored = p.getStored(t, authServer)
require.Equal(t, types.OriginConfigFile, stored.Origin())
require.Empty(t, resourceDiff(configFileRes, stored))
})
t.Run("start with config file, reinit with another config file", func(t *testing.T) {
t.Parallel()
conf := setupConfig(t)
// Simulate a server with a config-file resource.
configFileRes := p.withConfigFile(t, &conf)
authServer := initAuthServer(t, conf)
stored := p.getStored(t, authServer)
require.Equal(t, types.OriginConfigFile, stored.Origin())
require.Empty(t, resourceDiff(configFileRes, stored))
// Reinitialize with another config-file resource.
anotherConfigFileRes := p.withAnotherConfigFile(t, &conf)
authServer = initAuthServer(t, conf)
// Verify the stored resource is updated.
stored = p.getStored(t, authServer)
require.Equal(t, types.OriginConfigFile, stored.Origin())
require.Empty(t, resourceDiff(anotherConfigFileRes, stored))
})
}
func TestAuthPreference(t *testing.T) {
t.Parallel()
ctx := context.Background()
testDynamicallyConfigurable(t, testDynamicallyConfigurableParams{
withDefaults: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
conf.AuthPreference = types.DefaultAuthPreference()
return conf.AuthPreference
},
withConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
fromConfigFile, err := types.NewAuthPreferenceFromConfigFile(types.AuthPreferenceSpecV2{
Type: constants.OIDC,
SignatureAlgorithmSuite: types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_BALANCED_V1,
})
require.NoError(t, err)
conf.AuthPreference = fromConfigFile
return conf.AuthPreference
},
withAnotherConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
conf.AuthPreference = newWebauthnAuthPreferenceConfigFromFile(t)
conf.AuthPreference.SetSignatureAlgorithmSuite(types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_HSM_V1)
return conf.AuthPreference
},
setDynamic: func(t *testing.T, authServer *auth.Server) {
dynamically, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
SecondFactor: constants.SecondFactorOff,
})
require.NoError(t, err)
_, err = authServer.UpsertAuthPreference(ctx, dynamically)
require.NoError(t, err)
},
getStored: func(t *testing.T, authServer *auth.Server) types.ResourceWithOrigin {
authPref, err := authServer.GetAuthPreference(ctx)
require.NoError(t, err)
return authPref
},
})
}
func TestVnetConfig(t *testing.T) {
t.Parallel()
conf := setupConfig(t)
authServer, err := auth.Init(context.Background(), conf)
require.NoError(t, err)
t.Cleanup(func() { authServer.Close() })
actualVnetConfig, err := authServer.GetVnetConfig(t.Context())
require.NoError(t, err)
defaultVnetConfig, err := vnet.DefaultVnetConfig()
require.NoError(t, err)
require.Empty(t, cmp.Diff(defaultVnetConfig.GetSpec(), actualVnetConfig.GetSpec(), protocmp.Transform()))
}
func TestAuthPreferenceSecondFactorOnly(t *testing.T) {
modules.SetInsecureTestMode(false)
defer modules.SetInsecureTestMode(true)
ctx := context.Background()
conf := setupConfig(t)
authPref, err := types.NewAuthPreferenceFromConfigFile(types.AuthPreferenceSpecV2{
SecondFactor: constants.SecondFactorOff,
})
require.NoError(t, err)
conf.AuthPreference = authPref
_, err = auth.Init(ctx, conf)
require.Error(t, err)
}
func TestClusterNetworkingConfig(t *testing.T) {
t.Parallel()
ctx := context.Background()
testDynamicallyConfigurable(t, testDynamicallyConfigurableParams{
withDefaults: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
conf.ClusterNetworkingConfig = types.DefaultClusterNetworkingConfig()
return conf.ClusterNetworkingConfig
},
withConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
fromConfigFile, err := types.NewClusterNetworkingConfigFromConfigFile(types.ClusterNetworkingConfigSpecV2{
ClientIdleTimeout: types.Duration(7 * time.Minute),
})
require.NoError(t, err)
conf.ClusterNetworkingConfig = fromConfigFile
return conf.ClusterNetworkingConfig
},
withAnotherConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
anotherFromConfigFile, err := types.NewClusterNetworkingConfigFromConfigFile(types.ClusterNetworkingConfigSpecV2{
ClientIdleTimeout: types.Duration(10 * time.Minute),
KeepAliveInterval: types.Duration(3 * time.Minute),
})
require.NoError(t, err)
conf.ClusterNetworkingConfig = anotherFromConfigFile
return conf.ClusterNetworkingConfig
},
setDynamic: func(t *testing.T, authServer *auth.Server) {
dynamically, err := types.NewClusterNetworkingConfigFromConfigFile(types.ClusterNetworkingConfigSpecV2{
KeepAliveInterval: types.Duration(4 * time.Minute),
})
require.NoError(t, err)
dynamically.SetOrigin(types.OriginDynamic)
_, err = authServer.UpsertClusterNetworkingConfig(ctx, dynamically)
require.NoError(t, err)
},
getStored: func(t *testing.T, authServer *auth.Server) types.ResourceWithOrigin {
authPref, err := authServer.GetClusterNetworkingConfig(ctx)
require.NoError(t, err)
return authPref
},
})
}
func TestSessionRecordingConfig(t *testing.T) {
t.Parallel()
ctx := context.Background()
testDynamicallyConfigurable(t, testDynamicallyConfigurableParams{
withDefaults: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
conf.SessionRecordingConfig = types.DefaultSessionRecordingConfig()
return conf.SessionRecordingConfig
},
withConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
fromConfigFile, err := types.NewSessionRecordingConfigFromConfigFile(types.SessionRecordingConfigSpecV2{
Mode: types.RecordOff,
})
require.NoError(t, err)
conf.SessionRecordingConfig = fromConfigFile
return conf.SessionRecordingConfig
},
withAnotherConfigFile: func(t *testing.T, conf *auth.InitConfig) types.ResourceWithOrigin {
anotherFromConfigFile, err := types.NewSessionRecordingConfigFromConfigFile(types.SessionRecordingConfigSpecV2{
Mode: types.RecordAtProxySync,
})
require.NoError(t, err)
conf.SessionRecordingConfig = anotherFromConfigFile
return conf.SessionRecordingConfig
},
setDynamic: func(t *testing.T, authServer *auth.Server) {
dynamically, err := types.NewSessionRecordingConfigFromConfigFile(types.SessionRecordingConfigSpecV2{
Mode: types.RecordAtNodeSync,
})
require.NoError(t, err)
dynamically.SetOrigin(types.OriginDynamic)
_, err = authServer.UpsertSessionRecordingConfig(ctx, dynamically)
require.NoError(t, err)
},
getStored: func(t *testing.T, authServer *auth.Server) types.ResourceWithOrigin {
authPref, err := authServer.GetSessionRecordingConfig(ctx)
require.NoError(t, err)
return authPref
},
})
}
func TestClusterID(t *testing.T) {
t.Parallel()
conf := setupConfig(t)
ctx := context.Background()
authServer, err := auth.Init(ctx, conf)
require.NoError(t, err)
defer authServer.Close()
cc, err := authServer.GetClusterName(ctx)
require.NoError(t, err)
clusterID := cc.GetClusterID()
require.NotEmpty(t, clusterID)
// do it again and make sure cluster ID hasn't changed
authServer, err = auth.Init(ctx, conf)
require.NoError(t, err)
defer authServer.Close()
cc, err = authServer.GetClusterName(ctx)
require.NoError(t, err)
require.Equal(t, clusterID, cc.GetClusterID())
}
// TestClusterName ensures that a cluster can not be renamed.
func TestClusterName(t *testing.T) {
t.Parallel()
conf := setupConfig(t)
ctx := context.Background()
authServer, err := auth.Init(ctx, conf)
require.NoError(t, err)
defer authServer.Close()
// Start the auth server with a different cluster name. The auth server
// should start, but with the original name.
newConfig := conf
newConfig.ClusterName, err = services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
ClusterName: "dev.localhost",
})
require.NoError(t, err)
authServer, err = auth.Init(context.Background(), newConfig)
require.NoError(t, err)
defer authServer.Close()
cn, err := authServer.GetClusterName(ctx)
require.NoError(t, err)
require.NotEqual(t, newConfig.ClusterName.GetClusterName(), cn.GetClusterName())
require.Equal(t, conf.ClusterName.GetClusterName(), cn.GetClusterName())
}
type failingTrustInternal struct {
services.TrustInternal
}
func (t *failingTrustInternal) CreateCertAuthority(ctx context.Context, ca types.CertAuthority) error {
return trace.Errorf("error")
}
// TestInitCertFailureRecovery ensures the auth server is able to recover from
// a failure in the cert creation process.
func TestInitCertFailureRecovery(t *testing.T) {
t.Parallel()
ctx := context.Background()
cap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.SAML,
})
require.NoError(t, err)
conf := setupConfig(t)
// BootstrapResources have lead to an unrecoverable state in the past.
// See https://github.com/gravitational/teleport/pull/49638.
conf.BootstrapResources = []types.Resource{cap}
_, err = auth.Init(ctx, conf, func(s *auth.Server) error {
s.TrustInternal = &failingTrustInternal{
TrustInternal: s.TrustInternal,
}
return nil
})
require.Error(t, err)
_, err = auth.Init(ctx, conf)
require.NoError(t, err)
}
// TestPresets tests behavior of presets
func TestPresets(t *testing.T) {
t.Parallel()
ctx := t.Context()
presetRoleNames := []string{
teleport.PresetEditorRoleName,
teleport.PresetAccessRoleName,
teleport.PresetAuditorRoleName,
teleport.PresetTerraformProviderRoleName,
teleport.PresetWildcardWorkloadIdentityIssuerRoleName,
teleport.PresetAccessPluginRoleName,
teleport.PresetAccessPluginWithReviewRoleName,
teleport.PresetListAccessRequestResourcesRoleName,
teleport.PresetMCPUserRoleName,
}
t.Run("EmptyCluster", func(t *testing.T) {
testModules := modulestest.OSSModules()
as := newTestAuthServer(ctx, t, testModules)
err := auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
require.NoError(t, err)
// Second call should not fail
err = auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
require.NoError(t, err)
// Presets were created
for _, role := range presetRoleNames {
_, err := as.GetRole(ctx, role)
require.NoError(t, err)
}
})
// Makes sure that existing role with the same name is not modified
t.Run("ExistingRole", func(t *testing.T) {
testModules := modulestest.OSSModules()
as := newTestAuthServer(ctx, t, testModules)
access := services.NewPresetEditorRole()
access.SetLogins(types.Allow, []string{"root"})
access, err := as.CreateRole(ctx, access)
require.NoError(t, err)
err = auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
require.NoError(t, err)
// Presets were created
for _, role := range presetRoleNames {
_, err := as.GetRole(ctx, role)
require.NoError(t, err)
}
out, err := as.GetRole(ctx, access.GetName())
require.NoError(t, err)
require.Equal(t, access.GetLogins(types.Allow), out.GetLogins(types.Allow))
})
// If a default allow condition is not present, ensure it gets added.
t.Run("AddDefaultAllowConditions", func(t *testing.T) {
testModules := modulestest.OSSModules()
as := newTestAuthServer(ctx, t, testModules)
editorRole := services.NewPresetEditorRole()
rules := editorRole.GetRules(types.Allow)
// Create a new set of rules based on the Editor Role, excluding the ConnectionDiagnostic.
// ConnectionDiagnostic is part of the default allow rules
var outdatedRules []types.Rule
for _, r := range rules {
if slices.Contains(r.Resources, types.KindConnectionDiagnostic) {
continue
}
outdatedRules = append(outdatedRules, r)
}
editorRole.SetRules(types.Allow, outdatedRules)
editorRole, err := as.CreateRole(ctx, editorRole)
require.NoError(t, err)
// Set up an old Access Role.
// Remove the new DatabaseServiceLabels default
accessRole := services.NewPresetAccessRole()
accessRole.SetDatabaseServiceLabels(types.Allow, types.Labels{})
accessRole, err = as.CreateRole(ctx, accessRole)
require.NoError(t, err)
err = auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
require.NoError(t, err)
outEditor, err := as.GetRole(ctx, editorRole.GetName())
require.NoError(t, err)
allowRules := outEditor.GetRules(types.Allow)
require.Condition(t, func() (success bool) {
for _, r := range allowRules {
if slices.Contains(r.Resources, types.KindConnectionDiagnostic) {
return true
}
}
return false
}, "missing default rule")
outAccess, err := as.GetRole(ctx, accessRole.GetName())
require.NoError(t, err)
allowedDatabaseServiceLabels := outAccess.GetDatabaseServiceLabels(types.Allow)
require.Equal(t, types.Labels{types.Wildcard: []string{types.Wildcard}}, allowedDatabaseServiceLabels, "missing default DatabaseServiceLabels")
})
// Don't set a default allow rule if the resource is present in the role.
// Either as part of allowing or denying rules.
t.Run("DefaultAllowRulesNotAppliedIfExplicitlyDefined", func(t *testing.T) {
testModules := modulestest.OSSModules()
as := newTestAuthServer(ctx, t, testModules)
// Set up a changed Editor Role
editorRole := services.NewPresetEditorRole()
allowRules := editorRole.GetRules(types.Allow)
// Create a new set of rules based on the Editor Role,
// setting a deny rule for a default allow rule
var outdateAllowRules []types.Rule
for _, r := range allowRules {
if slices.Contains(r.Resources, types.KindConnectionDiagnostic) {
continue
}
outdateAllowRules = append(outdateAllowRules, r)
}
editorRole.SetRules(types.Allow, outdateAllowRules)
// Explicitly deny Create to ConnectionDiagnostic
denyRules := editorRole.GetRules(types.Deny)
denyConnectionDiagnosticRule := types.NewRule(types.KindConnectionDiagnostic, []string{types.VerbCreate})
denyRules = append(denyRules, denyConnectionDiagnosticRule)
editorRole.SetRules(types.Deny, denyRules)
editorRole, err := as.CreateRole(ctx, editorRole)
require.NoError(t, err)
// Set up a changed Access Role
accessRole := services.NewPresetAccessRole()
// Remove a default allow label as well.
accessRole.SetDatabaseServiceLabels(types.Allow, types.Labels{})
// Explicitly deny DatabaseServiceLabels
accessRole.SetDatabaseServiceLabels(types.Deny, types.Labels{types.Wildcard: []string{types.Wildcard}})
accessRole, err = as.CreateRole(ctx, accessRole)
require.NoError(t, err)
// Apply defaults.
err = auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
require.NoError(t, err)
outEditor, err := as.GetRole(ctx, editorRole.GetName())
require.NoError(t, err)
allowRules = outEditor.GetRules(types.Allow)
require.Condition(t, func() (success bool) {
for _, r := range allowRules {
if slices.Contains(r.Resources, types.KindConnectionDiagnostic) {
return false
}
}
return true
}, "missing default rule")
outAccess, err := as.GetRole(ctx, accessRole.GetName())
require.NoError(t, err)
allowedDatabaseServiceLabels := outAccess.GetDatabaseServiceLabels(types.Allow)
require.Nil(t, allowedDatabaseServiceLabels, "does not set Allowed DatabaseService Labels")
deniedDatabaseServiceLabels := outAccess.GetDatabaseServiceLabels(types.Deny)
require.Equal(t, types.Labels{types.Wildcard: []string{types.Wildcard}}, deniedDatabaseServiceLabels, "keeps the deny label for DatabaseService")
})
upsertRoleTest := func(t *testing.T, buildType string, expectedPresetRoles []string, expectedSystemRoles []string) {
// test state
ctx := context.Background()
// mu protects created resource maps
var mu sync.Mutex
createdSystemRoles := make(map[string]types.Role)
createdPresets := make(map[string]types.Role)
//
// Test #1 - populating an empty cluster
//
roleManager := newMockRoleManager(t)
// EXPECT that non-system resources will be created once
// and once only.
roleManager.
On("CreateRole", mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
r := args[1].(types.Role)
mu.Lock()
defer mu.Unlock()
require.Contains(t, expectedPresetRoles, r.GetName())
require.NotContains(t, createdPresets, r.GetName())
require.False(t, types.IsSystemResource(r) && r.GetName() != teleport.SystemOktaRequesterRoleName)
createdPresets[r.GetName()] = r
}).
Return(func(_ context.Context, r types.Role) (types.Role, error) {
return r, nil
})
// EXPECT that any (and ONLY) system resources will be upserted
roleManager.
On("UpsertRole", mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
r := args[1].(types.Role)
mu.Lock()
defer mu.Unlock()
require.True(t, types.IsSystemResource(r))
require.Contains(t, expectedSystemRoles, r.GetName())
require.NotContains(t, slices.Collect(maps.Keys(createdSystemRoles)), r.GetName())
createdSystemRoles[r.GetName()] = r
}).
Maybe().
Return(func(_ context.Context, r types.Role) (types.Role, error) {
return r, nil
})
err := auth.CreatePresetRoles(ctx, buildType, roleManager)
require.NoError(t, err)
require.ElementsMatch(t, slices.Collect(maps.Keys(createdPresets)), expectedPresetRoles)
require.ElementsMatch(t, slices.Collect(maps.Keys(createdSystemRoles)), expectedSystemRoles)
roleManager.AssertExpectations(t)
//
// Test #2 - populating an already-populated cluster
//
roleManager = newMockRoleManager(t)
// EXPECT that createPresets will try to create all expected
// non-system roles
roleManager.
On("CreateRole", mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
mu.Lock()
defer mu.Unlock()
require.Contains(t, createdPresets, args[1].(types.Role).GetName())
}).
Return(nil, trace.AlreadyExists("dupe"))
// EXPECT that any (and ONLY) expected system roles will be
// automatically upserted
roleManager.
On("UpsertRole", mock.Anything, mock.Anything).
Run(requireSystemResource(t, 1)).
Maybe().
Return(func(_ context.Context, r types.Role) (types.Role, error) {
return r, nil
})
// EXPECT that all of the roles created in the previous step (and ONLY the
// roles created in the previous step will be queried.
for name, role := range createdPresets {
roleManager.
On("GetRole", mock.Anything, name).
Return(role, nil)
}
err = auth.CreatePresetRoles(ctx, buildType, roleManager)
require.NoError(t, err)
roleManager.AssertExpectations(t)
//
// Test #3 - populating an already-populated cluster with updated presets
//
roleManager = newMockRoleManager(t)
// Removing a specific resource which is part of the Default Allow Rules
// should trigger an UpsertRole call
editorRole := createdPresets[teleport.PresetEditorRoleName]
var allowRulesWithoutConnectionDiag []types.Rule
for _, r := range editorRole.GetRules(types.Allow) {
if slices.Contains(r.Resources, types.KindConnectionDiagnostic) {
continue
}
allowRulesWithoutConnectionDiag = append(allowRulesWithoutConnectionDiag, r)
}
editorRole.SetRules(types.Allow, allowRulesWithoutConnectionDiag)
// EXPECT that createPresets will try to create all expected
// non-system roles
remainingPresets := set.New(expectedPresetRoles...)
roleManager.
On("CreateRole", mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
mu.Lock()
defer mu.Unlock()
r := args[1].(types.Role)
require.Contains(t, createdPresets, r.GetName())
delete(remainingPresets, r.GetName())
}).
Return(nil, trace.AlreadyExists("dupe"))
// EXPECT that all of the roles created in the first step (and ONLY the
// roles created in the first step will be queried.
for name, role := range createdPresets {
roleManager.
On("GetRole", mock.Anything, name).
Return(role, nil)
}
// EXPECT that any system roles will be automatically upserted
// AND our modified editor resource will be updated using an upsert
roleManager.
On("UpsertRole", mock.Anything, mock.Anything).
Return(func(_ context.Context, r types.Role) (types.Role, error) {
if types.IsSystemResource(r) {
require.Contains(t, expectedSystemRoles, r.GetName())
return r, nil
}
require.Equal(t, teleport.PresetEditorRoleName, r.GetName())
return r, nil
})
err = auth.CreatePresetRoles(ctx, buildType, roleManager)
require.NoError(t, err)
require.Empty(t, remainingPresets)
roleManager.AssertExpectations(t)
}
t.Run("Does not upsert roles if nothing changes", func(t *testing.T) {
upsertRoleTest(t, modules.BuildOSS, presetRoleNames, nil)
})
t.Run("Enterprise", func(t *testing.T) {
testModules := modulestest.EnterpriseModules()
enterprisePresetRoleNames := append([]string{
teleport.PresetGroupAccessRoleName,
teleport.PresetRequesterRoleName,
teleport.PresetReviewerRoleName,
teleport.PresetDeviceAdminRoleName,
teleport.PresetDeviceEnrollRoleName,
teleport.PresetRequireTrustedDeviceRoleName,
teleport.SystemOktaRequesterRoleName, // This is treated as a preset
teleport.PresetBeamUserRoleName,
teleport.PresetBeamAdminRoleName,
}, presetRoleNames...)
enterpriseSystemRoleNames := []string{
teleport.SystemAutomaticAccessApprovalRoleName,
teleport.SystemOktaAccessRoleName,
teleport.SystemIdentityCenterAccessRoleName,
teleport.SystemBeamRoleName,
}
enterpriseUsers := []types.User{
services.NewSystemAutomaticAccessBotUser(testModules.BuildType()),
}
t.Run("EmptyCluster", func(t *testing.T) {
as := newTestAuthServer(ctx, t, testModules)
// Run multiple times to simulate starting auth on an
// existing cluster and asserting that everything still
// returns success
for range 2 {
err := auth.CreatePresetRoles(ctx, testModules.BuildType(), as)
require.NoError(t, err)
err = auth.CreatePresetUsers(ctx, testModules.BuildType(), as)
require.NoError(t, err)
}
// Preset Roles were created
for _, role := range append(enterprisePresetRoleNames, enterpriseSystemRoleNames...) {
_, err := as.GetRole(ctx, role)
require.NoError(t, err)
}
// Preset Users were created
for _, user := range enterpriseUsers {
_, err := as.GetUser(ctx, user.GetName(), false)
require.NoError(t, err)
}
})
t.Run("Does not upsert roles if nothing changes", func(t *testing.T) {
upsertRoleTest(t, testModules.BuildType(), enterprisePresetRoleNames, enterpriseSystemRoleNames)
})
t.Run("System users are always upserted", func(t *testing.T) {
ctx := t.Context()
sysUser := services.NewSystemAutomaticAccessBotUser(testModules.BuildType()).(*types.UserV2)
// GIVEN a user database...
manager := newMockUserManager(t)
// Set the expectation that all user creations will succeed EXCEPT
// for our known system user
manager.On("CreateUser", mock.Anything, mock.Anything).
Run(requireSystemResource(t, 1)).
Maybe().
Return(sysUser, nil)
// All attempts to upsert should succeed, and record the being upserted
var upsertedUsers []string
manager.On("UpsertUser", mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
u := args.Get(1).(types.User)
upsertedUsers = append(upsertedUsers, u.GetName())
}).
Return(sysUser, nil)
// WHEN I attempt to create the preset users...
err := auth.CreatePresetUsers(ctx, testModules.BuildType(), manager)
// EXPECT that the process succeeds and the system user was upserted
require.NoError(t, err)
manager.AssertExpectations(t)
require.Contains(t, upsertedUsers, sysUser.Metadata.Name)
})
})
}
func TestDashboardMode(t *testing.T) {
t.Parallel()
conf := setupConfig(t)
// dashboard mode is determined via cloud and recovery codes
conf.Modules = &modulestest.Modules{
TestBuildType: modules.BuildEnterprise,
TestFeatures: modules.Features{
Cloud: false,
RecoveryCodes: true,
},
}
ctx := t.Context()
authServer, err := auth.Init(ctx, conf)
require.NoError(t, err)
t.Cleanup(func() {
err = authServer.Close()
require.NoError(t, err)
})
// verify auth server is functional
_, err = authServer.GetClusterName(ctx)
require.NoError(t, err)
// verify that preset roles were NOT created in dashboard mode
presetRoles := auth.GetPresetRoles(conf.Modules.BuildType())
for _, role := range presetRoles {
_, err := authServer.GetRole(ctx, role.GetName())
require.True(t, trace.IsNotFound(err), "expected preset role %q to not exist in dashboard mode", role.GetName())
}
// verify preset users were NOT created in dashboard mode
presetUsers := auth.GetPresetUsers(conf.Modules.BuildType())
for _, user := range presetUsers {
_, err := authServer.GetUser(ctx, user.GetName(), false)
require.True(t, trace.IsNotFound(err), "expected preset user %q to not exist in dashboard mode", user.GetName())
}
}
func TestGetPresetUsers(t *testing.T) {
t.Parallel()
// no preset users for OSS
require.Empty(t, auth.GetPresetUsers(modules.BuildOSS))
// preset user @teleport-access-approval-bot on enterprise
require.Equal(t, []types.User{
services.NewSystemAutomaticAccessBotUser(modules.BuildEnterprise),
}, auth.GetPresetUsers(modules.BuildEnterprise))
}
type mockUserManager struct {
mock.Mock
}
func newMockUserManager(t *testing.T) *mockUserManager {
m := &mockUserManager{}
m.Test(t)
return m
}
func (m *mockUserManager) CreateUser(ctx context.Context, user types.User) (types.User, error) {
type delegateFn = func(context.Context, types.User) (types.User, error)
args := m.Called(ctx, user)
if delegate, ok := args.Get(0).(delegateFn); ok {
return delegate(ctx, user)
}
return args.Get(0).(types.User), args.Error(1)
}
func (m *mockUserManager) GetUser(ctx context.Context, username string, withSecrets bool) (types.User, error) {
type delegateFn = func(ctx context.Context, username string, withSecrets bool) (types.User, error)
args := m.Called(ctx, username, withSecrets)
if delegate, ok := args.Get(0).(delegateFn); ok {
return delegate(ctx, username, withSecrets)
}
return args.Get(0).(types.User), args.Error(1)
}
func (m *mockUserManager) UpsertUser(ctx context.Context, user types.User) (types.User, error) {
type delegateFn = func(context.Context, types.User) (types.User, error)
args := m.Called(ctx, user)
if delegate, ok := args.Get(0).(delegateFn); ok {
return delegate(ctx, user)
}
return args.Get(0).(types.User), args.Error(1)
}
var _ auth.PresetUsers = &mockUserManager{}
type mockRoleManager struct {
mock.Mock
}
var _ auth.PresetRoleManager = &mockRoleManager{}
func newMockRoleManager(t *testing.T) *mockRoleManager {
m := &mockRoleManager{}
m.Test(t)
return m
}
// CreateRole creates a role.
func (m *mockRoleManager) CreateRole(ctx context.Context, role types.Role) (types.Role, error) {
type delegateFn = func(context.Context, types.Role) (types.Role, error)
args := m.Called(ctx, role)
if delegate, ok := args[0].(delegateFn); ok {
return delegate(ctx, role)
}
if args[0] == nil {
return nil, args.Error(1)
}
return args[0].(types.Role), args.Error(1)
}
func (m *mockRoleManager) GetRole(ctx context.Context, name string) (types.Role, error) {
type delegateFn = func(context.Context, string) (types.Role, error)
args := m.Called(ctx, name)
if delegate, ok := args[0].(delegateFn); ok {
return delegate(ctx, name)
}
if args[0] == nil {
return nil, args.Error(1)
}
return args[0].(types.Role), args.Error(1)
}
func (m *mockRoleManager) UpsertRole(ctx context.Context, role types.Role) (types.Role, error) {
type delegateFn = func(context.Context, types.Role) (types.Role, error)
args := m.Called(ctx, role)
if delegate, ok := args[0].(delegateFn); ok {
return delegate(ctx, role)
}
return args[0].(types.Role), args.Error(1)
}
func requireSystemResource(t *testing.T, argno int) func(mock.Arguments) {
return func(args mock.Arguments) {
argOfInterest := args[argno]
require.Implements(t, (*types.Resource)(nil), argOfInterest)
require.True(t, types.IsSystemResource(argOfInterest.(types.Resource)))
}
}
func setupConfig(t *testing.T) auth.InitConfig {
tempDir := t.TempDir()
bk, err := memory.New(memory.Config{})
require.NoError(t, err)
processStorage, err := storage.NewProcessStorage(
context.Background(),
filepath.Join(tempDir, teleport.ComponentProcess),
)
require.NoError(t, err)
clusterName, err := services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{
ClusterName: "me.localhost",
})
require.NoError(t, err)
t.Cleanup(func() {
bk.Close()
processStorage.Close()
})
authority, err := testauthority.NewKeygen(modules.BuildOSS, time.Now)
require.NoError(t, err)
return auth.InitConfig{
DataDir: tempDir,
HostUUID: "00000000-0000-0000-0000-000000000000",
NodeName: "foo",
Backend: bk,
VersionStorage: processStorage,
Authority: authority,
ClusterAuditConfig: types.DefaultClusterAuditConfig(),
ClusterNetworkingConfig: types.DefaultClusterNetworkingConfig(),
SessionRecordingConfig: types.DefaultSessionRecordingConfig(),
ClusterName: clusterName,
StaticTokens: types.DefaultStaticTokens(),
AuthPreference: types.DefaultAuthPreference(),
SkipPeriodicOperations: true,
Tracer: tracing.NoopTracer(teleport.ComponentAuth),
}
}
func newWebauthnAuthPreferenceConfigFromFile(t *testing.T) types.AuthPreference {
ap, err := types.NewAuthPreferenceFromConfigFile(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorWebauthn,
Webauthn: &types.Webauthn{
RPID: "localhost",
},
})
require.NoError(t, err)
return ap
}
// Example resources generated using `tctl get all --with-secrets`.
const (
hostCAYAML = `kind: cert_authority
metadata:
name: me.localhost
revision: 6cc380df-816d-4c6b-8fc8-c20af75eede2
spec:
active_keys:
ssh:
- private_key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSU5WTzBiNUxSOXk2Nm5SRGJHN3JJUzFRZ3dBcUVpSWtMZS9WVmFrd3pJZ2oKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
public_key: c3NoLWVkMjU1MTkgQUFBQUMzTnphQzFsWkRJMU5URTVBQUFBSUw2ZWtVSTg3U3VOYkFiWnhPbGxRUEJJWGdPVjFNcEt4UWVNQXB0MklpVlYK
tls:
- cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNBakNDQWFlZ0F3SUJBZ0lSQUpKYkRNWGtYN3ZOR0EreFRQeGlzYzR3Q2dZSUtvWkl6ajBFQXdJd1lERVYKTUJNR0ExVUVDaE1NYldVdWJHOWpZV3hvYjNOME1SVXdFd1lEVlFRREV3eHRaUzVzYjJOaGJHaHZjM1F4TURBdQpCZ05WQkFVVEp6RTVORFUwTURBME5UUTJOakkyTlRrMk1qazBOakkxTURNNU5UYzJNamM1TkRBNE1qYzJOakFlCkZ3MHlOREV5TXpFeE5ESTJNRFphRncwek5ERXlNamt4TkRJMk1EWmFNR0F4RlRBVEJnTlZCQW9UREcxbExteHYKWTJGc2FHOXpkREVWTUJNR0ExVUVBeE1NYldVdWJHOWpZV3hvYjNOME1UQXdMZ1lEVlFRRkV5Y3hPVFExTkRBdwpORFUwTmpZeU5qVTVOakk1TkRZeU5UQXpPVFUzTmpJM09UUXdPREkzTmpZd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxCmhrak9QUU1CQndOQ0FBUzZTZE92WEdZV2wyY2FCRHFPcXRRN3RyNW1JWkdKR0JFYXJWUnMvSFFvYXpiZExKK0IKMExMUFhHemQvOTNxZ04wNXJVWGhyNHVXb3pEeTMxR2V4ZDZmbzBJd1FEQU9CZ05WSFE4QkFmOEVCQU1DQVlZdwpEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVLWmhpWVRwc3Z0NTkwTXc1OGhPY2xNcTBVYmN3CkNnWUlLb1pJemowRUF3SURTUUF3UmdJaEFLcFExNWt6MFZlQThmOEE5S3RKRS9LLytxVkkyNGFpMnM5dytmYTQKVFNFdkFpRUEwWWtNZGNFc1dqTjBFeFpzVmVsRU5EdS9hcWhtUkpua2FpakJDNjFBY0Q0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ3RTcVllSWhXeTZ3S3QxVDYKUVdVSitTVkF0SjRpNkppajhac0hmWkw2YzJXaFJBTkNBQVM2U2RPdlhHWVdsMmNhQkRxT3F0UTd0cjVtSVpHSgpHQkVhclZScy9IUW9hemJkTEorQjBMTFBYR3pkLzkzcWdOMDVyVVhocjR1V296RHkzMUdleGQ2ZgotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==
additional_trusted_keys: {}
cluster_name: me.localhost
type: host
sub_kind: host
version: v2`
userCAYAML = `kind: cert_authority
metadata:
name: me.localhost
revision: b82425a2-dd10-49c6-9b8a-3f10e3ec8a41
spec:
active_keys:
ssh:
- private_key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSU1tK2EyVlRqSHV2OVhrWG1UR2Roem5GSVJPK0pTaVEyWUF6eHNleEJFZGEKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
public_key: c3NoLWVkMjU1MTkgQUFBQUMzTnphQzFsWkRJMU5URTVBQUFBSUtZYU1pUWZTUWgyQVpqN2lNSEFBQUpxU0FQWWJLL0gzRFJ3NWtUSXBDRUMK
tls:
- cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNBakNDQWFlZ0F3SUJBZ0lSQUpsUkc4OFlqTURVUmZmOENYMnRpbkF3Q2dZSUtvWkl6ajBFQXdJd1lERVYKTUJNR0ExVUVDaE1NYldVdWJHOWpZV3hvYjNOME1SVXdFd1lEVlFRREV3eHRaUzVzYjJOaGJHaHZjM1F4TURBdQpCZ05WQkFVVEp6SXdNemM1TXpBeU16UXpNelV5TURFNE9URXdNRFU0TnpNek9EWXlNalkxT1RVMk1qQTVOakFlCkZ3MHlOREV5TXpFeE5ESTJNRFphRncwek5ERXlNamt4TkRJMk1EWmFNR0F4RlRBVEJnTlZCQW9UREcxbExteHYKWTJGc2FHOXpkREVWTUJNR0ExVUVBeE1NYldVdWJHOWpZV3hvYjNOME1UQXdMZ1lEVlFRRkV5Y3lNRE0zT1RNdwpNak0wTXpNMU1qQXhPRGt4TURBMU9EY3pNemcyTWpJMk5UazFOakl3T1RZd1dUQVRCZ2NxaGtqT1BRSUJCZ2dxCmhrak9QUU1CQndOQ0FBUlN0T0tLYklVeGppSTFIZEJGU2xVQW1LQW0wUXlxejljWVZEdlZEc0RNT1BqNFRTWGsKOHp5Q0FhYXloTlgra21lZlU3R2ZIeDBlVVMxbVhBYnl6UnNHbzBJd1FEQU9CZ05WSFE4QkFmOEVCQU1DQVlZdwpEd1lEVlIwVEFRSC9CQVV3QXdFQi96QWRCZ05WSFE0RUZnUVVKZi94RjBtMStZM1lIRlRubVJhcGJ1dVdqa293CkNnWUlLb1pJemowRUF3SURTUUF3UmdJaEFLZkU5bXpPdzcrcUx3bEFxdFFWUVBNODVFTGp2NlFSdmZrcUE3aWoKSEtCMUFpRUFoNjVJZmV4NFpwT0szYVVUVTZQWGFkODlDdmhVb3NFMzRzWU1FZHdxZk9zPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZzRXUVZQclNGTmR6bVFMRWcKQkMvTU5sb2JEL0dKekNGU01vOUZsV0pjZDY2aFJBTkNBQVJTdE9LS2JJVXhqaUkxSGRCRlNsVUFtS0FtMFF5cQp6OWNZVkR2VkRzRE1PUGo0VFNYazh6eUNBYWF5aE5YK2ttZWZVN0dmSHgwZVVTMW1YQWJ5elJzRwotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==
additional_trusted_keys: {}
cluster_name: me.localhost
type: user
sub_kind: user
version: v2`
databaseClientCAYAML = `kind: cert_authority
metadata:
name: me.localhost
revision: 797ca33f-e922-45db-b531-ab62af23963c
spec:
active_keys:
tls:
- cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURqRENDQW5TZ0F3SUJBZ0lRWGtnVThDZFo3TnkrbVJacG9GZUwzVEFOQmdrcWhraUc5dzBCQVFzRkFEQmcKTVJVd0V3WURWUVFLRXd4dFpTNXNiMk5oYkdodmMzUXhGVEFUQmdOVkJBTVRERzFsTG14dlkyRnNhRzl6ZERFdwpNQzRHQTFVRUJSTW5NVEkxTXpJeE56QXhOalV5TnpJMk16QTBORE13TlRRM05EQTNNVEE0TXpjM05qUXpPVGszCk1CNFhEVEkwTVRJek1URTBNall3TmxvWERUTTBNVEl5T1RFME1qWXdObG93WURFVk1CTUdBMVVFQ2hNTWJXVXUKYkc5allXeG9iM04wTVJVd0V3WURWUVFERXd4dFpTNXNiMk5oYkdodmMzUXhNREF1QmdOVkJBVVRKekV5TlRNeQpNVGN3TVRZMU1qY3lOak13TkRRek1EVTBOelF3TnpFd09ETTNOelkwTXprNU56Q0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFMMDZCa0hXMTBpWm1NTHZ6Y01tNC9WNmk3M2EvWjNpM1hzN1IxamYKc0FDQlB5TkNJTkFCbnFoV0NHdWJPb0JWT3REemQyVUJTZ0JXYVVNcXhiUytmQjBLYllEVDVkaFN1YXd4Wk8zQwpmN0xaZWtlZk4vb1FZSjk2WlVoemVvL1g4dkZZNE5ocXBIbXJMV1VRTGpZUEZ5cVhIWGM1L3hTUkVPVm8ybnZyCjNOWXFnR2EwRUFvMTRvRUJsR20zMmNQUWNvTGRBUUhqaFJrampQYStNeXRGSmNzeFkxWTFDemM5V1hnYzZvK1AKRWp0U3dRMW9Fd3ZOVHhYbXp1UElOcTZxdVZjS1kwcDF0anBWT1R1dWpaZ1R5NGRIMkJIQ1I5d1ZxejlVRGJPWgpWVHVKWjVnRFdQQkpGcVRPNUhkbzh5QnVLOXM0RndYZDJoaC9oa1YrNitabXdJRUNBd0VBQWFOQ01FQXdEZ1lEClZSMFBBUUgvQkFRREFnR21NQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdIUVlEVlIwT0JCWUVGRTZ4R09aSmFoTzUKc3FmQ3pZK0ZxTjQ3eGpWeE1BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQk41V2JLdVRzaWlvUlltS3BlcGtiTQppT2FCbGJ4ajZtOTZDVHZpczJSeXlmUU1qSXduT1dYbUtMMHpGQUtvZThDWVk0UEJFWEtuZzZBd0NNVXdzWnNPCkZPMFE2Q21JUzlwQXdVZURIZ3VrN2x2dWQyeGFvb0MrazY4OGtmUlZhRjVPTW5WaW4xajBJY1MwM1d1QlhSckwKVDVwODFza3JOaG9Gd1BtdnU0RkZmU1lHb1YrajQ4NC83Y0FOY3J2c0FFdFBFdk9EdzlUVG5vb05zdUoreVhaMAoyVW5LSG1rOTFCeGdZVDVHUE1CVkYwQWxzenhjcVN0S2hBMlhrdzBCY1pnTXVBa3BYSXJRbUZNNGh0RnIyY0NOCkpTTk1mVUpsZVlXLzJiNmJvbENZTUliTFoxeldaaDBVYTJ6QjlOME9wNE1aS25xRmhtc2JVWFFOa0xGd1FEYzAKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFcFFJQkFBS0NBUUVBdlRvR1FkYlhTSm1Zd3UvTnd5Ymo5WHFMdmRyOW5lTGRlenRIV04rd0FJRS9JMElnCjBBR2VxRllJYTVzNmdGVTYwUE4zWlFGS0FGWnBReXJGdEw1OEhRcHRnTlBsMkZLNXJERms3Y0ovc3RsNlI1ODMKK2hCZ24zcGxTSE42ajlmeThWamcyR3FrZWFzdFpSQXVOZzhYS3BjZGR6bi9GSkVRNVdqYWUrdmMxaXFBWnJRUQpDalhpZ1FHVWFiZlp3OUJ5Z3QwQkFlT0ZHU09NOXI0ekswVWx5ekZqVmpVTE56MVplQnpxajQ4U08xTEJEV2dUCkM4MVBGZWJPNDhnMnJxcTVWd3BqU25XMk9sVTVPNjZObUJQTGgwZllFY0pIM0JXclAxUU5zNWxWTzRsbm1BTlkKOEVrV3BNN2tkMmp6SUc0cjJ6Z1hCZDNhR0grR1JYN3I1bWJBZ1FJREFRQUJBb0lCQUM4MnF4a0NZZlRiWGlKRgpjekdlSW9LOWNPQ09JM21oZ1dHZUNNOUVBTVlmZVlGeW5uMUg2aTVXU1FPUVY2aHRtNTlISUNNemp5TkdiRDAyCkR0NXFLTTJXTEh4WVlxRDNBeHpUdGpzY3JJQVRnMDhiaXZ2NTJpSHdpQlRydTBqb3VOVS9OOXJId1FJYWs5a0QKa0lRc2Y3dEF1VGxtWHg3aWt6U3FWTmxXb0dOUENZek9Bb3FZcXlTaWhEc0pLZ3N1SVFOV0lPVzE1dGxmb0NsQgpTTkF0cGxlclBNbFl6T2FjaUZVWkJFbGVOWEp0VURwd0szOGU5N2RMclJrYWRLeXhSVTNVTTE4dFR5TDZrZ2hMCmVsT3JBWDE2K0RFdm1PeDhGQlgyS29tak9OL0pORVZUN2lkcndyVnh2OU1PTUFpQjRDMms0a25jbUxIVE5DTE0KelJmOHY0RUNnWUVBNVg1d0VXVlZ0WFhNek02VVZCZ1ZwMGZmNVlPTTNTV3FoOHE1OGQrLzVBS0ZZSjZ5U1BpLwpoaVJvZ2xXc2NheXV3UVhTaHVMV2ZQVHJxMmVQNFVlcGdEa0g0c3NiamFwODFTMFZnN2JJZ3ZrdGxOTjdFMkRJCnFhMS9pNzBBQW9GOFNNY20vRE00N3RiTGxxS0ZwT21GSjB3dk1zUkh5aTJGM3pRaDB1aTFTQThDZ1lFQTB4VDgKSFZISnl1VHEvMERBdi9kLy9RNEtFUW5CS3ZmaFV5K0FOd0NGNkFhRW5USnAzTTRmNkpmUkVRUytFd1ZVVFk5WAoxWkNBdTd5N2NoQmtORG5TMmZJR2ZiQnYramVGWm9SZDRkQ3RwUnB1bHJka21yNGFaZnZnaGZsanJkdVZrdEM3CmZtQjZORTl5TURMYVBWT3Z4VEZGd3ZYMHRRUFdmYVNpL0Z3NVhtOENnWUVBdUtJT201QkJjbXBCeUl4eXZXMWIKRG1nKzg3SHdoSU1uUFhTV1FNaFk0Nkk3bUU1VTlXeGEraHNVa2JkSHMzVFFhNjY1ZjVmRUpHZ1BxcWo1RXEvSwo2TVA1V2pjNkJiR2lHUWZhaFV0cTZpUjZ6WCtQUnpuWWR0cUZBUEdmcm1ScWowcmFUSkVSUHVaRWlQNWNNeDlFCjV5YmQyaVFiOWNiR0s1c1BrMVZ4YzNVQ2dZRUFybkVzUGNyRzJyKyttYjVZelF6c29DUkhHM2VWUlQ1ZjM5QmsKeEkvUkdrU3d1ZnpjMGhjaTlhVHBxWWZpMFhOWkRWUUdRYi9QTTlld2pYNlFZVHpjVFRPZ082VmhsVWJuSHljTApNMEN6RUx3OFlxQWpLMk1xQzloUjRFYVBJekpTZFdlOVc1Njl2NWRjaGdxd28zZ1N6Z04vWkxUQlRBdGs2cWJ4CjcxOEVKazhDZ1lFQXZGckFPc1VJdmh4T0FYYk5mMUxwbnlabXVORUg3ek9XZ1FNM1ZlNG9iT3hqenVHaGs2RFUKVGhFdDg0emhsMk5LeFVacHRwdEJLRkdxM3B2dGc2TElqbEtpWmNPUEovbHcrTVZPb0U0S1Y5U0F3Q1VPN3o4eApDbTlzM05ZQTV6RWZRdGg0OEt5NmdBZVhZUHlkMWZidjl2QXlwM3FQbEpxa281SG5peHNxamNvPQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQo=
additional_trusted_keys: {}
cluster_name: me.localhost
type: db_client
sub_kind: db_client
version: v2`
databaseCAYAML = `kind: cert_authority
metadata:
name: me.localhost
revision: 3b019bb5-051c-461a-a7f4-e335fc031bf3
spec:
active_keys:
tls:
- cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURqVENDQW5XZ0F3SUJBZ0lSQUs5OUdYVGFmVEhNNWN2cEpzK0xhTkV3RFFZSktvWklodmNOQVFFTEJRQXcKWURFVk1CTUdBMVVFQ2hNTWJXVXViRzlqWVd4b2IzTjBNUlV3RXdZRFZRUURFd3h0WlM1c2IyTmhiR2h2YzNReApNREF1QmdOVkJBVVRKekl6TXpJMk5EUTFNalk0T0RBd016RTFOekl3TWpNeE16TTVOekl3T0RZMU5UVTFORGMyCk9UQWVGdzB5TkRFeU16RXhOREkyTURaYUZ3MHpOREV5TWpreE5ESTJNRFphTUdBeEZUQVRCZ05WQkFvVERHMWwKTG14dlkyRnNhRzl6ZERFVk1CTUdBMVVFQXhNTWJXVXViRzlqWVd4b2IzTjBNVEF3TGdZRFZRUUZFeWN5TXpNeQpOalEwTlRJMk9EZ3dNRE14TlRjeU1ESXpNVE16T1RjeU1EZzJOVFUxTlRRM05qa3dnZ0VpTUEwR0NTcUdTSWIzCkRRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRE8ybGRZbjhIc3ZNWTkzZVFMSWRoMUk5WmJtWWFlZm80UHJJRTgKNXA0bUpSUnVMMDg2Y0xlbFZNZDc0K1h3RmlSOUpLNlY1ZHBYVGw1cFoxTVBEbjh1cUkvMjAwWnlvVHlad1ZWUgpISUdwSjFqTVRZbjBQcDE0TC81Uk9MbXAwVTEzL3hRVDd4a2QxZnA4QVdzVzFYYnloc1hYaEhkMUpraTRFY0djCkJtSjc0Nml3aGxyUVNZcTNvSFpXNzZjdVVaM0g2bU9QVzVZenl1cUVYR3J3SUxneExZSVZNNXlxTUpEbHdiTU8KMzlBTGE4bzl1U1ZiaGJ1QjlaeG9BT1h4ck9ERGJSN0RuVTc0cEFTQm5pV2g5aTFEMTBBK2NlMEQySVNjTFcvWAo4MXpKSzVGSXRZSldSVDQvZUo3MmNMUGUycFRlSTMreEFCNW9tZVBCQjlrZjlLanRBZ01CQUFHalFqQkFNQTRHCkExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlFacW9pb3paYVoKMTVOcFMrVjVUV1RUNnY0S0V6QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFPZFhnYk1XWUM0TFdmMnRtNUtyZQoyM2J5MlgxWHZYQ2U4L1RGdFVsMUNhdHVrV2MwY205cXUxckxiRy84ZUxCWjA2cUZSc1VTSEUxamEyRUtkREU0CjU5Y0p3bE5DN1hCMjRTQ2s1QTcxNGtpSDZMQjAxRk00WGEzWjg4YnZCSHV2ak01Q1ErN1lSSjlGZFd6YjFKK3UKREF3czI1V0haWTFGV0ovUGt5eTVycjJNTWVvS3dWMGRNSjJJWlZVMmpGZkVhYXhLWml4Y29EbG5KclIzWThEMwovUVZRckY5d0xkQWVENkExTlpjbnd3Q2F0MWFoOVA1cWhQdGlnc2lsOVM5dkd3T0JyQUEwNE9iZXQrVFNjd0tnCkVQQWhFWVlkRWUvRzV6MVZUQXBCUCtzeXRWcHdRZXQxZDRlV2VvVW04QU8yZEVuQWhhM0FvU1pmQjFTVVgxTmEKb3c9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBenRwWFdKL0I3THpHUGQza0N5SFlkU1BXVzVtR25uNk9ENnlCUE9hZUppVVViaTlQCk9uQzNwVlRIZStQbDhCWWtmU1N1bGVYYVYwNWVhV2RURHc1L0xxaVA5dE5HY3FFOG1jRlZVUnlCcVNkWXpFMkoKOUQ2ZGVDLytVVGk1cWRGTmQvOFVFKzhaSGRYNmZBRnJGdFYyOG9iRjE0UjNkU1pJdUJIQm5BWmllK09vc0laYQowRW1LdDZCMlZ1K25MbEdkeCtwamoxdVdNOHJxaEZ4cThDQzRNUzJDRlRPY3FqQ1E1Y0d6RHQvUUMydktQYmtsClc0VzdnZldjYUFEbDhhemd3MjBldzUxTytLUUVnWjRsb2ZZdFE5ZEFQbkh0QTlpRW5DMXYxL05jeVN1UlNMV0MKVmtVK1AzaWU5bkN6M3RxVTNpTi9zUUFlYUpuandRZlpIL1NvN1FJREFRQUJBb0lCQUgxbTZ2c2taeG1SWEJHWAptcStSQmp3RnpPZGRUTHA3ZUw1UjAwdkxkK2NpSloraStNSXlJWE9PMFJ6dmphK2VqT0o5UVlaSWdiVGFJdXg5Cm9tSUhaTjB4ZlkyaWlodm1XZW5ReGx0VkQ5b3ZxMnE0TzBFaVVLN1RVYmVGenpEL1hacTR2a0JUZklPVS9MVCsKMnlCTnF6M2VyTVE2WDMxYkIwem9IdHJyRi91SWNzNHNnZXFOTWFtU2JUY2I1ODRSbHhUNDA0MUdlWWdtNlZCRQpHWnpRSjJESXlQbEExZFhGUW5lcHpaaWRqQmdJMWJKS3lTMzVsekdjWVc0aGgvbVFhUVp5ZmRuS3Z4QmlsdUlBCjBxcnAvTEQ4QVp6RWxONVNMb2JtMXVEZEIrRW1RNllKbDEvdm5iWEY1QWFXSDN6b3U1OHJDcGFudDcraVJ6OGwKNFhuWjl0VUNnWUVBN1dPajhMSDBYVnFMcDMyTnNrK2xnNmYwRSttVUZnMU93eWZGSEJPdlpsRDQ1T3ZQNWx6ZgpsLzNoaUJiZEMzNHNhWU1HdW9GZlRhMjJiVHpwTXl6eXFhZVRhdy9HS3psR2Jlc0dIRk9Xd3F2U2Ivd2tWTEUvCkZocTZGSE1vWGp0aTE4eFhjRXdJQmVQaHhVZVkrZkd3V0RTREV1c3NZdFFxQjh1ajBXaDlYUDhDZ1lFQTN4SFgKbUhpdjFWLytPMkNPZDdWUkFWaThKTS9JZ0NQTk84L0o2SWRYTkNwKy8wNW10dEh0QnpyVFN0cXBGMlBSUWJEaApjYmpuYk05TGJTTVNKTHNtYndTOEhtT3MwSGQwSzU1aXdqRmFxNUdOT0xBQmlrOGMrU2Naa0d5YitxRHlGRU9UCkMzMThFSEg5SlZ6L24zYWxHZmRGWG9xWkZ1RXJvQWJCTkFEZlBoTUNnWUJsZHkxZmQvQ201a2pDOGx0YVY4aTcKR1ZLdUlDeDNzSUIxMGMzaVRsZXVOL1hxZ3hCOXVqeW56cEJUaHRJOFUxWFFVM3pRd3ZObFZGYWhJbVBheDkrQQp2R3U2V3llczJmSk1rU1F2ZjFyMUlsUDBJYVcxdlh6bGljNzNackZlZGF1dDZWMkdWamtucTF1WTR4MXoxK1kwCkRWM28vRFFnbWViTkpqR0RGRkpoS1FLQmdEaG1VSFp5ZlRLYjFMRzZsZ3JhUXlMdUJwUGdIVGVZMWJrN3JqY20Ka1B2VmlzcU9UaFlIT2NETU5NUUdTUjVxMUd1aGh6NnptMys5WWJxMFZWQUlLWTJFU3ZQOEM2T2hzRE9mRmlVMwpTVTk3dTVNTG5UZ1ZES1JLS0lLRmsySm84d3dBa2RzajNReGpaYmZlclpycDZwQ0lIbmZxM3c0VDNHM1hoMTNZCm9wa1ZBb0dCQU5USGlsZHVuWXBCTXNFVks5TFRWSm95TlNOTVdBbGFGaWlwZllmWm82M3RyQzgxK0FtMHFDQW4KZTF1V2NwRFFkQlZzbGRnTWV2TEcwOUNBU05rRjN0ZUJqWEsranNzWTRWMllTU1Bwd0dyWWhjZHpFMFBIZEVROApiMGllSGJUMGRZZ3hjWmczbW9pN1BDZi90d3Joa2lDdllIMlBMR2hzMkduV1JzSEQzcHY1Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
additional_trusted_keys: {}
cluster_name: me.localhost
type: db
sub_kind: db
version: v2`
jwtCAYAML = `kind: cert_authority
metadata:
name: me.localhost
revision: a3f0310c-80fe-4c43-95d5-ba3f3cf9aedc
spec:
active_keys:
jwt:
- private_key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ3dFaDQ2empZTHltT0ZqNWIKSVZDNXBkMUxSbWU1clAzTmRVN3hKQ0plMDJ1aFJBTkNBQVRYQXA5MFRRTmowaDhCNHllR0dnNFJWQStsbVV4MwpRN3EzdFBLbEJaSVpiMWtXSHdSRmFXdUJmYUQ3Sis0MnUwSjRvVGxqYU5zUUg2Vm8wRXRLTkI4ZwotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tCg==
public_key: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFMXdLZmRFMERZOUlmQWVNbmhob09FVlFQcFpsTQpkME82dDdUeXBRV1NHVzlaRmg4RVJXbHJnWDJnK3lmdU5ydENlS0U1WTJqYkVCK2xhTkJMU2pRZklBPT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==
additional_trusted_keys: {}
cluster_name: me.localhost
type: jwt
sub_kind: jwt
version: v2`
openSSHCAYAML = `kind: cert_authority
metadata:
name: me.localhost
revision: 2c49b2de-7529-4e8f-9442-dec2357f2e93
spec:
active_keys:
ssh:
- private_key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1DNENBUUF3QlFZREsyVndCQ0lFSUhoT0o3KzZlek4raTZ5dVVhVDBPT01rVm1NK0pIZm41Sm5ONzJFNmduWTgKLS0tLS1FTkQgUFJJVkFURSBLRVktLS0tLQo=
public_key: c3NoLWVkMjU1MTkgQUFBQUMzTnphQzFsWkRJMU5URTVBQUFBSVBuQ0ZocHk3TUdBZTNBSDBjTDBkMmVUK20xNVF1dmptMUU2QXlubm1uSkYK
additional_trusted_keys: {}
cluster_name: me.localhost
type: openssh
sub_kind: openssh
version: v2`
samlCAYAML = `kind: cert_authority
metadata:
name: me.localhost
revision: 252e2c73-6805-48d4-8491-dde615ff8c47
spec:
active_keys:
tls:
- cert: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURqVENDQW5XZ0F3SUJBZ0lSQUtDdHRGNGZ1V0ZTSzJaNm1mb3VPcmt3RFFZSktvWklodmNOQVFFTEJRQXcKWURFVk1CTUdBMVVFQ2hNTWJXVXViRzlqWVd4b2IzTjBNUlV3RXdZRFZRUURFd3h0WlM1c2IyTmhiR2h2YzNReApNREF1QmdOVkJBVVRKekl4TXpVM09EUXdORGszTXpFd056RTBORFkxTWpJMk9EVXlOelV5T0Rjek1qRTBOak0yCk1UQWVGdzB5TkRFeU16RXhOREkyTURaYUZ3MHpOREV5TWpreE5ESTJNRFphTUdBeEZUQVRCZ05WQkFvVERHMWwKTG14dlkyRnNhRzl6ZERFVk1CTUdBMVVFQXhNTWJXVXViRzlqWVd4b2IzTjBNVEF3TGdZRFZRUUZFeWN5TVRNMQpOemcwTURRNU56TXhNRGN4TkRRMk5USXlOamcxTWpjMU1qZzNNekl4TkRZek5qRXdnZ0VpTUEwR0NTcUdTSWIzCkRRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRREVBQTFtbG1jeU5uK3BiYzVtRkxVRWNFN3JSWWtxNlEydnNKdnYKd0F3dDRpbjhGSmoxdVJVenU1YUJqYUs5bERDN1cyd2JwSjI4SGZuZmtkb2ttOHN4T0xqTTh5YUlaSzBYNG9ZcwpTTDFhdFVPMUsrUkt5citJNGc0dzJBd05QUURsUE5xU0t3dlNzRXhsQzhSSXpUTllZYXVpQVUxaWdoVW5zZTUxCm5jMVRTbzJ0NStuemNoVEt2SU1qV0U5TUpmZ2lBRDRjVXlHVU5DQXV1dTRndWJEZVBFdStYZmVhUjNCcXJoNUwKaTRiWGdQanF3NnBLMTZzY1h5QlYwL1RzdGsxYXhpekh3M2h4SG5INDNFeUtsUVNneXBPNnNLTFFmR0VEVTZ6UgpjdlFIOHNOc0g0YmVNL2Zzb0RoeXFZQVZKbDVNT011UG0zTHBpdHJ1YktEOXA4S1ZBZ01CQUFHalFqQkFNQTRHCkExVWREd0VCL3dRRUF3SUJwakFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQjBHQTFVZERnUVdCQlJqNytSM3pabEoKRlpPU05pM2xsOHhUclNQNVREQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFwZmJHWXA5Q1hoNksvL1FKY3kyUQpqVjF0RDk1akZYcVVyZ0JrZ1g0cmplaEVqQjd0N3k1SnNXZlJIK25DL2UxaGRqL3dlZWMxenR1MFJNazUxRThlCmJCNWFaLzV1eVhMWXlMVGJWaVpZQzEzV0EyTlZpY200d08yWDQvK29NNzVQTzFwUmRvTm4weGRlcDFPa2o2VDcKQlA3WVM3SUhYRlMwUE00bUQxRDRsTHRIQmhqcjdJQnNlK2ZSRXBaZEJZMktqOTZ3NjZzT0c1RlNNMVY0NWRiNQoyY1lhTHFEcGR3aFVEY0xDZWVIVnYzaTB4UGk2TUJmWTFoVGd1MjRTMFpHZnhpR2F5Qk5FWEpKL1dtNk5kQkFUClhVckV2YkU3Z2tORWRkeXJPUW9sWDA5R3lYU0dNbDVXMTVtMHNVaythNk5RanlVQTVxZU1sTTNWN1Q1REVLcWgKV0E9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBeEFBTlpwWm5NalovcVczT1poUzFCSEJPNjBXSkt1a05yN0NiNzhBTUxlSXAvQlNZCjlia1ZNN3VXZ1kyaXZaUXd1MXRzRzZTZHZCMzUzNUhhSkp2TE1UaTR6UE1taUdTdEYrS0dMRWk5V3JWRHRTdmsKU3NxL2lPSU9NTmdNRFQwQTVUemFraXNMMHJCTVpRdkVTTTB6V0dHcm9nRk5Zb0lWSjdIdWRaM05VMHFOcmVmcAo4M0lVeXJ5REkxaFBUQ1g0SWdBK0hGTWhsRFFnTHJydUlMbXczanhMdmwzM21rZHdhcTRlUzR1RzE0RDQ2c09xClN0ZXJIRjhnVmRQMDdMWk5Xc1lzeDhONGNSNXgrTnhNaXBVRW9NcVR1ckNpMEh4aEExT3MwWEwwQi9MRGJCK0cKM2pQMzdLQTRjcW1BRlNaZVREakxqNXR5NllyYTdteWcvYWZDbFFJREFRQUJBb0lCQUZtSU1KYnBJM0REaG1OdAozbmV4QTlOb1BoU282ZlNwQ3ZCemUzZjBRVndBVU85dXRVU2g3RFo2ZlZEbTB5MUljVTVVZjdqTTVLVFhDSnFBCjlLWCthTDR1Uy9TTEtkSHFNMHVTMVhtTExMd3Z5eU1LVHJsL2ppaklJblZiYTMzc25Pa2FlRG1HNGxxMjM5N1UKbGpBdlZFSU9NNm5JY0lJTUsvKzYvdFBKWnM2aGxXZXAzcGRuOWMwUXBUWGExbGpiZ1FVMHVNN2NwanRLRE4rcwpYRGRzb2N3VGpUV2MwNGY0aDBtS3NkdmlxeG80bVBSK0tyV3Brb1pFdFhiUGQ4THg5OVNkQjJ0aEs2enBiNEErCmZKOFlXYjhZTWp3SGpXRW5OYVpiM2FocGVuRkVyVWZFb1hlRGc4ajltak1VSGFSUURIK3ZGN0RINlI3U3lRb1QKOWo2dFBXRUNnWUVBemptNCtKamswbEd3SmVDMzAyN01Fd2lQMlNwcG0yRlNLY1MzVnM0cmE3TGhFR1pSZEpBcQpYSDNpdEVjZWViZTg4aU1RcDJXam1DMktEN1ZLOTBTN2pVUUpLbEw2eTM5WDh0RGlJRHQ4Y3l2OVd6TTJEK0p2Cm0vc3BFc1A4aURJTEVYZDJYUk1JNHhja2tXRml0TkE0SDNnODNtYWRPVWkzOEsyN0xyR3Q0b2tDZ1lFQTgwNkgKbWJOY1hKSTRML09FdTdHd2hVSk1uOGdLaWpvWStMdmZvQlN5Ym9SdzFIWkNvYnRZM2NLd0xNSnAvNkdQVG1lbwpucWlXNWs0YjJLVlpuczl0elNSRWhZa1NNTWRyZllhTis3bGZKTEozS3BhenNXMGlWbk5HaFlidzZVSnY0TmpnCjMzYnQvemh4bkp5ckhzS3B0VndiWm9lWmN2RDFkcVB5cm5kOVRLMENnWUVBdDkvYnZ6eUQrY3NBSmlYQmdmR3UKWCtJb2NGZFNwa29WK2t2OXRKWkxQTkhYdnNtY0l6UlBzUHhGWUx4d3ZkSkgxQlhUeVkza1dkRnc0aVNoWE91WgoxcEV0SXVHdDREZ0E4TzJ5VVU3NDNiQUJUSW5TMEVMemhMNWlsdXJNaFpzcEp6Kys5Nm43S0kvLytPZytIRDN6CmJJdkdxZjRRZlgwTEZMdXl4Q1dFaHhFQ2dZQVk2eG9JSzg1eHpLZmtnVlEreE53SFNkci9Ja1d5RW5Fc1NGR0cKMjVmS3FkWEViTGcyU0RHNXhJNjJodExFVTQrUndCd000OGRRbnY5TEdPUXMxNkd2T04rcnJYWW5lTVVSZmc1YwprWWVsQW9JaDRuMVUxcENGdWhpbTVFTVlJSzNFb1hHbWNVKytxOUUyOFBTMW1jbzN3TTh0bVFXbU4vZHJ4eTY3Cm41RTlvUUtCZ0R4VmptWGpCd1hkY3RhQm8ra3JHT1JWV2hJUzVWUFQrVkgwQ1F2MTUxcCtvT2EyMldQNSttbSsKUjJrcEhWNXRVTmUyUE9VZU5oMVE5U3JKQ1NLWmQ2K0JsT0U4bjQvc0kyNDJvbmRnelhrL21LRnc4NjBMQmdUbApkWTRvTC9mUU1mQ242aU4vMXBGZmFhWDZSV1lVMUplSTduK3VFZ0hDTFE4Z2x3eU1JcFdmCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
additional_trusted_keys: {}
cluster_name: me.localhost
type: saml_idp
sub_kind: saml_idp
version: v2`
healthCheckConfigYAML = `kind: health_check_config
metadata:
name: valid
revision: c1159783-930e-4a79-bb19-0d0d866d7af6
spec:
enabled: true
match:
db_labels:
- name: "*"
values:
- "*"
version: v1`
)
func TestInit_bootstrap(t *testing.T) {
t.Parallel()
hostCA := resourceFromYAML(t, hostCAYAML).(types.CertAuthority)
userCA := resourceFromYAML(t, userCAYAML).(types.CertAuthority)
jwtCA := resourceFromYAML(t, jwtCAYAML).(types.CertAuthority)
dbServerCA := resourceFromYAML(t, databaseCAYAML).(types.CertAuthority)
dbClientCA := resourceFromYAML(t, databaseClientCAYAML).(types.CertAuthority)
osshCA := resourceFromYAML(t, openSSHCAYAML).(types.CertAuthority)
samlCA := resourceFromYAML(t, samlCAYAML).(types.CertAuthority)
invalidHostCA := resourceFromYAML(t, hostCAYAML).(types.CertAuthority)
invalidHostCA.(*types.CertAuthorityV2).Spec.ActiveKeys.SSH = nil
invalidUserCA := resourceFromYAML(t, userCAYAML).(types.CertAuthority)
invalidUserCA.(*types.CertAuthorityV2).Spec.ActiveKeys.SSH = nil
invalidJWTCA := resourceFromYAML(t, jwtCAYAML).(types.CertAuthority)
invalidJWTCA.(*types.CertAuthorityV2).Spec.ActiveKeys.JWT = nil
invalidDBServerCA := resourceFromYAML(t, databaseCAYAML).(types.CertAuthority)
invalidDBServerCA.(*types.CertAuthorityV2).Spec.ActiveKeys.TLS = nil
invalidDBClientCA := resourceFromYAML(t, databaseClientCAYAML).(types.CertAuthority)
invalidDBClientCA.(*types.CertAuthorityV2).Spec.ActiveKeys.TLS = nil
invalidOSSHCA := resourceFromYAML(t, openSSHCAYAML).(types.CertAuthority)
invalidOSSHCA.(*types.CertAuthorityV2).Spec.ActiveKeys.SSH = nil
invalidSAMLCA := resourceFromYAML(t, samlCAYAML).(types.CertAuthority)
invalidSAMLCA.(*types.CertAuthorityV2).Spec.ActiveKeys.TLS = nil
tests := []struct {
name string
modifyConfig func(*auth.InitConfig)
assertError require.ErrorAssertionFunc
}{
{
// Issue https://github.com/gravitational/teleport/issues/7853.
name: "OK bootstrap CAs",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.BootstrapResources = append(
cfg.BootstrapResources,
hostCA.Clone(),
userCA.Clone(),
jwtCA.Clone(),
dbServerCA.Clone(),
dbClientCA.Clone(),
osshCA.Clone(),
samlCA.Clone(),
)
},
assertError: require.NoError,
},
{
name: "OK bootstrap health check config",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.BootstrapResources = append(
cfg.BootstrapResources,
newHealthCheckConfig(t),
)
},
assertError: require.NoError,
},
{
name: "NOK bootstrap health check config invalid",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.BootstrapResources = append(
cfg.BootstrapResources,
newHealthCheckConfig(t, func(hcc *healthcheckconfigv1.HealthCheckConfig) {
hcc.GetSpec().SetHealthyThreshold(9000)
}),
)
},
assertError: require.Error,
},
{
name: "NOK bootstrap Host CA missing keys",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.BootstrapResources = append(
cfg.BootstrapResources,
invalidHostCA.Clone(),
userCA.Clone(),
jwtCA.Clone(),
dbServerCA.Clone(),
dbClientCA.Clone(),
osshCA.Clone(),
samlCA.Clone(),
)
},
assertError: require.Error,
},
{
name: "NOK bootstrap User CA missing keys",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.BootstrapResources = append(
cfg.BootstrapResources,
hostCA.Clone(),
invalidUserCA.Clone(),
jwtCA.Clone(),
dbServerCA.Clone(),
dbClientCA.Clone(),
osshCA.Clone(),
samlCA.Clone(),
)
},
assertError: require.Error,
},
{
name: "NOK bootstrap JWT CA missing keys",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.BootstrapResources = append(
cfg.BootstrapResources,
hostCA.Clone(),
userCA.Clone(),
invalidJWTCA.Clone(),
dbServerCA.Clone(),
dbClientCA.Clone(),
osshCA.Clone(),
samlCA.Clone(),
)
},
assertError: require.Error,
},
{
name: "NOK bootstrap Database CA missing keys",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.BootstrapResources = append(
cfg.BootstrapResources,
hostCA.Clone(),
userCA.Clone(),
jwtCA.Clone(),
invalidDBServerCA.Clone(),
osshCA.Clone(),
samlCA.Clone(),
)
},
assertError: require.Error,
},
{
name: "NOK bootstrap Database Client CA missing keys",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.BootstrapResources = append(
cfg.BootstrapResources,
hostCA.Clone(),
userCA.Clone(),
jwtCA.Clone(),
dbServerCA.Clone(),
invalidDBClientCA.Clone(),
osshCA.Clone(),
samlCA.Clone(),
)
},
assertError: require.Error,
},
{
name: "NOK bootstrap OpenSSH CA missing keys",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.BootstrapResources = append(
cfg.BootstrapResources,
hostCA.Clone(),
userCA.Clone(),
jwtCA.Clone(),
dbServerCA.Clone(),
dbClientCA.Clone(),
invalidOSSHCA.Clone(),
samlCA.Clone(),
)
},
assertError: require.Error,
},
{
name: "NOK bootstrap SAML IdP CA missing keys",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.BootstrapResources = append(
cfg.BootstrapResources,
hostCA.Clone(),
userCA.Clone(),
jwtCA.Clone(),
dbServerCA.Clone(),
dbClientCA.Clone(),
osshCA.Clone(),
invalidSAMLCA.Clone(),
)
},
assertError: require.Error,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfg := setupConfig(t)
test.modifyConfig(&cfg)
_, err := auth.Init(context.Background(), cfg)
test.assertError(t, err)
})
}
}
const (
userYAML = `kind: user
version: v2
metadata:
name: myuser
spec:
roles: ["admin"]`
tokenYAML = `kind: token
version: v2
metadata:
name: github-token
expires: "3000-01-01T00:00:00Z"
spec:
roles: [Bot]
join_method: github
bot_name: github-demo
github:
allow:
- repository: gravitational/example`
badTokenYAML = `kind: token
version: v2
metadata:
name: iam-token-without-orgid-but-ous
expires: "3000-01-01T00:00:00Z"
spec:
roles: [Node]
join_method: iam
allow:
- aws_account: "123456789012"
aws_organizational_units:
include: [r-rootid]`
boundKeypairTokenYAML = `kind: token
version: v2
metadata:
name: bound-keypair-token
expires: "3000-01-01T00:00:00Z"
spec:
roles: [Bot]
join_method: bound_keypair
bot_name: bound-keypair-demo
bound_keypair:
onboarding:
initial_public_key: "ssh-ed25519 AAAAtestinitialpublickey"
recovery:
limit: 1
mode: insecure`
// boundKeypairTokenNoRecoveryYAML omits spec.bound_keypair.recovery
// entirely; CheckAndSetDefaults must default it (limit 1) at unmarshal time,
// before apply-on-startup validation runs.
boundKeypairTokenNoRecoveryYAML = `kind: token
version: v2
metadata:
name: bound-keypair-token
expires: "3000-01-01T00:00:00Z"
spec:
roles: [Bot]
join_method: bound_keypair
bot_name: bound-keypair-demo
bound_keypair:
onboarding:
initial_public_key: "ssh-ed25519 AAAAtestinitialpublickey"`
roleYAML = `kind: role
version: v7
metadata:
name: admin
expires: "3000-01-01T00:00:00Z"
spec:
allow:
logins: ['admin']
kubernetes_groups: ['edit']
node_labels:
'*': '*'
kubernetes_labels:
'*': '*'
kubernetes_resources:
- kind: '*'
namespace: '*'
name: '*'
verbs: ['*']`
lockYAML = `
kind: lock
version: v2
metadata:
name: b1c785d8-8165-41fc-8dd6-252d534334d3
spec:
created_at: "2023-11-07T18:44:35.361806Z"
created_by: Admin
target:
user: myuser
`
clusterNetworkingConfYAML = `
kind: cluster_networking_config
metadata:
name: cluster-networking-config
spec:
proxy_listener_mode: 1
`
authPrefYAML = `
kind: cluster_auth_preference
metadata:
name: cluster-auth-preference
spec:
second_factor: off
type: local
version: v2
`
workloadIdentityYAML = `kind: workload_identity
version: v1
metadata:
name: example-workload-identity
spec:
spiffe:
id: /svc/example
`
botYAML = `kind: bot
metadata:
name: my-bot
spec:
roles: ["admin"]
`
)
func TestInit_ApplyOnStartup(t *testing.T) {
t.Parallel()
user := resourceFromYAML(t, userYAML).(types.User)
token := resourceFromYAML(t, tokenYAML).(types.ProvisionToken)
badToken := resourceFromYAML(t, badTokenYAML).(types.ProvisionToken)
role := resourceFromYAML(t, roleYAML).(types.Role)
lock := resourceFromYAML(t, lockYAML).(types.Lock)
clusterNetworkingConfig := resourceFromYAML(t, clusterNetworkingConfYAML).(types.ClusterNetworkingConfig)
authPref := resourceFromYAML(t, authPrefYAML).(types.AuthPreference)
workloadIdentity := resourceFromYAML(t, workloadIdentityYAML)
bot := resourceFromYAML(t, botYAML)
tests := []struct {
name string
modifyConfig func(*auth.InitConfig)
assertError require.ErrorAssertionFunc
}{
{
name: "Apply unsupported resource",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, lock)
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, user)
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, token)
},
assertError: require.Error,
},
{
name: "Apply invalid provision token",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, badToken)
},
assertError: require.Error,
},
{
name: "Apply ProvisionToken",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, token)
},
assertError: require.NoError,
},
{
name: "Apply User (invalid, missing role)",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, user)
},
assertError: require.Error,
},
// We test both user+role and role+user to validate that ordering doesn't matter
{
name: "Apply User+Role",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, user)
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
},
assertError: require.NoError,
},
{
name: "Apply Role+User",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, user)
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
},
assertError: require.NoError,
},
{
name: "Apply Role",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
},
assertError: require.NoError,
},
{
name: "Apply ClusterNetworkingConfig",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, clusterNetworkingConfig)
},
assertError: require.NoError,
},
{
name: "Apply AuthPreference",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, authPref)
},
assertError: require.NoError,
},
{
name: "Apply WorkloadIdentity",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, workloadIdentity)
},
assertError: require.NoError,
},
{
name: "Apply Role+Bot",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, bot)
},
assertError: require.NoError,
},
{
name: "Apply Bot+Role",
modifyConfig: func(cfg *auth.InitConfig) {
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, bot)
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, role)
},
assertError: require.NoError,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cfg := setupConfig(t)
test.modifyConfig(&cfg)
_, err := auth.Init(context.Background(), cfg)
test.assertError(t, err)
})
}
}
// TestInit_ApplyOnStartup_BoundKeypair verifies that bound_keypair tokens
// applied via --apply-on-startup have their status.bound_keypair initialized
// (the join path rejects tokens without it), and that re-applying on a
// subsequent auth restart preserves an already-populated status rather than
// resetting the bot's join state.
func TestInit_ApplyOnStartup_BoundKeypair(t *testing.T) {
t.Parallel()
ctx := context.Background()
t.Run("initializes status on first apply", func(t *testing.T) {
token := resourceFromYAML(t, boundKeypairTokenYAML).(types.ProvisionToken)
cfg := setupConfig(t)
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, token)
srv, err := auth.Init(ctx, cfg)
require.NoError(t, err)
stored, err := srv.GetToken(ctx, token.GetName())
require.NoError(t, err)
require.NotNil(t, stored.GetBoundKeypairStatus(),
"apply-on-startup must initialize status.bound_keypair, otherwise bound_keypair joins are rejected")
})
t.Run("applies a token with a defaulted recovery policy", func(t *testing.T) {
// A config that omits spec.bound_keypair.recovery must still apply:
// UnmarshalProvisionToken defaults recovery (limit 1) via
// CheckAndSetDefaults before apply-on-startup validation runs, so Init
// must not reject it.
token := resourceFromYAML(t, boundKeypairTokenNoRecoveryYAML).(*types.ProvisionTokenV2)
require.NotNil(t, token.Spec.BoundKeypair.Recovery,
"unmarshal must default recovery when the config omits it")
cfg := setupConfig(t)
cfg.ApplyOnStartupResources = []types.Resource{token}
srv, err := auth.Init(ctx, cfg)
require.NoError(t, err)
stored, err := srv.GetToken(ctx, token.GetName())
require.NoError(t, err)
require.Equal(t, uint32(1), stored.GetBoundKeypair().Recovery.Limit,
"apply-on-startup must persist the defaulted recovery limit")
})
t.Run("discards config-supplied status on first apply", func(t *testing.T) {
// apply-on-startup config is spec-only and its status is untrusted. A
// bound_public_key supplied via config must not bind a key on creation:
// that would skip the join ceremony's proof-of-possession.
token := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
token.Status = &types.ProvisionTokenStatusV2{
BoundKeypair: &types.ProvisionTokenStatusV2BoundKeypair{
BoundPublicKey: "ssh-ed25519 AAAAconfigsuppliedkey",
RecoveryCount: 7,
},
}
cfg := setupConfig(t)
cfg.ApplyOnStartupResources = []types.Resource{token}
srv, err := auth.Init(ctx, cfg)
require.NoError(t, err)
stored, err := srv.GetToken(ctx, token.GetName())
require.NoError(t, err)
status := stored.GetBoundKeypairStatus()
require.NotNil(t, status)
require.Empty(t, status.BoundPublicKey,
"config-supplied bound_public_key must not be persisted on first apply")
require.Zero(t, status.RecoveryCount,
"config-supplied recovery count must not be persisted on first apply")
})
t.Run("generates a registration secret when no onboarding credential is set", func(t *testing.T) {
// The Terraform onboarding flow supplies neither an initial_public_key
// nor an explicit registration_secret; apply-on-startup must generate
// the secret so the bot has a credential to join with.
token := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
token.Spec.BoundKeypair.Onboarding.InitialPublicKey = ""
token.Spec.BoundKeypair.Onboarding.RegistrationSecret = ""
cfg := setupConfig(t)
cfg.ApplyOnStartupResources = []types.Resource{token}
srv, err := auth.Init(ctx, cfg)
require.NoError(t, err)
stored, err := srv.GetToken(ctx, token.GetName())
require.NoError(t, err)
require.NotEmpty(t, stored.GetBoundKeypairStatus().RegistrationSecret,
"apply-on-startup must generate a registration secret when none is configured")
})
t.Run("preserves populated status on re-apply", func(t *testing.T) {
token := resourceFromYAML(t, boundKeypairTokenYAML).(types.ProvisionToken)
tokenV2 := token.(*types.ProvisionTokenV2)
cfg := setupConfig(t)
cfg.ApplyOnStartupResources = append(cfg.ApplyOnStartupResources, token)
srv, err := auth.Init(ctx, cfg)
require.NoError(t, err)
// Simulate a successful bound_keypair join populating the status.
stored, err := srv.GetToken(ctx, token.GetName())
require.NoError(t, err)
storedV2, ok := stored.(*types.ProvisionTokenV2)
require.True(t, ok)
require.NotNil(t, storedV2.GetBoundKeypairStatus())
storedV2.Status.BoundKeypair.BoundPublicKey = "ssh-ed25519 AAAAboundpublickey"
storedV2.Status.BoundKeypair.RecoveryCount = 3
require.NoError(t, srv.UpsertToken(ctx, storedV2))
// Recovery policy is declarative and may change without changing the
// identity to which the key is bound.
tokenV2.Spec.BoundKeypair.Recovery.Limit = 5
// Re-running Init against the same backend re-applies the (spec-only)
// startup resource, mimicking an auth restart.
_, err = auth.Init(ctx, cfg)
require.NoError(t, err)
reStored, err := srv.GetToken(ctx, token.GetName())
require.NoError(t, err)
status := reStored.GetBoundKeypairStatus()
require.NotNil(t, status)
require.Equal(t, "ssh-ed25519 AAAAboundpublickey", status.BoundPublicKey,
"re-apply must preserve the bound public key")
require.Equal(t, uint32(3), status.RecoveryCount,
"re-apply must preserve the recovery count")
require.Equal(t, uint32(5), reStored.GetBoundKeypair().Recovery.Limit,
"re-apply must update the recovery policy")
})
t.Run("does not modify the registration secret on re-apply", func(t *testing.T) {
// Re-apply never modifies status. The stored registration secret is
// authoritative; editing spec.onboarding.registration_secret has no
// effect once the token exists.
token := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
token.Spec.BoundKeypair.Onboarding.InitialPublicKey = ""
token.Spec.BoundKeypair.Onboarding.RegistrationSecret = "old-registration-secret"
cfg := setupConfig(t)
cfg.ApplyOnStartupResources = []types.Resource{token}
srv, err := auth.Init(ctx, cfg)
require.NoError(t, err)
stored, err := srv.GetToken(ctx, token.GetName())
require.NoError(t, err)
require.Equal(t, "old-registration-secret", stored.GetBoundKeypairStatus().RegistrationSecret)
updated := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
updated.Spec.BoundKeypair.Onboarding.InitialPublicKey = ""
updated.Spec.BoundKeypair.Onboarding.RegistrationSecret = "new-registration-secret"
cfg.ApplyOnStartupResources = []types.Resource{updated}
_, err = auth.Init(ctx, cfg)
require.NoError(t, err)
stored, err = srv.GetToken(ctx, token.GetName())
require.NoError(t, err)
require.Equal(t, "old-registration-secret", stored.GetBoundKeypairStatus().RegistrationSecret,
"re-apply must never modify status; the stored registration secret is authoritative")
})
t.Run("preserves status even when the spec identity changes", func(t *testing.T) {
// Re-apply never modifies status. Spec fields (including the bot) are
// applied freely; changing them does not unbind the key. To reset a
// binding, an admin must delete and recreate the token.
token := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
cfg := setupConfig(t)
cfg.ApplyOnStartupResources = []types.Resource{token}
srv, err := auth.Init(ctx, cfg)
require.NoError(t, err)
stored, err := srv.GetToken(ctx, token.GetName())
require.NoError(t, err)
storedV2 := stored.(*types.ProvisionTokenV2)
storedV2.Status.BoundKeypair.BoundPublicKey = "ssh-ed25519 AAAAboundpublickey"
require.NoError(t, srv.UpsertToken(ctx, storedV2))
updated := resourceFromYAML(t, boundKeypairTokenYAML).(*types.ProvisionTokenV2)
updated.Spec.BotName = "different-bot"
cfg.ApplyOnStartupResources = []types.Resource{updated}
_, err = auth.Init(ctx, cfg)
require.NoError(t, err)
stored, err = srv.GetToken(ctx, token.GetName())
require.NoError(t, err)
require.Equal(t, "different-bot", stored.(*types.ProvisionTokenV2).Spec.BotName,
"spec fields are applied freely")
require.Equal(t, "ssh-ed25519 AAAAboundpublickey", stored.GetBoundKeypairStatus().BoundPublicKey,
"re-apply must preserve status; changing spec never unbinds the key")
})
}
func resourceFromYAML(t *testing.T, value string) types.Resource {
t.Helper()
ur := &services.UnknownResource{}
err := kyaml.NewYAMLToJSONDecoder(strings.NewReader(value)).Decode(ur)
require.NoError(t, err)
resource, err := services.UnmarshalResource(ur.Kind, ur.Raw)
require.NoError(t, err)
return resource
}
func resourceDiff(res1, res2 types.Resource) string {
return cmp.Diff(res1, res2,
cmpopts.IgnoreFields(types.Metadata{}, "Revision", "Namespace"),
cmpopts.EquateEmpty())
}
func newHealthCheckConfig(t *testing.T, opts ...func(*healthcheckconfigv1.HealthCheckConfig)) types.Resource {
r := resourceFromYAML(t, healthCheckConfigYAML)
inner := r.(types.Resource153UnwrapperT[*healthcheckconfigv1.HealthCheckConfig]).UnwrapT()
for _, opt := range opts {
opt(inner)
}
return r
}
// TestSyncUpgadeWindowStartHour verifies the core logic of the upgrade window start
// hour behavior.
func TestSyncUpgradeWindowStartHour(t *testing.T) {
t.Parallel()
ctx := context.Background()
conf := setupConfig(t)
authServer, err := auth.Init(ctx, conf)
require.NoError(t, err)
t.Cleanup(func() { authServer.Close() })
// no getter is registered, sync should fail
require.Error(t, authServer.SyncUpgradeWindowStartHour(ctx))
// maintenance config does not exist yet
cmc, err := authServer.GetClusterMaintenanceConfig(ctx)
require.Error(t, err)
require.True(t, trace.IsNotFound(err))
require.Nil(t, cmc)
// set up fake getter
var mu sync.Mutex
var fakeHour int64
var fakeError error
authServer.SetUpgradeWindowStartHourGetter(func(ctx context.Context) (int64, error) {
mu.Lock()
defer mu.Unlock()
return fakeHour, fakeError
})
// sync should now succeed
require.NoError(t, authServer.SyncUpgradeWindowStartHour(ctx))
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
require.NoError(t, err)
agentWindow, ok := cmc.GetAgentUpgradeWindow()
require.True(t, ok)
require.Equal(t, uint32(0), agentWindow.UTCStartHour)
require.Equal(t, []string{"Mon", "Tue", "Wed", "Thu"}, agentWindow.Weekdays)
// change the served hour
mu.Lock()
fakeHour = 16
mu.Unlock()
require.NoError(t, authServer.SyncUpgradeWindowStartHour(ctx))
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
require.NoError(t, err)
agentWindow, ok = cmc.GetAgentUpgradeWindow()
require.True(t, ok)
require.Equal(t, uint32(16), agentWindow.UTCStartHour)
// set sync to fail with out of range hour
mu.Lock()
fakeHour = 36
mu.Unlock()
require.Error(t, authServer.SyncUpgradeWindowStartHour(ctx))
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
require.NoError(t, err)
agentWindow, ok = cmc.GetAgentUpgradeWindow()
require.True(t, ok)
// verify that the old hour value persists since the sync failed
require.Equal(t, uint32(16), agentWindow.UTCStartHour)
// set sync to fail with impossible int type-cast
mu.Lock()
fakeHour = math.MaxInt64
mu.Unlock()
require.Error(t, authServer.SyncUpgradeWindowStartHour(ctx))
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
require.NoError(t, err)
agentWindow, ok = cmc.GetAgentUpgradeWindow()
require.True(t, ok)
// verify that the old hour value persists since the sync failed
require.Equal(t, uint32(16), agentWindow.UTCStartHour)
mu.Lock()
fakeHour = 18
mu.Unlock()
// sync should now succeed again
require.NoError(t, authServer.SyncUpgradeWindowStartHour(ctx))
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
require.NoError(t, err)
agentWindow, ok = cmc.GetAgentUpgradeWindow()
require.True(t, ok)
// verify that we got the new hour value
require.Equal(t, uint32(18), agentWindow.UTCStartHour)
// set sync to fail with error
mu.Lock()
fakeHour = 12
fakeError = fmt.Errorf("uh-oh")
mu.Unlock()
require.Error(t, authServer.SyncUpgradeWindowStartHour(ctx))
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
require.NoError(t, err)
agentWindow, ok = cmc.GetAgentUpgradeWindow()
require.True(t, ok)
// verify that the old hour value persists since the sync failed
require.Equal(t, uint32(18), agentWindow.UTCStartHour)
// recover and set hour to zero
mu.Lock()
fakeHour = 0
fakeError = nil
mu.Unlock()
// sync should now succeed again
require.NoError(t, authServer.SyncUpgradeWindowStartHour(ctx))
cmc, err = authServer.GetClusterMaintenanceConfig(ctx)
require.NoError(t, err)
agentWindow, ok = cmc.GetAgentUpgradeWindow()
require.True(t, ok)
// verify that we got the new hour value
require.Equal(t, uint32(0), agentWindow.UTCStartHour)
}
// TestIdentityChecker verifies auth identity properly validates host
// certificates when connecting to an SSH server.
func TestIdentityChecker(t *testing.T) {
t.Parallel()
ctx := context.Background()
conf := setupConfig(t)
authServer, err := auth.Init(ctx, conf)
require.NoError(t, err)
t.Cleanup(func() { authServer.Close() })
lockWatcher, err := services.NewLockWatcher(ctx, services.LockWatcherConfig{
ResourceWatcherConfig: services.ResourceWatcherConfig{
Component: teleport.ComponentAuth,
Client: authServer,
},
})
require.NoError(t, err)
authServer.SetLockWatcher(lockWatcher)
clusterName, err := authServer.GetDomainName()
require.NoError(t, err)
ca, err := authServer.GetCertAuthority(ctx, types.CertAuthID{
Type: types.HostCA,
DomainName: clusterName,
}, true)
require.NoError(t, err)
signers, err := sshutils.GetSigners(ca)
require.NoError(t, err)
require.Len(t, signers, 1)
realCert, err := apisshutils.MakeRealHostCert(signers[0])
require.NoError(t, err)
spoofedCert, err := apisshutils.MakeSpoofedHostCert(signers[0])
require.NoError(t, err)
tests := []struct {
desc string
cert ssh.Signer
err bool
}{
{
desc: "should be able to connect with real cert",
cert: realCert,
err: false,
},
{
desc: "should not be able to connect with spoofed cert",
cert: spoofedCert,
err: true,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
handler := sshutils.NewChanHandlerFunc(func(_ context.Context, ccx *sshutils.ConnectionContext, nch ssh.NewChannel) {
ch, _, err := nch.Accept()
require.NoError(t, err)
require.NoError(t, ch.Close())
})
sshServer, err := sshutils.NewServer(
"test",
utils.NetAddr{AddrNetwork: "tcp", Addr: "localhost:0"},
handler,
sshutils.StaticHostSigners(test.cert),
sshutils.AuthMethods{NoClient: true},
sshutils.SetInsecureSkipHostValidation(),
)
require.NoError(t, err)
t.Cleanup(func() { sshServer.Close() })
require.NoError(t, sshServer.Start())
identity, err := auth.GenerateIdentity(authServer, state.IdentityID{
Role: types.RoleNode,
HostUUID: uuid.New().String(),
NodeName: "node-1",
}, nil, nil)
require.NoError(t, err)
sshClientConfig, err := identity.SSHClientConfig(false)
require.NoError(t, err)
dialer := proxy.DialerFromEnvironment(sshServer.Addr())
sconn, err := dialer.Dial(ctx, "tcp", sshServer.Addr(), sshClientConfig)
if test.err {
require.Error(t, err)
} else {
require.NoError(t, err)
require.NoError(t, sconn.Close())
}
})
}
}
func TestInitCreatesCertsIfMissing(t *testing.T) {
t.Parallel()
ctx := context.Background()
conf := setupConfig(t)
auth, err := auth.Init(ctx, conf)
require.NoError(t, err)
t.Cleanup(func() {
err = auth.Close()
require.NoError(t, err)
})
for _, caType := range types.CertAuthTypes {
cert, err := auth.GetCertAuthorities(ctx, caType, false)
require.NoError(t, err)
require.Len(t, cert, 1)
}
}
func TestTeleportProcessAuthVersionUpgradeCheck(t *testing.T) {
tests := []struct {
name string
initialVersion string
initialProcVersion string
expectedVersion string
expectError bool
skipCheck bool
}{
{
name: "first-launch",
initialVersion: "",
expectedVersion: teleport.Version,
expectError: false,
},
{
name: "old-version-upgrade",
initialVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-1),
expectedVersion: teleport.Version,
expectError: false,
},
{
name: "old-version-upgrade-proc",
initialProcVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-1),
expectedVersion: teleport.Version,
expectError: false,
},
{
name: "major-upgrade-fail",
initialVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-2),
expectedVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-2),
expectError: true,
},
{
name: "major-upgrade-fail-proc",
initialProcVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-2),
expectError: true,
},
{
name: "major-upgrade-fail-proc-with-skip-check",
initialProcVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-2),
expectedVersion: teleport.Version,
skipCheck: true,
},
{
name: "major-upgrade-with-dev-skip-check",
initialVersion: fmt.Sprintf("%d.0.0", api.VersionMajor-2),
expectedVersion: teleport.Version,
expectError: false,
skipCheck: true,
},
{
name: "major-downgrade-fail",
initialVersion: fmt.Sprintf("%d.0.0", api.VersionMajor+2),
expectedVersion: fmt.Sprintf("%d.0.0", api.VersionMajor+2),
expectError: true,
},
{
name: "major-downgrade-fail-proc",
initialProcVersion: fmt.Sprintf("%d.0.0", api.VersionMajor+2),
expectError: true,
},
{
name: "major-downgrade-fail-proc-with-skip-check",
initialProcVersion: fmt.Sprintf("%d.0.0", api.VersionMajor+2),
expectedVersion: teleport.Version,
skipCheck: true,
},
{
name: "major-downgrade-with-dev-skip-check",
initialVersion: fmt.Sprintf("%d.0.0", api.VersionMajor+2),
expectedVersion: teleport.Version,
expectError: false,
skipCheck: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx := t.Context()
authCfg := setupConfig(t)
authCfg.InsecureMode = true
service, err := local.NewBackendInfoService(authCfg.Backend)
require.NoError(t, err)
if test.initialProcVersion != "" {
ver, err := semver.NewVersion(test.initialProcVersion)
require.NoError(t, err)
err = authCfg.VersionStorage.WriteTeleportVersion(ctx, *ver)
require.NoError(t, err)
}
if test.initialVersion != "" {
ver, err := semver.NewVersion(test.initialVersion)
require.NoError(t, err)
backendInfo, err := backendinfo.NewBackendInfo(backendinfov1.BackendInfoSpec_builder{
TeleportVersion: ver.String(),
}.Build())
require.NoError(t, err)
_, err = service.CreateBackendInfo(ctx, backendInfo)
require.NoError(t, err)
}
if test.skipCheck {
authCfg.SkipVersionCheck = true
}
_, err = auth.Init(ctx, authCfg)
if test.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
// Verifies that version is removed from process storage.
if test.initialProcVersion != "" && !test.expectError {
_, err := authCfg.VersionStorage.GetTeleportVersion(ctx)
require.True(t, trace.IsNotFound(err))
}
if test.expectedVersion != "" {
backendInfo, err := service.GetBackendInfo(ctx)
require.NoError(t, err)
require.Equal(t, test.expectedVersion, backendInfo.GetSpec().GetTeleportVersion())
}
})
}
}
type mockDatabaseObjectImportRules struct {
services.DatabaseObjectImportRules
listRules []*dbobjectimportrulev1.DatabaseObjectImportRule
created *dbobjectimportrulev1.DatabaseObjectImportRule
upserted *dbobjectimportrulev1.DatabaseObjectImportRule
}
func (m *mockDatabaseObjectImportRules) ListDatabaseObjectImportRules(context.Context, int, string) ([]*dbobjectimportrulev1.DatabaseObjectImportRule, string, error) {
return m.listRules, "", nil
}
func (m *mockDatabaseObjectImportRules) CreateDatabaseObjectImportRule(ctx context.Context, rule *dbobjectimportrulev1.DatabaseObjectImportRule) (*dbobjectimportrulev1.DatabaseObjectImportRule, error) {
m.created = rule
return rule, nil
}
func (m *mockDatabaseObjectImportRules) UpsertDatabaseObjectImportRule(ctx context.Context, rule *dbobjectimportrulev1.DatabaseObjectImportRule) (*dbobjectimportrulev1.DatabaseObjectImportRule, error) {
m.upserted = rule
return rule, nil
}
func Test_createPresetDatabaseObjectImportRule(t *testing.T) {
presetRule := databaseobjectimportrule.NewPresetImportAllObjectsRule()
require.NotNil(t, presetRule)
customRule, err := databaseobjectimportrule.NewDatabaseObjectImportRule("dev_rule", dbobjectimportrulev1.DatabaseObjectImportRuleSpec_builder{
Priority: 100,
DatabaseLabels: label.FromMap(map[string][]string{"env": {"dev"}}),
Mappings: []*dbobjectimportrulev1.DatabaseObjectImportRuleMapping{dbobjectimportrulev1.DatabaseObjectImportRuleMapping_builder{
Match: dbobjectimportrulev1.DatabaseObjectImportMatch_builder{
TableNames: []string{"*"},
}.Build(),
AddLabels: map[string]string{
"env": "dev",
},
Scope: dbobjectimportrulev1.DatabaseObjectImportScope_builder{
SchemaNames: []string{"public"},
}.Build(),
}.Build()},
}.Build())
require.NoError(t, err)
tests := []struct {
name string
existingRules []*dbobjectimportrulev1.DatabaseObjectImportRule
expectCreate *dbobjectimportrulev1.DatabaseObjectImportRule
expectUpsert *dbobjectimportrulev1.DatabaseObjectImportRule
}{
{
name: "create preset in new cluster",
expectCreate: presetRule,
},
{
name: "no action with custom rule",
existingRules: []*dbobjectimportrulev1.DatabaseObjectImportRule{customRule},
},
{
name: "no action with preset rule",
existingRules: []*dbobjectimportrulev1.DatabaseObjectImportRule{presetRule},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
m := &mockDatabaseObjectImportRules{
listRules: test.existingRules,
}
err := auth.CreatePresetDatabaseObjectImportRule(context.Background(), m)
require.NoError(t, err)
require.True(t, proto.Equal(test.expectCreate, m.created))
require.True(t, proto.Equal(test.expectUpsert, m.upserted))
})
}
}
// TestInitWithAutoUpdateResources verifies that auth init support bootstrapping and apply
// `AutoUpdateConfig` and `AutoUpdateVersion` resources as well as unmarshalling them from
// yaml configuration.
func TestInitWithAutoUpdateResources(t *testing.T) {
t.Parallel()
const autoUpdateConfigYAML = `kind: autoupdate_config
metadata:
name: autoupdate-config
spec:
tools:
mode: enabled
version: v1`
const autoUpdateVersionYAML = `kind: autoupdate_version
metadata:
name: autoupdate-version
spec:
tools:
target_version: 1.2.3
version: v1`
ctx := context.Background()
resources := []types.Resource{
resourceFromYAML(t, autoUpdateConfigYAML),
resourceFromYAML(t, autoUpdateVersionYAML),
}
for _, test := range []struct {
name string
fn func(cfg *auth.InitConfig)
}{
{name: "bootstrap", fn: func(cfg *auth.InitConfig) { cfg.BootstrapResources = resources }},
{name: "apply", fn: func(cfg *auth.InitConfig) { cfg.ApplyOnStartupResources = resources }},
} {
t.Run(test.name, func(t *testing.T) {
cfg := setupConfig(t)
test.fn(&cfg)
auth, err := auth.Init(ctx, cfg)
require.NoError(t, err)
config, err := auth.GetAutoUpdateConfig(ctx)
assert.NoError(t, err)
assert.Equal(t, "enabled", config.GetSpec().GetTools().GetMode())
version, err := auth.GetAutoUpdateVersion(ctx)
assert.NoError(t, err)
assert.Equal(t, "1.2.3", version.GetSpec().GetTools().GetTargetVersion())
})
}
}
func TestInitWithWorkloadIdentityResources(t *testing.T) {
t.Parallel()
ctx := t.Context()
resources := []types.Resource{
resourceFromYAML(t, workloadIdentityYAML),
}
for _, test := range []struct {
name string
fn func(cfg *auth.InitConfig)
}{
{name: "bootstrap", fn: func(cfg *auth.InitConfig) { cfg.BootstrapResources = resources }},
{name: "apply", fn: func(cfg *auth.InitConfig) { cfg.ApplyOnStartupResources = resources }},
} {
t.Run(test.name, func(t *testing.T) {
cfg := setupConfig(t)
test.fn(&cfg)
auth, err := auth.Init(ctx, cfg)
require.NoError(t, err)
workloadIdentity, err := auth.GetWorkloadIdentity(ctx, workloadidentityv1pb.GetWorkloadIdentityRequest_builder{Name: "example-workload-identity"}.Build())
require.NoError(t, err)
require.Equal(t, "/svc/example", workloadIdentity.GetSpec().GetSpiffe().GetId())
})
}
}