opentofu/internal/backend/remote-state/http/backend.go

261 lines
8.5 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package http
import (
"context"
"crypto/tls"
[fixes 31700] Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. (#31699) * Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. * Fix style. * Skip cert validation to be sure error is related to missing client cert; not untrusted server cert. * Remove misplaced err check. * Fix the size of test using http backend. * Just for correctness, include all certs in the pem encoded cert - sometimes certs come with a chain of their signers. * Adjusted names as recommended in PR comments. * Adjusted names to be full-length and more descriptive. * Added full-fledged testing with mTLS http server * Fix goimports. * Fix the names of the backend config. * Exclusive lock for write and delete. * Revert "Fix goimports." This reverts commit 7d40f6099fbbb675fb2e25e35ee40aeafe3d0a22. * goimports just for server test. * Added the go:generation for the mock. * Move the TLS configuration out to make it more readable - don't replace the HTTPClient as the retryablehttp already creates one - just configure its TLS. * Just switch the client/data params - felt more natural this way. * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/testdata/gencerts.sh Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * the location of the file name is not sensitive. * Added error if only one of client_certificate_pem and client_private_key_pem are set. * Remove testify from test cases; use t.Error* for assert and t.Fatal* for require. * Fixed import consistency * Just use default openssl. * Since file(...) is so trivial to use, changed the client cert, key, and ca cert to be the data. See also https://github.com/hashicorp/terraform-provider-http/pull/211 Co-authored-by: Sheridan C Rawlins <scr@ouryahoo.com> Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com>
2023-01-26 08:08:07 -06:00
"crypto/x509"
"errors"
"fmt"
2021-11-29 17:45:35 -06:00
"log"
"net/http"
"net/url"
"time"
"github.com/hashicorp/go-retryablehttp"
[fixes 31700] Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. (#31699) * Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. * Fix style. * Skip cert validation to be sure error is related to missing client cert; not untrusted server cert. * Remove misplaced err check. * Fix the size of test using http backend. * Just for correctness, include all certs in the pem encoded cert - sometimes certs come with a chain of their signers. * Adjusted names as recommended in PR comments. * Adjusted names to be full-length and more descriptive. * Added full-fledged testing with mTLS http server * Fix goimports. * Fix the names of the backend config. * Exclusive lock for write and delete. * Revert "Fix goimports." This reverts commit 7d40f6099fbbb675fb2e25e35ee40aeafe3d0a22. * goimports just for server test. * Added the go:generation for the mock. * Move the TLS configuration out to make it more readable - don't replace the HTTPClient as the retryablehttp already creates one - just configure its TLS. * Just switch the client/data params - felt more natural this way. * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/testdata/gencerts.sh Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * the location of the file name is not sensitive. * Added error if only one of client_certificate_pem and client_private_key_pem are set. * Remove testify from test cases; use t.Error* for assert and t.Fatal* for require. * Fixed import consistency * Just use default openssl. * Since file(...) is so trivial to use, changed the client cert, key, and ca cert to be the data. See also https://github.com/hashicorp/terraform-provider-http/pull/211 Co-authored-by: Sheridan C Rawlins <scr@ouryahoo.com> Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com>
2023-01-26 08:08:07 -06:00
"github.com/opentofu/opentofu/internal/backend"
"github.com/opentofu/opentofu/internal/legacy/helper/schema"
"github.com/opentofu/opentofu/internal/logging"
"github.com/opentofu/opentofu/internal/states/remote"
"github.com/opentofu/opentofu/internal/states/statemgr"
)
func New() backend.Backend {
s := &schema.Backend{
Schema: map[string]*schema.Schema{
"address": &schema.Schema{
Type: schema.TypeString,
Required: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_ADDRESS", nil),
Description: "The address of the REST endpoint",
},
"update_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UPDATE_METHOD", "POST"),
Description: "HTTP method to use when updating state",
},
"lock_address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_ADDRESS", nil),
Description: "The address of the lock REST endpoint",
},
"unlock_address": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_ADDRESS", nil),
Description: "The address of the unlock REST endpoint",
},
"lock_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_METHOD", "LOCK"),
Description: "The HTTP method to use when locking",
},
"unlock_method": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_METHOD", "UNLOCK"),
Description: "The HTTP method to use when unlocking",
},
"username": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_USERNAME", nil),
Description: "The username for HTTP basic authentication",
},
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_PASSWORD", nil),
Description: "The password for HTTP basic authentication",
},
"skip_cert_verification": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "Whether to skip TLS verification.",
},
"retry_max": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_MAX", 2),
Description: "The number of HTTP request retries.",
},
"retry_wait_min": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MIN", 1),
Description: "The minimum time in seconds to wait between HTTP request attempts.",
},
"retry_wait_max": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MAX", 30),
Description: "The maximum time in seconds to wait between HTTP request attempts.",
},
[fixes 31700] Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. (#31699) * Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. * Fix style. * Skip cert validation to be sure error is related to missing client cert; not untrusted server cert. * Remove misplaced err check. * Fix the size of test using http backend. * Just for correctness, include all certs in the pem encoded cert - sometimes certs come with a chain of their signers. * Adjusted names as recommended in PR comments. * Adjusted names to be full-length and more descriptive. * Added full-fledged testing with mTLS http server * Fix goimports. * Fix the names of the backend config. * Exclusive lock for write and delete. * Revert "Fix goimports." This reverts commit 7d40f6099fbbb675fb2e25e35ee40aeafe3d0a22. * goimports just for server test. * Added the go:generation for the mock. * Move the TLS configuration out to make it more readable - don't replace the HTTPClient as the retryablehttp already creates one - just configure its TLS. * Just switch the client/data params - felt more natural this way. * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/testdata/gencerts.sh Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * the location of the file name is not sensitive. * Added error if only one of client_certificate_pem and client_private_key_pem are set. * Remove testify from test cases; use t.Error* for assert and t.Fatal* for require. * Fixed import consistency * Just use default openssl. * Since file(...) is so trivial to use, changed the client cert, key, and ca cert to be the data. See also https://github.com/hashicorp/terraform-provider-http/pull/211 Co-authored-by: Sheridan C Rawlins <scr@ouryahoo.com> Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com>
2023-01-26 08:08:07 -06:00
"client_ca_certificate_pem": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CA_CERTIFICATE_PEM", ""),
Description: "A PEM-encoded CA certificate chain used by the client to verify server certificates during TLS authentication.",
},
"client_certificate_pem": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CERTIFICATE_PEM", ""),
Description: "A PEM-encoded certificate used by the server to verify the client during mutual TLS (mTLS) authentication.",
},
"client_private_key_pem": &schema.Schema{
Type: schema.TypeString,
Optional: true,
DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_PRIVATE_KEY_PEM", ""),
Description: "A PEM-encoded private key, required if client_certificate_pem is specified.",
},
},
}
b := &Backend{Backend: s}
b.Backend.ConfigureFunc = b.configure
return b
}
type Backend struct {
*schema.Backend
client *httpClient
}
[fixes 31700] Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. (#31699) * Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. * Fix style. * Skip cert validation to be sure error is related to missing client cert; not untrusted server cert. * Remove misplaced err check. * Fix the size of test using http backend. * Just for correctness, include all certs in the pem encoded cert - sometimes certs come with a chain of their signers. * Adjusted names as recommended in PR comments. * Adjusted names to be full-length and more descriptive. * Added full-fledged testing with mTLS http server * Fix goimports. * Fix the names of the backend config. * Exclusive lock for write and delete. * Revert "Fix goimports." This reverts commit 7d40f6099fbbb675fb2e25e35ee40aeafe3d0a22. * goimports just for server test. * Added the go:generation for the mock. * Move the TLS configuration out to make it more readable - don't replace the HTTPClient as the retryablehttp already creates one - just configure its TLS. * Just switch the client/data params - felt more natural this way. * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/testdata/gencerts.sh Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * the location of the file name is not sensitive. * Added error if only one of client_certificate_pem and client_private_key_pem are set. * Remove testify from test cases; use t.Error* for assert and t.Fatal* for require. * Fixed import consistency * Just use default openssl. * Since file(...) is so trivial to use, changed the client cert, key, and ca cert to be the data. See also https://github.com/hashicorp/terraform-provider-http/pull/211 Co-authored-by: Sheridan C Rawlins <scr@ouryahoo.com> Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com>
2023-01-26 08:08:07 -06:00
// configureTLS configures TLS when needed; if there are no conditions requiring TLS, no change is made.
func (b *Backend) configureTLS(client *retryablehttp.Client, data *schema.ResourceData) error {
// If there are no conditions needing to configure TLS, leave the client untouched
skipCertVerification := data.Get("skip_cert_verification").(bool)
clientCACertificatePem := data.Get("client_ca_certificate_pem").(string)
clientCertificatePem := data.Get("client_certificate_pem").(string)
clientPrivateKeyPem := data.Get("client_private_key_pem").(string)
if !skipCertVerification && clientCACertificatePem == "" && clientCertificatePem == "" && clientPrivateKeyPem == "" {
return nil
}
if clientCertificatePem != "" && clientPrivateKeyPem == "" {
return fmt.Errorf("client_certificate_pem is set but client_private_key_pem is not")
}
if clientPrivateKeyPem != "" && clientCertificatePem == "" {
return fmt.Errorf("client_private_key_pem is set but client_certificate_pem is not")
}
// TLS configuration is needed; create an object and configure it
var tlsConfig tls.Config
client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = &tlsConfig
if skipCertVerification {
// ignores TLS verification
tlsConfig.InsecureSkipVerify = true
}
if clientCACertificatePem != "" {
// trust servers based on a CA
tlsConfig.RootCAs = x509.NewCertPool()
if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(clientCACertificatePem)) {
return errors.New("failed to append certs")
}
}
if clientCertificatePem != "" && clientPrivateKeyPem != "" {
// attach a client certificate to the TLS handshake (aka mTLS)
certificate, err := tls.X509KeyPair([]byte(clientCertificatePem), []byte(clientPrivateKeyPem))
if err != nil {
return fmt.Errorf("cannot load client certificate: %w", err)
}
tlsConfig.Certificates = []tls.Certificate{certificate}
}
return nil
}
func (b *Backend) configure(ctx context.Context) error {
data := schema.FromContextBackendConfig(ctx)
address := data.Get("address").(string)
updateURL, err := url.Parse(address)
if err != nil {
return fmt.Errorf("failed to parse address URL: %w", err)
}
if updateURL.Scheme != "http" && updateURL.Scheme != "https" {
return fmt.Errorf("address must be HTTP or HTTPS")
}
updateMethod := data.Get("update_method").(string)
var lockURL *url.URL
if v, ok := data.GetOk("lock_address"); ok && v.(string) != "" {
var err error
lockURL, err = url.Parse(v.(string))
if err != nil {
return fmt.Errorf("failed to parse lockAddress URL: %w", err)
}
if lockURL.Scheme != "http" && lockURL.Scheme != "https" {
return fmt.Errorf("lockAddress must be HTTP or HTTPS")
}
}
lockMethod := data.Get("lock_method").(string)
var unlockURL *url.URL
if v, ok := data.GetOk("unlock_address"); ok && v.(string) != "" {
var err error
unlockURL, err = url.Parse(v.(string))
if err != nil {
return fmt.Errorf("failed to parse unlockAddress URL: %w", err)
}
if unlockURL.Scheme != "http" && unlockURL.Scheme != "https" {
return fmt.Errorf("unlockAddress must be HTTP or HTTPS")
}
}
unlockMethod := data.Get("unlock_method").(string)
rClient := retryablehttp.NewClient()
rClient.RetryMax = data.Get("retry_max").(int)
rClient.RetryWaitMin = time.Duration(data.Get("retry_wait_min").(int)) * time.Second
rClient.RetryWaitMax = time.Duration(data.Get("retry_wait_max").(int)) * time.Second
2021-11-29 17:45:35 -06:00
rClient.Logger = log.New(logging.LogOutput(), "", log.Flags())
[fixes 31700] Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. (#31699) * Add mTLS support for http backend by way of client cert & key, as well as enterprise cacert. * Fix style. * Skip cert validation to be sure error is related to missing client cert; not untrusted server cert. * Remove misplaced err check. * Fix the size of test using http backend. * Just for correctness, include all certs in the pem encoded cert - sometimes certs come with a chain of their signers. * Adjusted names as recommended in PR comments. * Adjusted names to be full-length and more descriptive. * Added full-fledged testing with mTLS http server * Fix goimports. * Fix the names of the backend config. * Exclusive lock for write and delete. * Revert "Fix goimports." This reverts commit 7d40f6099fbbb675fb2e25e35ee40aeafe3d0a22. * goimports just for server test. * Added the go:generation for the mock. * Move the TLS configuration out to make it more readable - don't replace the HTTPClient as the retryablehttp already creates one - just configure its TLS. * Just switch the client/data params - felt more natural this way. * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/testdata/gencerts.sh Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * Update internal/backend/remote-state/http/backend.go Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com> * the location of the file name is not sensitive. * Added error if only one of client_certificate_pem and client_private_key_pem are set. * Remove testify from test cases; use t.Error* for assert and t.Fatal* for require. * Fixed import consistency * Just use default openssl. * Since file(...) is so trivial to use, changed the client cert, key, and ca cert to be the data. See also https://github.com/hashicorp/terraform-provider-http/pull/211 Co-authored-by: Sheridan C Rawlins <scr@ouryahoo.com> Co-authored-by: kmoe <5575356+kmoe@users.noreply.github.com>
2023-01-26 08:08:07 -06:00
if err = b.configureTLS(rClient, data); err != nil {
return err
}
b.client = &httpClient{
URL: updateURL,
UpdateMethod: updateMethod,
LockURL: lockURL,
LockMethod: lockMethod,
UnlockURL: unlockURL,
UnlockMethod: unlockMethod,
Username: data.Get("username").(string),
Password: data.Get("password").(string),
// accessible only for testing use
Client: rClient,
}
return nil
}
func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
if name != backend.DefaultStateName {
return nil, backend.ErrWorkspacesNotSupported
}
return &remote.State{Client: b.client}, nil
}
func (b *Backend) Workspaces() ([]string, error) {
return nil, backend.ErrWorkspacesNotSupported
}
func (b *Backend) DeleteWorkspace(string, bool) error {
return backend.ErrWorkspacesNotSupported
}