github.com/opentofu/opentofu@v1.7.1/internal/backend/remote-state/http/backend.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package http
     7  
     8  import (
     9  	"context"
    10  	"crypto/tls"
    11  	"crypto/x509"
    12  	"errors"
    13  	"fmt"
    14  	"log"
    15  	"net/http"
    16  	"net/url"
    17  	"regexp"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/hashicorp/go-retryablehttp"
    22  
    23  	"github.com/opentofu/opentofu/internal/backend"
    24  	"github.com/opentofu/opentofu/internal/encryption"
    25  	"github.com/opentofu/opentofu/internal/legacy/helper/schema"
    26  	"github.com/opentofu/opentofu/internal/logging"
    27  	"github.com/opentofu/opentofu/internal/states/remote"
    28  	"github.com/opentofu/opentofu/internal/states/statemgr"
    29  )
    30  
    31  func New(enc encryption.StateEncryption) backend.Backend {
    32  	s := &schema.Backend{
    33  		Schema: map[string]*schema.Schema{
    34  			"address": &schema.Schema{
    35  				Type:        schema.TypeString,
    36  				Required:    true,
    37  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_ADDRESS", nil),
    38  				Description: "The address of the REST endpoint",
    39  			},
    40  			"update_method": &schema.Schema{
    41  				Type:        schema.TypeString,
    42  				Optional:    true,
    43  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UPDATE_METHOD", "POST"),
    44  				Description: "HTTP method to use when updating state",
    45  			},
    46  			"lock_address": &schema.Schema{
    47  				Type:        schema.TypeString,
    48  				Optional:    true,
    49  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_ADDRESS", nil),
    50  				Description: "The address of the lock REST endpoint",
    51  			},
    52  			"unlock_address": &schema.Schema{
    53  				Type:        schema.TypeString,
    54  				Optional:    true,
    55  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_ADDRESS", nil),
    56  				Description: "The address of the unlock REST endpoint",
    57  			},
    58  			"lock_method": &schema.Schema{
    59  				Type:        schema.TypeString,
    60  				Optional:    true,
    61  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_LOCK_METHOD", "LOCK"),
    62  				Description: "The HTTP method to use when locking",
    63  			},
    64  			"unlock_method": &schema.Schema{
    65  				Type:        schema.TypeString,
    66  				Optional:    true,
    67  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_UNLOCK_METHOD", "UNLOCK"),
    68  				Description: "The HTTP method to use when unlocking",
    69  			},
    70  			"username": &schema.Schema{
    71  				Type:        schema.TypeString,
    72  				Optional:    true,
    73  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_USERNAME", nil),
    74  				Description: "The username for HTTP basic authentication",
    75  			},
    76  			"password": &schema.Schema{
    77  				Type:        schema.TypeString,
    78  				Optional:    true,
    79  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_PASSWORD", nil),
    80  				Description: "The password for HTTP basic authentication",
    81  			},
    82  			"skip_cert_verification": &schema.Schema{
    83  				Type:        schema.TypeBool,
    84  				Optional:    true,
    85  				Default:     false,
    86  				Description: "Whether to skip TLS verification.",
    87  			},
    88  			"retry_max": &schema.Schema{
    89  				Type:        schema.TypeInt,
    90  				Optional:    true,
    91  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_MAX", 2),
    92  				Description: "The number of HTTP request retries.",
    93  			},
    94  			"retry_wait_min": &schema.Schema{
    95  				Type:        schema.TypeInt,
    96  				Optional:    true,
    97  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MIN", 1),
    98  				Description: "The minimum time in seconds to wait between HTTP request attempts.",
    99  			},
   100  			"retry_wait_max": &schema.Schema{
   101  				Type:        schema.TypeInt,
   102  				Optional:    true,
   103  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_RETRY_WAIT_MAX", 30),
   104  				Description: "The maximum time in seconds to wait between HTTP request attempts.",
   105  			},
   106  			"client_ca_certificate_pem": &schema.Schema{
   107  				Type:        schema.TypeString,
   108  				Optional:    true,
   109  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CA_CERTIFICATE_PEM", ""),
   110  				Description: "A PEM-encoded CA certificate chain used by the client to verify server certificates during TLS authentication.",
   111  			},
   112  			"client_certificate_pem": &schema.Schema{
   113  				Type:        schema.TypeString,
   114  				Optional:    true,
   115  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_CERTIFICATE_PEM", ""),
   116  				Description: "A PEM-encoded certificate used by the server to verify the client during mutual TLS (mTLS) authentication.",
   117  			},
   118  			"client_private_key_pem": &schema.Schema{
   119  				Type:        schema.TypeString,
   120  				Optional:    true,
   121  				DefaultFunc: schema.EnvDefaultFunc("TF_HTTP_CLIENT_PRIVATE_KEY_PEM", ""),
   122  				Description: "A PEM-encoded private key, required if client_certificate_pem is specified.",
   123  			},
   124  			"headers": &schema.Schema{
   125  				Type:     schema.TypeMap,
   126  				Elem:     &schema.Schema{Type: schema.TypeString},
   127  				Optional: true,
   128  				ValidateFunc: func(cv interface{}, ck string) ([]string, []error) {
   129  					nameRegex := regexp.MustCompile("[^a-zA-Z0-9-_]")
   130  					valueRegex := regexp.MustCompile("[^[:ascii:]]")
   131  
   132  					headers := cv.(map[string]interface{})
   133  					err := make([]error, 0, len(headers))
   134  					for name, value := range headers {
   135  						if len(name) == 0 || nameRegex.MatchString(name) {
   136  							err = append(err, fmt.Errorf(
   137  								"%s \"%s\" name must not be empty and only contain A-Za-z0-9-_ characters", ck, name))
   138  						}
   139  
   140  						v := value.(string)
   141  						if len(strings.TrimSpace(v)) == 0 || valueRegex.MatchString(v) {
   142  							err = append(err, fmt.Errorf(
   143  								"%s \"%s\" value must not be empty and only contain ascii characters", ck, name))
   144  						}
   145  					}
   146  					return nil, err
   147  				},
   148  				Description: "A map of headers, when set will be included with HTTP requests sent to the HTTP backend",
   149  			},
   150  		},
   151  	}
   152  
   153  	b := &Backend{Backend: s, encryption: enc}
   154  	b.Backend.ConfigureFunc = b.configure
   155  	return b
   156  }
   157  
   158  type Backend struct {
   159  	*schema.Backend
   160  	encryption encryption.StateEncryption
   161  
   162  	client *httpClient
   163  }
   164  
   165  // configureTLS configures TLS when needed; if there are no conditions requiring TLS, no change is made.
   166  func (b *Backend) configureTLS(client *retryablehttp.Client, data *schema.ResourceData) error {
   167  	// If there are no conditions needing to configure TLS, leave the client untouched
   168  	skipCertVerification := data.Get("skip_cert_verification").(bool)
   169  	clientCACertificatePem := data.Get("client_ca_certificate_pem").(string)
   170  	clientCertificatePem := data.Get("client_certificate_pem").(string)
   171  	clientPrivateKeyPem := data.Get("client_private_key_pem").(string)
   172  	if !skipCertVerification && clientCACertificatePem == "" && clientCertificatePem == "" && clientPrivateKeyPem == "" {
   173  		return nil
   174  	}
   175  	if clientCertificatePem != "" && clientPrivateKeyPem == "" {
   176  		return fmt.Errorf("client_certificate_pem is set but client_private_key_pem is not")
   177  	}
   178  	if clientPrivateKeyPem != "" && clientCertificatePem == "" {
   179  		return fmt.Errorf("client_private_key_pem is set but client_certificate_pem is not")
   180  	}
   181  
   182  	// TLS configuration is needed; create an object and configure it
   183  	var tlsConfig tls.Config
   184  	client.HTTPClient.Transport.(*http.Transport).TLSClientConfig = &tlsConfig
   185  
   186  	if skipCertVerification {
   187  		// ignores TLS verification
   188  		tlsConfig.InsecureSkipVerify = true
   189  	}
   190  	if clientCACertificatePem != "" {
   191  		// trust servers based on a CA
   192  		tlsConfig.RootCAs = x509.NewCertPool()
   193  		if !tlsConfig.RootCAs.AppendCertsFromPEM([]byte(clientCACertificatePem)) {
   194  			return errors.New("failed to append certs")
   195  		}
   196  	}
   197  	if clientCertificatePem != "" && clientPrivateKeyPem != "" {
   198  		// attach a client certificate to the TLS handshake (aka mTLS)
   199  		certificate, err := tls.X509KeyPair([]byte(clientCertificatePem), []byte(clientPrivateKeyPem))
   200  		if err != nil {
   201  			return fmt.Errorf("cannot load client certificate: %w", err)
   202  		}
   203  		tlsConfig.Certificates = []tls.Certificate{certificate}
   204  	}
   205  
   206  	return nil
   207  }
   208  
   209  func (b *Backend) configure(ctx context.Context) error {
   210  	data := schema.FromContextBackendConfig(ctx)
   211  
   212  	address := data.Get("address").(string)
   213  	updateURL, err := url.Parse(address)
   214  	if err != nil {
   215  		return fmt.Errorf("failed to parse address URL: %w", err)
   216  	}
   217  	if updateURL.Scheme != "http" && updateURL.Scheme != "https" {
   218  		return fmt.Errorf("address must be HTTP or HTTPS")
   219  	}
   220  
   221  	updateMethod := data.Get("update_method").(string)
   222  
   223  	var lockURL *url.URL
   224  	if v, ok := data.GetOk("lock_address"); ok && v.(string) != "" {
   225  		var err error
   226  		lockURL, err = url.Parse(v.(string))
   227  		if err != nil {
   228  			return fmt.Errorf("failed to parse lockAddress URL: %w", err)
   229  		}
   230  		if lockURL.Scheme != "http" && lockURL.Scheme != "https" {
   231  			return fmt.Errorf("lockAddress must be HTTP or HTTPS")
   232  		}
   233  	}
   234  
   235  	lockMethod := data.Get("lock_method").(string)
   236  
   237  	var unlockURL *url.URL
   238  	if v, ok := data.GetOk("unlock_address"); ok && v.(string) != "" {
   239  		var err error
   240  		unlockURL, err = url.Parse(v.(string))
   241  		if err != nil {
   242  			return fmt.Errorf("failed to parse unlockAddress URL: %w", err)
   243  		}
   244  		if unlockURL.Scheme != "http" && unlockURL.Scheme != "https" {
   245  			return fmt.Errorf("unlockAddress must be HTTP or HTTPS")
   246  		}
   247  	}
   248  
   249  	unlockMethod := data.Get("unlock_method").(string)
   250  
   251  	username := data.Get("username").(string)
   252  	password := data.Get("password").(string)
   253  
   254  	var headers map[string]string
   255  	if dv, ok := data.GetOk("headers"); ok {
   256  		dh := dv.(map[string]interface{})
   257  		headers = make(map[string]string, len(dh))
   258  
   259  		for k, v := range dh {
   260  			switch strings.ToLower(k) {
   261  			case "authorization":
   262  				if username != "" {
   263  					return fmt.Errorf("headers \"%s\" cannot be set when providing username", k)
   264  				}
   265  			case "content-type", "content-md5":
   266  				return fmt.Errorf("headers \"%s\" is reserved", k)
   267  			default:
   268  				headers[k] = v.(string)
   269  			}
   270  		}
   271  	}
   272  
   273  	rClient := retryablehttp.NewClient()
   274  	rClient.RetryMax = data.Get("retry_max").(int)
   275  	rClient.RetryWaitMin = time.Duration(data.Get("retry_wait_min").(int)) * time.Second
   276  	rClient.RetryWaitMax = time.Duration(data.Get("retry_wait_max").(int)) * time.Second
   277  	rClient.Logger = log.New(logging.LogOutput(), "", log.Flags())
   278  	if err = b.configureTLS(rClient, data); err != nil {
   279  		return err
   280  	}
   281  
   282  	b.client = &httpClient{
   283  		URL:          updateURL,
   284  		UpdateMethod: updateMethod,
   285  
   286  		LockURL:      lockURL,
   287  		LockMethod:   lockMethod,
   288  		UnlockURL:    unlockURL,
   289  		UnlockMethod: unlockMethod,
   290  
   291  		Headers:  headers,
   292  		Username: username,
   293  		Password: password,
   294  
   295  		// accessible only for testing use
   296  		Client: rClient,
   297  	}
   298  	return nil
   299  }
   300  
   301  func (b *Backend) StateMgr(name string) (statemgr.Full, error) {
   302  	if name != backend.DefaultStateName {
   303  		return nil, backend.ErrWorkspacesNotSupported
   304  	}
   305  
   306  	return remote.NewState(b.client, b.encryption), nil
   307  }
   308  
   309  func (b *Backend) Workspaces() ([]string, error) {
   310  	return nil, backend.ErrWorkspacesNotSupported
   311  }
   312  
   313  func (b *Backend) DeleteWorkspace(string, bool) error {
   314  	return backend.ErrWorkspacesNotSupported
   315  }