From 650f7399c85a61dc535003cfcfe73517039900f6 Mon Sep 17 00:00:00 2001 From: Hugo Shaka Date: Thu, 25 Jul 2024 17:36:23 -0400 Subject: [PATCH] Refactor Terraform credential loading (#44037) * Refactor Terraform credential loading * Warn about expiry * kip expired credentials * fixup! kip expired credentials * Use constants everywhere + add godocs * fixup! Use constants everywhere + add godocs * Address marco's feedback * fixup! Address marco's feedback * tidy go mod * lint * re-render TF docs --- api/client/credentials.go | 59 +++ api/client/credentials_test.go | 26 + docs/pages/reference/terraform-provider.mdx | 2 +- integrations/terraform/go.mod | 3 +- .../terraform/provider/credentials.go | 487 ++++++++++++++++++ .../terraform/provider/credentials_test.go | 118 +++++ integrations/terraform/provider/provider.go | 351 ++++--------- 7 files changed, 807 insertions(+), 239 deletions(-) create mode 100644 integrations/terraform/provider/credentials.go create mode 100644 integrations/terraform/provider/credentials_test.go diff --git a/api/client/credentials.go b/api/client/credentials.go index f9f3b67cc45d1..b38a1f999c066 100644 --- a/api/client/credentials.go +++ b/api/client/credentials.go @@ -662,3 +662,62 @@ func (d *DynamicIdentityFileCreds) Expiry() (time.Time, bool) { return x509Cert.NotAfter, true } + +// KeyPair returns a Credential give a TLS key, certificate and CA certificates PEM-encoded. +// It behaves live LoadKeyPair except it doesn't read the TLS material from a file. +// This is useful when key and certs are not on the disk (e.g. environment variables). +// This should be preferred over manually building a tls.Config and calling LoadTLS +// as Credentials returned by KeyPair can report their expiry, which allows to warn +// the user in case of expired certificates. +func KeyPair(certPEM, keyPEM, caPEM []byte) (Credentials, error) { + if len(certPEM) == 0 { + return nil, trace.BadParameter("missing certificate PEM data") + } + if len(keyPEM) == 0 { + return nil, trace.BadParameter("missing private key PEM data") + } + return &staticKeypairCreds{ + certPEM: certPEM, + keyPEM: keyPEM, + caPEM: caPEM, + }, nil +} + +// staticKeypairCreds uses keypair certificates to provide client credentials. +type staticKeypairCreds struct { + certPEM []byte + keyPEM []byte + caPEM []byte +} + +// TLSConfig returns TLS configuration. +func (c *staticKeypairCreds) TLSConfig() (*tls.Config, error) { + cert, err := keys.X509KeyPair(c.certPEM, c.keyPEM) + if err != nil { + return nil, trace.Wrap(err) + } + + pool := x509.NewCertPool() + if ok := pool.AppendCertsFromPEM(c.caPEM); !ok { + return nil, trace.BadParameter("invalid TLS CA cert PEM") + } + + return configureTLS(&tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: pool, + }), nil +} + +// SSHClientConfig returns SSH configuration. +func (c *staticKeypairCreds) SSHClientConfig() (*ssh.ClientConfig, error) { + return nil, trace.NotImplemented("no ssh config") +} + +// Expiry returns the credential expiry. +func (c *staticKeypairCreds) Expiry() (time.Time, bool) { + cert, _, err := keys.X509Certificate(c.certPEM) + if err != nil { + return time.Time{}, false + } + return cert.NotAfter, true +} diff --git a/api/client/credentials_test.go b/api/client/credentials_test.go index 896a873c19811..63a28889e0f0b 100644 --- a/api/client/credentials_test.go +++ b/api/client/credentials_test.go @@ -218,6 +218,32 @@ func TestLoadKeyPair(t *testing.T) { require.False(t, ok, "expiry should be unknown on a broken credential") } +func TestKeyPair(t *testing.T) { + t.Parallel() + + // Load expected tls.Config. + expectedTLSConfig := getExpectedTLSConfig(t) + + // Load key pair from disk. + creds, err := KeyPair(tlsCert, keyPEM, tlsCACert) + require.NoError(t, err) + + // Build tls.Config and compare to expected tls.Config. + tlsConfig, err := creds.TLSConfig() + require.NoError(t, err) + requireEqualTLSConfig(t, expectedTLSConfig, tlsConfig) + + // Load invalid keypairs. + invalidIdentityCreds, err := KeyPair([]byte("invalid_cert"), []byte("invalid_key"), []byte("invalid_ca_cert")) + require.NoError(t, err) + _, err = invalidIdentityCreds.TLSConfig() + require.Error(t, err) + + // Load missing keypairs + _, err = KeyPair(nil, nil, nil) + require.Error(t, err) +} + func TestLoadProfile(t *testing.T) { t.Parallel() profileName := "proxy.example.com" diff --git a/docs/pages/reference/terraform-provider.mdx b/docs/pages/reference/terraform-provider.mdx index d965ea0e4d1a5..1ed0af6b46f8b 100644 --- a/docs/pages/reference/terraform-provider.mdx +++ b/docs/pages/reference/terraform-provider.mdx @@ -130,7 +130,7 @@ This auth method has the following limitations: ### Optional -- `addr` (String) host:port where Teleport Auth Service is running. This can also be set with the environment variable `TF_TELEPORT_ADDR`. +- `addr` (String) host:port of the Teleport address. This can be the Teleport Proxy Service address (port 443 or 4080) or the Teleport Auth Service address (port 3025). This can also be set with the environment variable `TF_TELEPORT_ADDR`. - `cert_base64` (String) Base64 encoded TLS auth certificate. This can also be set with the environment variable `TF_TELEPORT_CERT_BASE64`. - `cert_path` (String) Path to Teleport auth certificate file. This can also be set with the environment variable `TF_TELEPORT_CERT`. - `dial_timeout_duration` (String) DialTimeout sets timeout when trying to connect to the server. This can also be set with the environment variable `TF_TELEPORT_DIAL_TIMEOUT_DURATION`. diff --git a/integrations/terraform/go.mod b/integrations/terraform/go.mod index 3f039a0a084c1..cfb971022b0aa 100644 --- a/integrations/terraform/go.mod +++ b/integrations/terraform/go.mod @@ -28,6 +28,8 @@ require ( google.golang.org/protobuf v1.34.1 ) +require github.com/hashicorp/terraform-plugin-log v0.9.0 + require ( cloud.google.com/go v0.112.2 // indirect cloud.google.com/go/auth v0.3.0 // indirect @@ -206,7 +208,6 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.21.0 // indirect github.com/hashicorp/terraform-json v0.22.1 // indirect - github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.1 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect diff --git a/integrations/terraform/provider/credentials.go b/integrations/terraform/provider/credentials.go new file mode 100644 index 0000000000000..9e04ed9639909 --- /dev/null +++ b/integrations/terraform/provider/credentials.go @@ -0,0 +1,487 @@ +/* +Copyright 2024 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "bytes" + "context" + "encoding/base64" + "fmt" + "strings" + "text/template" + "time" + + "github.com/gravitational/trace" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/gravitational/teleport/api/client" + "github.com/gravitational/teleport/api/constants" +) + +var supportedCredentialSources = CredentialSources{ + CredentialsFromKeyAndCertPath{}, + CredentialsFromKeyAndCertBase64{}, + CredentialsFromIdentityFilePath{}, + CredentialsFromIdentityFileString{}, + CredentialsFromIdentityFileBase64{}, + CredentialsFromProfile{}, +} + +// CredentialSources is a list of CredentialSource +type CredentialSources []CredentialSource + +// ActiveSources returns the list of active sources, and an error diagnostic if no source is active. +// The error diagnostic explains why every source is inactive. +func (s CredentialSources) ActiveSources(ctx context.Context, config providerData) (CredentialSources, diag.Diagnostics) { + var activeSources CredentialSources + inactiveReason := strings.Builder{} + for _, source := range s { + active, reason := source.IsActive(config) + logFields := map[string]interface{}{ + "source": source.Name(), + "active": active, + "reason": reason, + } + if !active { + tflog.Info(ctx, "credentials source is not active, skipping", logFields) + inactiveReason.WriteString(fmt.Sprintf(" - cannot read credentials %s because %s\n", source.Name(), reason)) + continue + } + tflog.Info(ctx, "credentials source is active", logFields) + activeSources = append(activeSources, source) + } + if len(activeSources) == 0 { + // TODO: make this a hard failure in v17 + // We currently try to load credentials from the user profile. + // As trying broken credentials takes 30 seconds this is a very bad UX and we should get rid of this. + // Credentials from profile are not passing MFA4Admin anyway. + summary := inactiveReason.String() + + "\nThe provider will fallback to your current local profile (this behavior is deprecated and will be removed in v17, you should specify the profile name or directory)." + return CredentialSources{CredentialsFromProfile{isDefault: true}}, diag.Diagnostics{diag.NewWarningDiagnostic( + "No active Teleport credentials source found", + summary, + )} + } + return activeSources, nil +} + +// BuildClient sequentially builds credentials for every source and tries to use them to connect to Teleport. +// Any CredentialSource failing to return a Credential and a tls.Config causes a hard failure. +// If we have a valid credential but cannot connect, we send a warning and continue with the next credential +// (this is for backward compatibility). +// Expired credentials are skipped for the sake of UX. This is the most common failure mode and we can +// return an error quickly instead of hanging for 30 whole seconds. +func (s CredentialSources) BuildClient(ctx context.Context, clientCfg client.Config, providerCfg providerData) (*client.Client, diag.Diagnostics) { + diags := diag.Diagnostics{} + for _, source := range s { + logFields := map[string]interface{}{ + "source": source.Name(), + } + tflog.Info(ctx, fmt.Sprintf("trying to build a client %s", source.Name()), logFields) + creds, err := source.Credentials(ctx, providerCfg) + if err != nil { + logFields["error"] = err.Error() + tflog.Error(ctx, "failed to obtain credential", logFields) + _, reason := source.IsActive(providerCfg) + diags.AddError( + fmt.Sprintf("Failed to obtain Teleport credentials %s", source.Name()), + brokenCredentialErrorSummary(source.Name(), reason, err), + ) + return nil, diags + } + + // Smoke test to see if the credential is valid + // This catches all the "file not found" issues and other broken credentials + // so we can turn them into a hard failure. + _, err = creds.TLSConfig() + if err != nil { + logFields["error"] = err.Error() + tflog.Error(ctx, "failed to get a TLSConfig from the credential", logFields) + _, reason := source.IsActive(providerCfg) + diags.AddError( + fmt.Sprintf("Invalid Teleport credentials %s", source.Name()), + brokenCredentialErrorSummary(source.Name(), reason, err), + ) + + return nil, diags + } + + now := time.Now() + if expiry, ok := creds.Expiry(); ok && !expiry.IsZero() && expiry.Before(now) { + diags.AddWarning( + fmt.Sprintf("Teleport credentials %s are expired", source.Name()), + fmt.Sprintf(`The credentials %s are expired. Expiration is %q while current time is %q). You might need to refresh them. The provider will not attempt to use those credentials.`, + source.Name(), expiry.Local(), now.Local()), + ) + continue + } + + clientCfg.Credentials = []client.Credentials{creds} + // In case of connection failure, this takes 30 seconds to return, which is very, very long. + clt, err := client.New(ctx, clientCfg) + if err != nil { + logFields["error"] = err.Error() + tflog.Error(ctx, "failed to connect with the credential", logFields) + diags.AddWarning( + fmt.Sprintf("Failed to connect with credentials %s", source.Name()), + fmt.Sprintf("The client built from the credentials %s failed to connect to %q with the error: %s.", + source.Name(), clientCfg.Addrs[0], err, + )) + continue + } + // A client was successfully built + return clt, diags + } + // No client was built + diags.AddError("Impossible to build Teleport client", s.failedToBuildClientErrorSummary(clientCfg.Addrs[0])) + return nil, diags +} + +const failedToBuildClientErrorTemplate = `"Every credential source provided has failed. The Terraform provider cannot connect to the Teleport cluster '{{.Addr}}'. + +The provider tried building a client: +{{- range $_, $source := .Sources }} +- {{ $source }} +{{- end }} + +You can find more information about why each credential source failed in the Terraform warnings above this error.` + +// failedToBuildClientErrorSummary builds a user-friendly message explaining we failed to build a functional Teleport +// client and listing every connection method we tried. +func (s CredentialSources) failedToBuildClientErrorSummary(addr string) string { + var sources []string + for _, source := range s { + sources = append(sources, source.Name()) + } + + tpl := template.Must(template.New("failed-to-build-client-error-summary").Parse(failedToBuildClientErrorTemplate)) + values := struct { + Addr string + Sources []string + }{ + Addr: addr, + Sources: sources, + } + buffer := new(bytes.Buffer) + err := tpl.Execute(buffer, values) + if err != nil { + return "Failed to build error summary. This is a provider bug: " + err.Error() + } + return buffer.String() +} + +const brokenCredentialErrorTemplate = `The Terraform provider tried to build credentials {{ .Source }} but received the following error: + +{{ .Error }} + +The provider tried to use the credential source because {{ .Reason }}. You must either address the error or disable the credential source by removing its values.` + +// brokenCredentialErrorSummary returns a user-friendly message explaining why we failed to +func brokenCredentialErrorSummary(name, activeReason string, err error) string { + tpl := template.Must(template.New("broken-credential-error-summary").Parse(brokenCredentialErrorTemplate)) + values := struct { + Source string + Error string + Reason string + }{ + Source: name, + Error: err.Error(), + Reason: activeReason, + } + buffer := new(bytes.Buffer) + tplErr := tpl.Execute(buffer, values) + if tplErr != nil { + return fmt.Sprintf("Failed to build error '%s' summary. This is a provider bug: %s", err, tplErr) + } + return buffer.String() +} + +// CredentialSource is a potential way for the Terraform provider to obtain the +// client.Credentials needed to connect to the Teleport cluster. +// A CredentialSource is active if the user specified configuration specific to this source. +// Only active CredentialSources are considered by the Provider. +type CredentialSource interface { + Name() string + IsActive(providerData) (bool, string) + Credentials(context.Context, providerData) (client.Credentials, error) +} + +// CredentialsFromKeyAndCertPath builds credentials from key, cert and ca cert paths. +type CredentialsFromKeyAndCertPath struct{} + +// Name implements CredentialSource and returns the source name. +func (CredentialsFromKeyAndCertPath) Name() string { + return "from Key, Cert, and CA path" +} + +// IsActive implements CredentialSource and returns if the source is active and why. +func (CredentialsFromKeyAndCertPath) IsActive(config providerData) (bool, string) { + certPath := stringFromConfigOrEnv(config.CertPath, constants.EnvVarTerraformCertificates, "") + keyPath := stringFromConfigOrEnv(config.KeyPath, constants.EnvVarTerraformKey, "") + + // This method is active as soon as a cert or a key path are set. + active := certPath != "" || keyPath != "" + + return activeReason( + active, + attributeTerraformCertificates, attributeTerraformKey, + constants.EnvVarTerraformCertificates, constants.EnvVarTerraformKey, + ) +} + +// Credentials implements CredentialSource and returns a client.Credentials for the provider. +func (CredentialsFromKeyAndCertPath) Credentials(ctx context.Context, config providerData) (client.Credentials, error) { + certPath := stringFromConfigOrEnv(config.CertPath, constants.EnvVarTerraformCertificates, "") + keyPath := stringFromConfigOrEnv(config.KeyPath, constants.EnvVarTerraformKey, "") + caPath := stringFromConfigOrEnv(config.RootCaPath, constants.EnvVarTerraformRootCertificates, "") + + // Validate that we have all paths. + if certPath == "" { + return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformCertificates, constants.EnvVarTerraformCertificates) + } + if keyPath == "" { + return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformKey, constants.EnvVarTerraformKey) + } + if caPath == "" { + return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformRootCertificates, constants.EnvVarTerraformRootCertificates) + } + + // Validate the files exist for a better UX? + + creds := client.LoadKeyPair(certPath, keyPath, caPath) + return creds, nil +} + +// CredentialsFromKeyAndCertBase64 builds credentials from key, cert, and CA cert base64. +type CredentialsFromKeyAndCertBase64 struct{} + +// Name implements CredentialSource and returns the source name. +func (CredentialsFromKeyAndCertBase64) Name() string { + return "from Key, Cert, and CA base64" +} + +// IsActive implements CredentialSource and returns if the source is active and why. +func (CredentialsFromKeyAndCertBase64) IsActive(config providerData) (bool, string) { + certBase64 := stringFromConfigOrEnv(config.CertBase64, constants.EnvVarTerraformCertificatesBase64, "") + keyBase64 := stringFromConfigOrEnv(config.KeyBase64, constants.EnvVarTerraformKeyBase64, "") + + // This method is active as soon as a cert or a key is passed. + active := certBase64 != "" || keyBase64 != "" + + return activeReason( + active, + attributeTerraformCertificatesBase64, attributeTerraformKeyBase64, + constants.EnvVarTerraformCertificatesBase64, constants.EnvVarTerraformKeyBase64, + ) +} + +// Credentials implements CredentialSource and returns a client.Credentials for the provider. +func (CredentialsFromKeyAndCertBase64) Credentials(ctx context.Context, config providerData) (client.Credentials, error) { + certBase64 := stringFromConfigOrEnv(config.CertBase64, constants.EnvVarTerraformCertificatesBase64, "") + keyBase64 := stringFromConfigOrEnv(config.KeyBase64, constants.EnvVarTerraformKeyBase64, "") + caBase64 := stringFromConfigOrEnv(config.RootCaBase64, constants.EnvVarTerraformRootCertificatesBase64, "") + + // Validate that we have all paths. + if certBase64 == "" { + return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformCertificatesBase64, constants.EnvVarTerraformCertificatesBase64) + } + if keyBase64 == "" { + return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformKeyBase64, constants.EnvVarTerraformKeyBase64) + } + if caBase64 == "" { + return nil, trace.BadParameter("missing parameter %q or environment variable %q", attributeTerraformRootCertificatesBase64, constants.EnvVarTerraformRootCertificatesBase64) + } + + certPEM, err := base64.StdEncoding.DecodeString(certBase64) + if err != nil { + return nil, trace.Wrap(err, "failed to decode the certificate's base64 (standard b64 encoding)") + } + keyPEM, err := base64.StdEncoding.DecodeString(keyBase64) + if err != nil { + return nil, trace.Wrap(err, "failed to decode the key's base64 (standard b64 encoding)") + } + caPEM, err := base64.StdEncoding.DecodeString(caBase64) + if err != nil { + return nil, trace.Wrap(err, "failed to decode the CA's base64 (standard b64 encoding)") + } + + creds, err := client.KeyPair(certPEM, keyPEM, caPEM) + return creds, trace.Wrap(err, "failed to load credentials from the PEM-encoded key and certificate") +} + +// CredentialsFromIdentityFilePath builds credentials from an identity file path. +type CredentialsFromIdentityFilePath struct{} + +// Name implements CredentialSource and returns the source name. +func (CredentialsFromIdentityFilePath) Name() string { + return "from the identity file path" +} + +// IsActive implements CredentialSource and returns if the source is active and why. +func (CredentialsFromIdentityFilePath) IsActive(config providerData) (bool, string) { + identityFilePath := stringFromConfigOrEnv(config.IdentityFilePath, constants.EnvVarTerraformIdentityFilePath, "") + + active := identityFilePath != "" + + return activeReason( + active, + attributeTerraformIdentityFilePath, constants.EnvVarTerraformIdentityFilePath, + ) +} + +// Credentials implements CredentialSource and returns a client.Credentials for the provider. +func (CredentialsFromIdentityFilePath) Credentials(ctx context.Context, config providerData) (client.Credentials, error) { + identityFilePath := stringFromConfigOrEnv(config.IdentityFilePath, constants.EnvVarTerraformIdentityFilePath, "") + + return client.LoadIdentityFile(identityFilePath), nil +} + +// CredentialsFromIdentityFileString builds credentials from an identity file passed as a string. +type CredentialsFromIdentityFileString struct{} + +// Name implements CredentialSource and returns the source name. +func (CredentialsFromIdentityFileString) Name() string { + return "from the identity file (passed as a string)" +} + +// IsActive implements CredentialSource and returns if the source is active and why. +func (CredentialsFromIdentityFileString) IsActive(config providerData) (bool, string) { + identityFileString := stringFromConfigOrEnv(config.IdentityFile, constants.EnvVarTerraformIdentityFile, "") + + active := identityFileString != "" + + return activeReason( + active, + attributeTerraformIdentityFile, constants.EnvVarTerraformIdentityFile, + ) +} + +// Credentials implements CredentialSource and returns a client.Credentials for the provider. +func (CredentialsFromIdentityFileString) Credentials(ctx context.Context, config providerData) (client.Credentials, error) { + identityFileString := stringFromConfigOrEnv(config.IdentityFile, constants.EnvVarTerraformIdentityFile, "") + + return client.LoadIdentityFileFromString(identityFileString), nil +} + +// CredentialsFromIdentityFileBase64 builds credentials from an identity file passed as a base64-encoded string. +type CredentialsFromIdentityFileBase64 struct{} + +// Name implements CredentialSource and returns the source name. +func (CredentialsFromIdentityFileBase64) Name() string { + return "from the identity file (passed as a base64-encoded string)" +} + +// IsActive implements CredentialSource and returns if the source is active and why. +func (CredentialsFromIdentityFileBase64) IsActive(config providerData) (bool, string) { + identityFileBase64 := stringFromConfigOrEnv(config.IdentityFileBase64, constants.EnvVarTerraformIdentityFileBase64, "") + + // This method is active as soon as a cert or a key path are set. + active := identityFileBase64 != "" + + return activeReason( + active, + attributeTerraformIdentityFileBase64, constants.EnvVarTerraformIdentityFileBase64, + ) +} + +// Credentials implements CredentialSource and returns a client.Credentials for the provider. +func (CredentialsFromIdentityFileBase64) Credentials(ctx context.Context, config providerData) (client.Credentials, error) { + identityFileBase64 := stringFromConfigOrEnv(config.IdentityFileBase64, constants.EnvVarTerraformIdentityFileBase64, "") + + identityFile, err := base64.StdEncoding.DecodeString(identityFileBase64) + if err != nil { + return nil, trace.Wrap(err, "decoding base64 identity file") + } + + return client.LoadIdentityFileFromString(string(identityFile)), nil +} + +// CredentialsFromProfile builds credentials from a local tsh profile. +type CredentialsFromProfile struct { + // isDefault represent if the CredentialSource is used as the default one. + // In this case, it explains that it is always active. + isDefault bool +} + +// Name implements CredentialSource and returns the source name. +func (c CredentialsFromProfile) Name() string { + name := "from the local profile" + if c.isDefault { + name += " (default)" + } + return name +} + +// IsActive implements CredentialSource and returns if the source is active and why. +func (c CredentialsFromProfile) IsActive(config providerData) (bool, string) { + if c.isDefault { + return true, "this is the default credential source, and no other credential was active" + } + + profileName := stringFromConfigOrEnv(config.ProfileName, constants.EnvVarTerraformProfileName, "") + profileDir := stringFromConfigOrEnv(config.ProfileDir, constants.EnvVarTerraformProfilePath, "") + + // This method is active as soon as a cert or a key path are set. + active := profileDir != "" || profileName != "" + return activeReason( + active, + attributeTerraformProfileName, attributeTerraformProfilePath, + constants.EnvVarTerraformProfileName, constants.EnvVarTerraformProfilePath, + ) +} + +// Credentials implements CredentialSource and returns a client.Credentials for the provider. +func (CredentialsFromProfile) Credentials(ctx context.Context, config providerData) (client.Credentials, error) { + profileName := stringFromConfigOrEnv(config.ProfileName, constants.EnvVarTerraformProfileName, "") + profileDir := stringFromConfigOrEnv(config.ProfileDir, constants.EnvVarTerraformProfilePath, "") + + return client.LoadProfile(profileDir, profileName), nil +} + +// activeReason renders a user-friendly active reason message describing if the credentials source is active +// and which parameters are controlling its activity. +func activeReason(active bool, params ...string) (bool, string) { + sb := new(strings.Builder) + var firstConjunction, lastConjunction string + + switch active { + case true: + firstConjunction = "either " + lastConjunction = "or " + case false: + firstConjunction = "neither " + lastConjunction = "nor " + } + + sb.WriteString(firstConjunction) + + for i, item := range params { + switch i { + case len(params) - 1: + sb.WriteString(lastConjunction) + sb.WriteString(item) + sb.WriteRune(' ') + default: + sb.WriteString(item) + sb.WriteString(", ") + } + + } + sb.WriteString("are set") + return active, sb.String() +} diff --git a/integrations/terraform/provider/credentials_test.go b/integrations/terraform/provider/credentials_test.go new file mode 100644 index 0000000000000..528a878e348ff --- /dev/null +++ b/integrations/terraform/provider/credentials_test.go @@ -0,0 +1,118 @@ +/* +Copyright 2024 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + "testing" + + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/client" +) + +func TestActiveSources(t *testing.T) { + ctx := context.Background() + + activeSource1 := fakeActiveCredentialsSource{"active1"} + activeSource2 := fakeActiveCredentialsSource{"active2"} + inactiveSource1 := fakeInactiveCredentialsSource{"inactive1"} + inactiveSource2 := fakeInactiveCredentialsSource{"inactive2"} + + tests := []struct { + name string + sources CredentialSources + expectedSources CredentialSources + wantErr bool + }{ + { + name: "no source", + sources: CredentialSources{}, + expectedSources: nil, + wantErr: true, + }, + { + name: "no active source", + sources: CredentialSources{ + inactiveSource1, + inactiveSource2, + }, + expectedSources: nil, + wantErr: true, + }, + { + name: "single active source", + sources: CredentialSources{ + activeSource1, + }, + expectedSources: CredentialSources{activeSource1}, + wantErr: false, + }, + { + name: "multiple active and inactive sources", + sources: CredentialSources{ + inactiveSource1, + activeSource1, + inactiveSource2, + activeSource2, + }, + expectedSources: CredentialSources{activeSource1, activeSource2}, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, diags := tt.sources.ActiveSources(ctx, providerData{}) + require.Equal(t, tt.wantErr, diags.HasError()) + require.Equal(t, tt.expectedSources, result) + }) + } +} + +type fakeActiveCredentialsSource struct { + name string +} + +func (f fakeActiveCredentialsSource) Name() string { + return f.name +} + +func (f fakeActiveCredentialsSource) IsActive(data providerData) (bool, string) { + return true, "" +} + +func (f fakeActiveCredentialsSource) Credentials(ctx context.Context, data providerData) (client.Credentials, error) { + return nil, trace.NotImplemented("not implemented") +} + +type fakeInactiveCredentialsSource struct { + name string +} + +func (f fakeInactiveCredentialsSource) Name() string { + return f.name +} + +func (f fakeInactiveCredentialsSource) IsActive(data providerData) (bool, string) { + return false, "" +} + +func (f fakeInactiveCredentialsSource) Credentials(ctx context.Context, data providerData) (client.Credentials, error) { + return nil, trace.NotImplemented("not implemented") +} diff --git a/integrations/terraform/provider/provider.go b/integrations/terraform/provider/provider.go index df2d1eeead643..19b6a4a4f2cc2 100644 --- a/integrations/terraform/provider/provider.go +++ b/integrations/terraform/provider/provider.go @@ -18,13 +18,9 @@ package provider import ( "context" - "crypto/tls" - "crypto/x509" - "encoding/base64" "fmt" "net" "os" - "path/filepath" "strconv" "strings" "time" @@ -47,6 +43,47 @@ const ( minServerVersion = "15.0.0-0" ) +const ( + // attributeTerraformAddress is the attribute configuring the Teleport address the Terraform provider connects to. + attributeTerraformAddress = "addr" + // attributeTerraformCertificates is the attribute configuring the path the Terraform provider loads its + // client certificates from. This only works for direct auth joining. + attributeTerraformCertificates = "cert_path" + // attributeTerraformCertificatesBase64 is the attribute configuring the client certificates used by the + // Terraform provider. This only works for direct auth joining. + attributeTerraformCertificatesBase64 = "cert_base64" + // attributeTerraformKey is the attribute configuring the path the Terraform provider loads its + // client key from. This only works for direct auth joining. + attributeTerraformKey = "key_path" + // attributeTerraformKeyBase64 is the attribute configuring the client key used by the + // Terraform provider. This only works for direct auth joining. + attributeTerraformKeyBase64 = "key_base64" + // attributeTerraformRootCertificates is the attribute configuring the path the Terraform provider loads its + // trusted CA certificates from. This only works for direct auth joining. + attributeTerraformRootCertificates = "root_ca_path" + // attributeTerraformRootCertificatesBase64 is the attribute configuring the CA certificates trusted by the + // Terraform provider. This only works for direct auth joining. + attributeTerraformRootCertificatesBase64 = "root_ca_base64" + // attributeTerraformProfileName is the attribute containing name of the profile used by the Terraform provider. + attributeTerraformProfileName = "profile_name" + // attributeTerraformProfilePath is the attribute containing the profile directory used by the Terraform provider. + attributeTerraformProfilePath = "profile_dir" + // attributeTerraformIdentityFilePath is the attribute containing the path to the identity file used by the provider. + attributeTerraformIdentityFilePath = "identity_file_path" + // attributeTerraformIdentityFile is the attribute containing the identity file used by the Terraform provider. + attributeTerraformIdentityFile = "identity_file" + // attributeTerraformIdentityFileBase64 is the attribute containing the base64-encoded identity file used by the Terraform provider. + attributeTerraformIdentityFileBase64 = "identity_file_base64" + // attributeTerraformRetryBaseDuration is the attribute configuring the base duration between two Terraform provider retries. + attributeTerraformRetryBaseDuration = "retry_base_duration" + // attributeTerraformRetryCapDuration is the attribute configuring the maximum duration between two Terraform provider retries. + attributeTerraformRetryCapDuration = "retry_cap_duration" + // attributeTerraformRetryMaxTries is the attribute configuring the maximum number of Terraform provider retries. + attributeTerraformRetryMaxTries = "retry_max_tries" + // attributeTerraformDialTimeoutDuration is the attribute configuring the Terraform provider dial timeout. + attributeTerraformDialTimeoutDuration = "dial_timeout_duration" +) + type RetryConfig struct { Base time.Duration Cap time.Duration @@ -105,92 +142,92 @@ func New() tfsdk.Provider { func (p *Provider) GetSchema(_ context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Attributes: map[string]tfsdk.Attribute{ - "addr": { + attributeTerraformAddress: { Type: types.StringType, Optional: true, - Description: "host:port where Teleport Auth Service is running. This can also be set with the environment variable `TF_TELEPORT_ADDR`.", + Description: fmt.Sprintf("host:port of the Teleport address. This can be the Teleport Proxy Service address (port 443 or 4080) or the Teleport Auth Service address (port 3025). This can also be set with the environment variable `%s`.", constants.EnvVarTerraformAddress), }, - "cert_path": { + attributeTerraformCertificates: { Type: types.StringType, Optional: true, - Description: "Path to Teleport auth certificate file. This can also be set with the environment variable `TF_TELEPORT_CERT`.", + Description: fmt.Sprintf("Path to Teleport auth certificate file. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformCertificates), }, - "cert_base64": { + attributeTerraformCertificatesBase64: { Type: types.StringType, Optional: true, - Description: "Base64 encoded TLS auth certificate. This can also be set with the environment variable `TF_TELEPORT_CERT_BASE64`.", + Description: fmt.Sprintf("Base64 encoded TLS auth certificate. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformCertificatesBase64), }, - "key_path": { + attributeTerraformKey: { Type: types.StringType, Optional: true, - Description: "Path to Teleport auth key file. This can also be set with the environment variable `TF_TELEPORT_KEY`.", + Description: fmt.Sprintf("Path to Teleport auth key file. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformKey), }, - "key_base64": { + attributeTerraformKeyBase64: { Type: types.StringType, Sensitive: true, Optional: true, - Description: "Base64 encoded TLS auth key. This can also be set with the environment variable `TF_TELEPORT_KEY_BASE64`.", + Description: fmt.Sprintf("Base64 encoded TLS auth key. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformKeyBase64), }, - "root_ca_path": { + attributeTerraformRootCertificates: { Type: types.StringType, Optional: true, - Description: "Path to Teleport Root CA. This can also be set with the environment variable `TF_TELEPORT_ROOT_CA`.", + Description: fmt.Sprintf("Path to Teleport Root CA. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformRootCertificates), }, - "root_ca_base64": { + attributeTerraformRootCertificatesBase64: { Type: types.StringType, Optional: true, - Description: "Base64 encoded Root CA. This can also be set with the environment variable `TF_TELEPORT_CA_BASE64`.", + Description: fmt.Sprintf("Base64 encoded Root CA. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformRootCertificatesBase64), }, - "profile_name": { + attributeTerraformProfileName: { Type: types.StringType, Optional: true, - Description: "Teleport profile name. This can also be set with the environment variable `TF_TELEPORT_PROFILE_NAME`.", + Description: fmt.Sprintf("Teleport profile name. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformProfileName), }, - "profile_dir": { + attributeTerraformProfilePath: { Type: types.StringType, Optional: true, - Description: "Teleport profile path. This can also be set with the environment variable `TF_TELEPORT_PROFILE_PATH`.", + Description: fmt.Sprintf("Teleport profile path. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformProfilePath), }, - "identity_file_path": { + attributeTerraformIdentityFilePath: { Type: types.StringType, Optional: true, - Description: "Teleport identity file path. This can also be set with the environment variable `TF_TELEPORT_IDENTITY_FILE_PATH`.", + Description: fmt.Sprintf("Teleport identity file path. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformIdentityFilePath), }, - "identity_file": { + attributeTerraformIdentityFile: { Type: types.StringType, Sensitive: true, Optional: true, - Description: "Teleport identity file content. This can also be set with the environment variable `TF_TELEPORT_IDENTITY_FILE`.", + Description: fmt.Sprintf("Teleport identity file content. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformIdentityFile), }, - "identity_file_base64": { + attributeTerraformIdentityFileBase64: { Type: types.StringType, Sensitive: true, Optional: true, - Description: "Teleport identity file content base64 encoded. This can also be set with the environment variable `TF_TELEPORT_IDENTITY_FILE_BASE64`.", + Description: fmt.Sprintf("Teleport identity file content base64 encoded. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformIdentityFileBase64), }, - "retry_base_duration": { + attributeTerraformRetryBaseDuration: { Type: types.StringType, Sensitive: false, Optional: true, - Description: "Retry algorithm when the API returns 'not found': base duration between retries (https://pkg.go.dev/time#ParseDuration). This can also be set with the environment variable `TF_TELEPORT_RETRY_BASE_DURATION`.", + Description: fmt.Sprintf("Retry algorithm when the API returns 'not found': base duration between retries (https://pkg.go.dev/time#ParseDuration). This can also be set with the environment variable `%s`.", constants.EnvVarTerraformRetryBaseDuration), }, - "retry_cap_duration": { + attributeTerraformRetryCapDuration: { Type: types.StringType, Sensitive: false, Optional: true, - Description: "Retry algorithm when the API returns 'not found': max duration between retries (https://pkg.go.dev/time#ParseDuration). This can also be set with the environment variable `TF_TELEPORT_RETRY_CAP_DURATION`.", + Description: fmt.Sprintf("Retry algorithm when the API returns 'not found': max duration between retries (https://pkg.go.dev/time#ParseDuration). This can also be set with the environment variable `%s`.", constants.EnvVarTerraformRetryCapDuration), }, - "retry_max_tries": { + attributeTerraformRetryMaxTries: { Type: types.StringType, Sensitive: false, Optional: true, - Description: "Retry algorithm when the API returns 'not found': max tries. This can also be set with the environment variable `TF_TELEPORT_RETRY_MAX_TRIES`.", + Description: fmt.Sprintf("Retry algorithm when the API returns 'not found': max tries. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformRetryMaxTries), }, - "dial_timeout_duration": { + attributeTerraformDialTimeoutDuration: { Type: types.StringType, Sensitive: false, Optional: true, - Description: "DialTimeout sets timeout when trying to connect to the server. This can also be set with the environment variable `TF_TELEPORT_DIAL_TIMEOUT_DURATION`.", + Description: fmt.Sprintf("DialTimeout sets timeout when trying to connect to the server. This can also be set with the environment variable `%s`.", constants.EnvVarTerraformDialTimeoutDuration), }, }, }, nil @@ -210,8 +247,6 @@ func (p *Provider) IsConfigured(diags diag.Diagnostics) bool { // Configure configures the Teleport client func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) { - var creds []client.Credentials - p.configureLog() var config providerData @@ -221,22 +256,11 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq return } - addr := p.stringFromConfigOrEnv(config.Addr, constants.EnvVarTerraformAddress, "") - certPath := p.stringFromConfigOrEnv(config.CertPath, constants.EnvVarTerraformCertificates, "") - certBase64 := p.stringFromConfigOrEnv(config.CertBase64, constants.EnvVarTerraformCertificatesBase64, "") - keyPath := p.stringFromConfigOrEnv(config.KeyPath, constants.EnvVarTerraformKey, "") - keyBase64 := p.stringFromConfigOrEnv(config.KeyBase64, constants.EnvVarTerraformKeyBase64, "") - caPath := p.stringFromConfigOrEnv(config.RootCaPath, constants.EnvVarTerraformRootCertificates, "") - caBase64 := p.stringFromConfigOrEnv(config.RootCaBase64, constants.EnvVarTerraformRootCertificatesBase64, "") - profileName := p.stringFromConfigOrEnv(config.ProfileName, constants.EnvVarTerraformProfileName, "") - profileDir := p.stringFromConfigOrEnv(config.ProfileDir, constants.EnvVarTerraformProfilePath, "") - identityFilePath := p.stringFromConfigOrEnv(config.IdentityFilePath, constants.EnvVarTerraformIdentityFilePath, "") - identityFile := p.stringFromConfigOrEnv(config.IdentityFile, constants.EnvVarTerraformIdentityFile, "") - identityFileBase64 := p.stringFromConfigOrEnv(config.IdentityFileBase64, constants.EnvVarTerraformIdentityFileBase64, "") - retryBaseDurationStr := p.stringFromConfigOrEnv(config.RetryBaseDuration, constants.EnvVarTerraformRetryBaseDuration, "1s") - retryCapDurationStr := p.stringFromConfigOrEnv(config.RetryCapDuration, constants.EnvVarTerraformRetryCapDuration, "5s") - maxTriesStr := p.stringFromConfigOrEnv(config.RetryMaxTries, constants.EnvVarTerraformRetryMaxTries, "10") - dialTimeoutDurationStr := p.stringFromConfigOrEnv(config.DialTimeoutDuration, constants.EnvVarTerraformDialTimeoutDuration, "30s") + addr := stringFromConfigOrEnv(config.Addr, constants.EnvVarTerraformAddress, "") + retryBaseDurationStr := stringFromConfigOrEnv(config.RetryBaseDuration, constants.EnvVarTerraformRetryBaseDuration, "1s") + retryCapDurationStr := stringFromConfigOrEnv(config.RetryCapDuration, constants.EnvVarTerraformRetryCapDuration, "5s") + maxTriesStr := stringFromConfigOrEnv(config.RetryMaxTries, constants.EnvVarTerraformRetryMaxTries, "10") + dialTimeoutDurationStr := stringFromConfigOrEnv(config.DialTimeoutDuration, constants.EnvVarTerraformDialTimeoutDuration, "30s") if !p.validateAddr(addr, resp) { return @@ -244,83 +268,26 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq log.WithFields(log.Fields{"addr": addr}).Debug("Using Teleport address") - if certPath != "" && keyPath != "" { - l := log.WithField("cert_path", certPath).WithField("key_path", keyPath).WithField("root_ca_path", caPath) - l.Debug("Using auth with certificate, private key and (optionally) CA read from files") - - cred, ok := p.getCredentialsFromKeyPair(certPath, keyPath, caPath, resp) - if !ok { - return - } - creds = append(creds, cred) - } - - if certBase64 != "" && keyBase64 != "" { - log.Debug("Using auth with certificate, private key and (optionally) CA read from base64 encoded vars") - cred, ok := p.getCredentialsFromBase64(certBase64, keyBase64, caBase64, resp) - if !ok { - return - } - creds = append(creds, cred) - } - - if identityFilePath != "" { - log.WithField("identity_file_path", identityFilePath).Debug("Using auth with identity file") - - if !p.fileExists(identityFilePath) { - resp.Diagnostics.AddError( - "Identity file not found", - fmt.Sprintf( - "File %v not found! Use `tctl auth sign --user=example@example.com --format=file --out=%v` to generate identity file", - identityFilePath, - identityFilePath, - ), - ) - return - } - - creds = append(creds, client.LoadIdentityFile(identityFilePath)) - } - - if identityFile != "" { - log.Debug("Using auth from identity file provided with environment variable TF_TELEPORT_IDENTITY_FILE") - creds = append(creds, client.LoadIdentityFileFromString(identityFile)) - } - - if identityFileBase64 != "" { - log.Debug("Using auth from base64 encoded identity file provided with environment variable TF_TELEPORT_IDENTITY_FILE_BASE64") - decoded, err := base64.StdEncoding.DecodeString(identityFileBase64) - if err != nil { - resp.Diagnostics.AddError( - "Failed to decode Identity file using base 64", - fmt.Sprintf("Error when trying to decode: %v", err), - ) - return - } - - creds = append(creds, client.LoadIdentityFileFromString(string(decoded))) - } - - if profileDir != "" || len(creds) == 0 { - log.WithFields(log.Fields{ - "dir": profileDir, - "name": profileName, - }).Debug("Using profile as the default auth method") - creds = append(creds, client.LoadProfile(profileDir, profileName)) - } - dialTimeoutDuration, err := time.ParseDuration(dialTimeoutDurationStr) if err != nil { resp.Diagnostics.AddError( "Failed to parse Dial Timeout Duration Cap Duration", - fmt.Sprintf("Please check if dial_timeout_duration (or TF_TELEPORT_DIAL_TIMEOUT_DURATION) is set correctly. Error: %s", err), + fmt.Sprintf( + "Please check if %s (or %s) is set correctly. Error: %s", + attributeTerraformDialTimeoutDuration, constants.EnvVarTerraformDialTimeoutDuration, err, + ), ) return } - client, err := client.New(ctx, client.Config{ + activeSources, diags := supportedCredentialSources.ActiveSources(ctx, config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + clientConfig := client.Config{ Addrs: []string{addr}, - Credentials: creds, DialTimeout: dialTimeoutDuration, DialOpts: []grpc.DialOption{ grpc.WithReturnConnectionError(), @@ -328,15 +295,15 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq grpc.WaitForReady(true), ), }, - }) + } - if err != nil { - log.WithError(err).Debug("Error connecting to Teleport!") - resp.Diagnostics.AddError("Error connecting to Teleport!", err.Error()) + clt, diags := activeSources.BuildClient(ctx, clientConfig, config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } - if !p.checkTeleportVersion(ctx, client, resp) { + if !p.checkTeleportVersion(ctx, clt, resp) { return } @@ -344,7 +311,10 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq if err != nil { resp.Diagnostics.AddError( "Failed to parse Retry Base Duration", - fmt.Sprintf("Please check if retry_cap_duration (or TF_TELEPORT_RETRY_BASE_DURATION) is set correctly. Error: %s", err), + fmt.Sprintf( + "Please check if %s (or %s) is set correctly. Error: %s", + attributeTerraformRetryBaseDuration, constants.EnvVarTerraformRetryBaseDuration, err, + ), ) return } @@ -353,7 +323,10 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq if err != nil { resp.Diagnostics.AddError( "Failed to parse Retry Cap Duration", - fmt.Sprintf("Please check if retry_cap_duration (or TF_TELEPORT_RETRY_CAP_DURATION) is set correctly. Error: %s", err), + fmt.Sprintf( + "Please check if %s (or %s) is set correctly. Error: %s", + attributeTerraformRetryCapDuration, constants.EnvVarTerraformRetryCapDuration, err, + ), ) return } @@ -362,7 +335,10 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq if err != nil { resp.Diagnostics.AddError( "Failed to parse Retry Max Tries", - fmt.Sprintf("Please check if retry_max_tries (or TF_TELEPORT_RETRY_MAX_TRIES) is set correctly. Error: %s", err), + fmt.Sprintf( + "Please check if %s (or %s) is set correctly. Error: %s", + attributeTerraformRetryMaxTries, constants.EnvVarTerraformRetryMaxTries, err, + ), ) return } @@ -372,7 +348,7 @@ func (p *Provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq Cap: retryCapDuration, MaxTries: int(maxTries), } - p.Client = client + p.Client = clt p.configured = true } @@ -402,7 +378,7 @@ func (p *Provider) checkTeleportVersion(ctx context.Context, client *client.Clie } // stringFromConfigOrEnv returns value from config or from env var if config value is empty, default otherwise -func (p *Provider) stringFromConfigOrEnv(value types.String, env string, def string) string { +func stringFromConfigOrEnv(value types.String, env string, def string) string { if value.Unknown || value.Null { value := os.Getenv(env) if value != "" { @@ -424,109 +400,25 @@ func (p *Provider) validateAddr(addr string, resp *tfsdk.ConfigureProviderRespon if addr == "" { resp.Diagnostics.AddError( "Teleport address is empty", - "Please, specify either TF_TELEPORT_ADDR or addr in provider configuration", + fmt.Sprintf("Please, specify either %s in provider configuration, or the %s environment variable", + attributeTerraformAddress, constants.EnvVarTerraformAddress), ) return false } _, _, err := net.SplitHostPort(addr) if err != nil { - log.WithField("addr", addr).WithError(err).Debug("Teleport addr format error!") + log.WithField("addr", addr).WithError(err).Debug("Teleport address format error!") resp.Diagnostics.AddError( - "Invalid Teleport addr format", - "Teleport addr must be specified as host:port", + "Invalid Teleport address format", + fmt.Sprintf("Teleport address must be specified as host:port. Got %q", addr), ) return false } return true } -// getCredentialsFromBase64 returns client.Credentials built from base64 encoded keys -func (p *Provider) getCredentialsFromBase64(certBase64, keyBase64, caBase64 string, resp *tfsdk.ConfigureProviderResponse) (client.Credentials, bool) { - cert, err := base64.StdEncoding.DecodeString(certBase64) - if err != nil { - resp.Diagnostics.AddError( - "Failed to base64 decode cert", - fmt.Sprintf("Please check if cert_base64 (or TF_TELEPORT_CERT_BASE64) is set correctly. Error: %s", err), - ) - return nil, false - } - key, err := base64.StdEncoding.DecodeString(keyBase64) - if err != nil { - resp.Diagnostics.AddError( - "Failed to base64 decode key", - fmt.Sprintf("Please check if key_base64 (or TF_TELEPORT_KEY_BASE64) is set correctly. Error: %s", err), - ) - return nil, false - } - rootCa, err := base64.StdEncoding.DecodeString(caBase64) - if err != nil { - resp.Diagnostics.AddError( - "Failed to base64 decode root ca", - fmt.Sprintf("Please check if root_ca_base64 (or TF_TELEPORT_CA_BASE64) is set correctly. Error: %s", err), - ) - return nil, false - } - tlsConfig, err := createTLSConfig(cert, key, rootCa) - if err != nil { - resp.Diagnostics.AddError( - "Failed to create TLS config", - fmt.Sprintf("Error: %s", err), - ) - return nil, false - } - return client.LoadTLS(tlsConfig), true -} - -// getCredentialsFromKeyPair returns client.Credentials built from path to key files -func (p *Provider) getCredentialsFromKeyPair(certPath string, keyPath string, caPath string, resp *tfsdk.ConfigureProviderResponse) (client.Credentials, bool) { - if !p.fileExists(certPath) { - resp.Diagnostics.AddError( - "Certificate file not found", - fmt.Sprintf("File %v not found! Use 'tctl auth sign --user=example@example.com --format=tls --out=%v' to generate keys", - certPath, - filepath.Dir(certPath), - ), - ) - return nil, false - } - - if !p.fileExists(keyPath) { - resp.Diagnostics.AddError( - "Private key file not found", - fmt.Sprintf("File %v not found! Use 'tctl auth sign --user=example@example.com --format=tls --out=%v' to generate keys", - keyPath, - filepath.Dir(keyPath), - ), - ) - return nil, false - } - - if !p.fileExists(caPath) { - resp.Diagnostics.AddError( - "Root CA certificate file not found", - fmt.Sprintf("File %v not found! Use 'tctl auth sign --user=example@example.com --format=tls --out=%v' to generate keys", - caPath, - filepath.Dir(caPath), - ), - ) - return nil, false - } - - return client.LoadKeyPair(certPath, keyPath, caPath), true -} - -// fileExists returns true if file exists -func (p *Provider) fileExists(path string) bool { - _, err := os.Stat(path) - if os.IsNotExist(err) { - return false - } - if err != nil { - return false - } - return true -} +// TODO(hugoShaka): fix logging in a future release by converting to tflog. // configureLog configures logging func (p *Provider) configureLog() { @@ -547,21 +439,6 @@ func (p *Provider) configureLog() { } } -// createTLSConfig returns tls.Config build from keys -func createTLSConfig(cert, key, rootCa []byte) (*tls.Config, error) { - keyPair, err := tls.X509KeyPair(cert, key) - if err != nil { - return nil, err - } - caCertPool := x509.NewCertPool() - caCertPool.AppendCertsFromPEM(rootCa) - - return &tls.Config{ - Certificates: []tls.Certificate{keyPair}, - RootCAs: caCertPool, - }, nil -} - // GetResources returns the map of provider resources func (p *Provider) GetResources(_ context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) { return map[string]tfsdk.ResourceType{