github.com/opentofu/opentofu@v1.7.1/internal/encryption/keyprovider/gcp_kms/config.go (about)

     1  package gcp_kms
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"os"
     7  
     8  	"github.com/mitchellh/go-homedir"
     9  	"github.com/opentofu/opentofu/internal/encryption/keyprovider"
    10  	"github.com/opentofu/opentofu/internal/httpclient"
    11  	"github.com/opentofu/opentofu/version"
    12  	"golang.org/x/oauth2"
    13  	"google.golang.org/api/impersonate"
    14  	"google.golang.org/api/option"
    15  
    16  	kms "cloud.google.com/go/kms/apiv1"
    17  )
    18  
    19  type keyManagementClientInit func(ctx context.Context, opts ...option.ClientOption) (keyManagementClient, error)
    20  
    21  // Can be overridden for test mocking
    22  var newKeyManagementClient keyManagementClientInit = func(ctx context.Context, opts ...option.ClientOption) (keyManagementClient, error) {
    23  	return kms.NewKeyManagementClient(ctx, opts...)
    24  }
    25  
    26  type Config struct {
    27  	Credentials string `hcl:"credentials,optional"`
    28  	AccessToken string `hcl:"access_token,optional"`
    29  
    30  	ImpersonateServiceAccount          string   `hcl:"impersonate_service_account,optional"`
    31  	ImpersonateServiceAccountDelegates []string `hcl:"impersonate_service_account_delegates,optional"`
    32  
    33  	KMSKeyName string `hcl:"kms_encryption_key"`
    34  	KeyLength  int    `hcl:"key_length"`
    35  }
    36  
    37  func stringAttrEnvFallback(val string, env string) string {
    38  	if val != "" {
    39  		return val
    40  	}
    41  	return os.Getenv(env)
    42  }
    43  
    44  // TODO This is copied in from the backend packge to prevent a circular dependency loop
    45  // If the argument is a path, ReadPathOrContents loads it and returns the contents,
    46  // otherwise the argument is assumed to be the desired contents and is simply
    47  // returned.
    48  func ReadPathOrContents(poc string) (string, error) {
    49  	if len(poc) == 0 {
    50  		return poc, nil
    51  	}
    52  
    53  	path := poc
    54  	if path[0] == '~' {
    55  		var err error
    56  		path, err = homedir.Expand(path)
    57  		if err != nil {
    58  			return path, err
    59  		}
    60  	}
    61  
    62  	if _, err := os.Stat(path); err == nil {
    63  		contents, err := os.ReadFile(path)
    64  		if err != nil {
    65  			return string(contents), err
    66  		}
    67  		return string(contents), nil
    68  	}
    69  
    70  	return poc, nil
    71  }
    72  
    73  func (c Config) Build() (keyprovider.KeyProvider, keyprovider.KeyMeta, error) {
    74  	// This mirrors the gcp remote state backend
    75  
    76  	// Apply env defaults if nessesary
    77  	c.Credentials = stringAttrEnvFallback(c.Credentials, "GOOGLE_CREDENTIALS")
    78  	c.AccessToken = stringAttrEnvFallback(c.AccessToken, "GOOGLE_OAUTH_ACCESS_TOKEN")
    79  	c.ImpersonateServiceAccount = stringAttrEnvFallback(c.ImpersonateServiceAccount, "GOOGLE_BACKEND_IMPERSONATE_SERVICE_ACCOUNT")
    80  	c.ImpersonateServiceAccount = stringAttrEnvFallback(c.ImpersonateServiceAccount, "GOOGLE_IMPERSONATE_SERVICE_ACCOUNT")
    81  
    82  	ctx := context.Background()
    83  
    84  	var opts []option.ClientOption
    85  	var credOptions []option.ClientOption
    86  
    87  	if c.AccessToken != "" {
    88  		tokenSource := oauth2.StaticTokenSource(&oauth2.Token{
    89  			AccessToken: c.AccessToken,
    90  		})
    91  		credOptions = append(credOptions, option.WithTokenSource(tokenSource))
    92  	} else if c.Credentials != "" {
    93  		// to mirror how the provider works, we accept the file path or the contents
    94  		contents, err := ReadPathOrContents(c.Credentials)
    95  		if err != nil {
    96  			return nil, nil, &keyprovider.ErrInvalidConfiguration{Message: "Error loading credentials", Cause: err}
    97  		}
    98  
    99  		if !json.Valid([]byte(contents)) {
   100  			return nil, nil, &keyprovider.ErrInvalidConfiguration{Message: "the string provided in credentials is neither valid json nor a valid file path"}
   101  		}
   102  
   103  		credOptions = append(credOptions, option.WithCredentialsJSON([]byte(contents)))
   104  	}
   105  
   106  	// Service Account Impersonation
   107  	if c.ImpersonateServiceAccount != "" {
   108  		ts, err := impersonate.CredentialsTokenSource(ctx, impersonate.CredentialsConfig{
   109  			TargetPrincipal: c.ImpersonateServiceAccount,
   110  			Scopes:          []string{"https://www.googleapis.com/auth/cloudkms"}, // I can't find a smaller scope than this...
   111  			Delegates:       c.ImpersonateServiceAccountDelegates,
   112  		}, credOptions...)
   113  
   114  		if err != nil {
   115  			return nil, nil, &keyprovider.ErrInvalidConfiguration{Cause: err}
   116  		}
   117  
   118  		opts = append(opts, option.WithTokenSource(ts))
   119  
   120  	} else {
   121  		opts = append(opts, credOptions...)
   122  	}
   123  
   124  	opts = append(opts, option.WithUserAgent(httpclient.OpenTofuUserAgent(version.Version)))
   125  
   126  	svc, err := newKeyManagementClient(ctx, opts...)
   127  	if err != nil {
   128  		return nil, nil, &keyprovider.ErrInvalidConfiguration{Cause: err}
   129  	}
   130  
   131  	if c.KMSKeyName == "" {
   132  		return nil, nil, &keyprovider.ErrInvalidConfiguration{Message: "kms_key_name must be provided"}
   133  	}
   134  
   135  	if c.KeyLength < 1 {
   136  		return nil, nil, &keyprovider.ErrInvalidConfiguration{Message: "key_length must be at least 1"}
   137  	}
   138  	if c.KeyLength > 1024 {
   139  		return nil, nil, &keyprovider.ErrInvalidConfiguration{Message: "key_length must be less than the GCP limit of 1024"}
   140  	}
   141  
   142  	return &keyProvider{
   143  		svc:       svc,
   144  		ctx:       ctx,
   145  		keyName:   c.KMSKeyName,
   146  		keyLength: c.KeyLength,
   147  	}, new(keyMeta), nil
   148  }