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 }