github.com/pulumi/terraform@v1.4.0/pkg/command/cliconfig/credentials.go (about) 1 package cliconfig 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io/ioutil" 8 "log" 9 "os" 10 "path/filepath" 11 "strings" 12 13 "github.com/zclconf/go-cty/cty" 14 ctyjson "github.com/zclconf/go-cty/cty/json" 15 16 svchost "github.com/hashicorp/terraform-svchost" 17 svcauth "github.com/hashicorp/terraform-svchost/auth" 18 "github.com/pulumi/terraform/pkg/configs/hcl2shim" 19 pluginDiscovery "github.com/pulumi/terraform/pkg/plugin/discovery" 20 "github.com/pulumi/terraform/pkg/replacefile" 21 ) 22 23 // credentialsConfigFile returns the path for the special configuration file 24 // that the credentials source will use when asked to save or forget credentials 25 // and when a "credentials helper" program is not active. 26 func credentialsConfigFile() (string, error) { 27 configDir, err := ConfigDir() 28 if err != nil { 29 return "", err 30 } 31 return filepath.Join(configDir, "credentials.tfrc.json"), nil 32 } 33 34 // CredentialsSource creates and returns a service credentials source whose 35 // behavior depends on which "credentials" and "credentials_helper" blocks, 36 // if any, are present in the receiving config. 37 func (c *Config) CredentialsSource(helperPlugins pluginDiscovery.PluginMetaSet) (*CredentialsSource, error) { 38 credentialsFilePath, err := credentialsConfigFile() 39 if err != nil { 40 // If we managed to load a Config object at all then we would already 41 // have located this file, so this error is very unlikely. 42 return nil, fmt.Errorf("can't locate credentials file: %s", err) 43 } 44 45 var helper svcauth.CredentialsSource 46 var helperType string 47 for givenType, givenConfig := range c.CredentialsHelpers { 48 available := helperPlugins.WithName(givenType) 49 if available.Count() == 0 { 50 log.Printf("[ERROR] Unable to find credentials helper %q; ignoring", givenType) 51 break 52 } 53 54 selected := available.Newest() 55 56 helperSource := svcauth.HelperProgramCredentialsSource(selected.Path, givenConfig.Args...) 57 helper = svcauth.CachingCredentialsSource(helperSource) // cached because external operation may be slow/expensive 58 helperType = givenType 59 60 // There should only be zero or one "credentials_helper" blocks. We 61 // assume that the config was validated earlier and so we don't check 62 // for extras here. 63 break 64 } 65 66 return c.credentialsSource(helperType, helper, credentialsFilePath), nil 67 } 68 69 // EmptyCredentialsSourceForTests constructs a CredentialsSource with 70 // no credentials pre-loaded and which writes new credentials to a file 71 // at the given path. 72 // 73 // As the name suggests, this function is here only for testing and should not 74 // be used in normal application code. 75 func EmptyCredentialsSourceForTests(credentialsFilePath string) *CredentialsSource { 76 cfg := &Config{} 77 return cfg.credentialsSource("", nil, credentialsFilePath) 78 } 79 80 // credentialsSource is an internal factory for the credentials source which 81 // allows overriding the credentials file path, which allows setting it to 82 // a temporary file location when testing. 83 func (c *Config) credentialsSource(helperType string, helper svcauth.CredentialsSource, credentialsFilePath string) *CredentialsSource { 84 configured := map[svchost.Hostname]cty.Value{} 85 for userHost, creds := range c.Credentials { 86 host, err := svchost.ForComparison(userHost) 87 if err != nil { 88 // We expect the config was already validated by the time we get 89 // here, so we'll just ignore invalid hostnames. 90 continue 91 } 92 93 // For now our CLI config continues to use HCL 1.0, so we'll shim it 94 // over to HCL 2.0 types. In future we will hopefully migrate it to 95 // HCL 2.0 instead, and so it'll be a cty.Value already. 96 credsV := hcl2shim.HCL2ValueFromConfigValue(creds) 97 configured[host] = credsV 98 } 99 100 writableLocal := readHostsInCredentialsFile(credentialsFilePath) 101 unwritableLocal := map[svchost.Hostname]cty.Value{} 102 for host, v := range configured { 103 if _, exists := writableLocal[host]; !exists { 104 unwritableLocal[host] = v 105 } 106 } 107 108 return &CredentialsSource{ 109 configured: configured, 110 unwritable: unwritableLocal, 111 credentialsFilePath: credentialsFilePath, 112 helper: helper, 113 helperType: helperType, 114 } 115 } 116 117 func collectCredentialsFromEnv() map[svchost.Hostname]string { 118 const prefix = "TF_TOKEN_" 119 120 ret := make(map[svchost.Hostname]string) 121 for _, ev := range os.Environ() { 122 eqIdx := strings.Index(ev, "=") 123 if eqIdx < 0 { 124 continue 125 } 126 name := ev[:eqIdx] 127 value := ev[eqIdx+1:] 128 if !strings.HasPrefix(name, prefix) { 129 continue 130 } 131 rawHost := name[len(prefix):] 132 133 // We accept double underscores in place of hyphens because hyphens are not valid 134 // identifiers in most shells and are therefore hard to set. 135 // This is unambiguous with replacing single underscores below because 136 // hyphens are not allowed at the beginning or end of a label and therefore 137 // odd numbers of underscores will not appear together in a valid variable name. 138 rawHost = strings.ReplaceAll(rawHost, "__", "-") 139 140 // We accept underscores in place of dots because dots are not valid 141 // identifiers in most shells and are therefore hard to set. 142 // Underscores are not valid in hostnames, so this is unambiguous for 143 // valid hostnames. 144 rawHost = strings.ReplaceAll(rawHost, "_", ".") 145 146 // Because environment variables are often set indirectly by OS 147 // libraries that might interfere with how they are encoded, we'll 148 // be tolerant of them being given either directly as UTF-8 IDNs 149 // or in Punycode form, normalizing to Punycode form here because 150 // that is what the Terraform credentials helper protocol will 151 // use in its requests. 152 // 153 // Using ForDisplay first here makes this more liberal than Terraform 154 // itself would usually be in that it will tolerate pre-punycoded 155 // hostnames that Terraform normally rejects in other contexts in order 156 // to ensure stored hostnames are human-readable. 157 dispHost := svchost.ForDisplay(rawHost) 158 hostname, err := svchost.ForComparison(dispHost) 159 if err != nil { 160 // Ignore invalid hostnames 161 continue 162 } 163 164 ret[hostname] = value 165 } 166 167 return ret 168 } 169 170 // hostCredentialsFromEnv returns a token credential by searching for a hostname-specific 171 // environment variable. The host parameter is expected to be in the "comparison" form, 172 // for example, hostnames containing non-ASCII characters like "café.fr" 173 // should be expressed as "xn--caf-dma.fr". If the variable based on the hostname is not 174 // defined, nil is returned. 175 // 176 // Hyphen and period characters are allowed in environment variable names, but are not valid POSIX 177 // variable names. However, it's still possible to set variable names with these characters using 178 // utilities like env or docker. Variable names may have periods translated to underscores and 179 // hyphens translated to double underscores in the variable name. 180 // For the example "café.fr", you may use the variable names "TF_TOKEN_xn____caf__dma_fr", 181 // "TF_TOKEN_xn--caf-dma_fr", or "TF_TOKEN_xn--caf-dma.fr" 182 func hostCredentialsFromEnv(host svchost.Hostname) svcauth.HostCredentials { 183 token, ok := collectCredentialsFromEnv()[host] 184 if !ok { 185 return nil 186 } 187 return svcauth.HostCredentialsToken(token) 188 } 189 190 // CredentialsSource is an implementation of svcauth.CredentialsSource 191 // that can read and write the CLI configuration, and possibly also delegate 192 // to a credentials helper when configured. 193 type CredentialsSource struct { 194 // configured describes the credentials explicitly configured in the CLI 195 // config via "credentials" blocks. This map will also change to reflect 196 // any writes to the special credentials.tfrc.json file. 197 configured map[svchost.Hostname]cty.Value 198 199 // unwritable describes any credentials explicitly configured in the 200 // CLI config in any file other than credentials.tfrc.json. We cannot update 201 // these automatically because only credentials.tfrc.json is subject to 202 // editing by this credentials source. 203 unwritable map[svchost.Hostname]cty.Value 204 205 // credentialsFilePath is the full path to the credentials.tfrc.json file 206 // that we'll update if any changes to credentials are requested and if 207 // a credentials helper isn't available to use instead. 208 // 209 // (This is a field here rather than just calling credentialsConfigFile 210 // directly just so that we can use temporary file location instead during 211 // testing.) 212 credentialsFilePath string 213 214 // helper is the credentials source representing the configured credentials 215 // helper, if any. When this is non-nil, it will be consulted for any 216 // hostnames not explicitly represented in "configured". Any writes to 217 // the credentials store will also be sent to a configured helper instead 218 // of the credentials.tfrc.json file. 219 helper svcauth.CredentialsSource 220 221 // helperType is the name of the type of credentials helper that is 222 // referenced in "helper", or the empty string if "helper" is nil. 223 helperType string 224 } 225 226 // Assertion that credentialsSource implements CredentialsSource 227 var _ svcauth.CredentialsSource = (*CredentialsSource)(nil) 228 229 func (s *CredentialsSource) ForHost(host svchost.Hostname) (svcauth.HostCredentials, error) { 230 // The first order of precedence for credentials is a host-specific environment variable 231 if envCreds := hostCredentialsFromEnv(host); envCreds != nil { 232 return envCreds, nil 233 } 234 235 // Then, any credentials block present in the CLI config 236 v, ok := s.configured[host] 237 if ok { 238 return svcauth.HostCredentialsFromObject(v), nil 239 } 240 241 // And finally, the credentials helper 242 if s.helper != nil { 243 return s.helper.ForHost(host) 244 } 245 246 return nil, nil 247 } 248 249 func (s *CredentialsSource) StoreForHost(host svchost.Hostname, credentials svcauth.HostCredentialsWritable) error { 250 return s.updateHostCredentials(host, credentials) 251 } 252 253 func (s *CredentialsSource) ForgetForHost(host svchost.Hostname) error { 254 return s.updateHostCredentials(host, nil) 255 } 256 257 // HostCredentialsLocation returns a value indicating what type of storage is 258 // currently used for the credentials for the given hostname. 259 // 260 // The current location of credentials determines whether updates are possible 261 // at all and, if they are, where any updates will be written. 262 func (s *CredentialsSource) HostCredentialsLocation(host svchost.Hostname) CredentialsLocation { 263 if _, unwritable := s.unwritable[host]; unwritable { 264 return CredentialsInOtherFile 265 } 266 if _, exists := s.configured[host]; exists { 267 return CredentialsInPrimaryFile 268 } 269 if s.helper != nil { 270 return CredentialsViaHelper 271 } 272 return CredentialsNotAvailable 273 } 274 275 // CredentialsFilePath returns the full path to the local credentials 276 // configuration file, so that a caller can mention this path in order to 277 // be transparent about where credentials will be stored. 278 // 279 // This file will be used for writes only if HostCredentialsLocation for the 280 // relevant host returns CredentialsInPrimaryFile or CredentialsNotAvailable. 281 // 282 // The credentials file path is found relative to the current user's home 283 // directory, so this function will return an error in the unlikely event that 284 // we cannot determine a suitable home directory to resolve relative to. 285 func (s *CredentialsSource) CredentialsFilePath() (string, error) { 286 return s.credentialsFilePath, nil 287 } 288 289 // CredentialsHelperType returns the name of the configured credentials helper 290 // type, or an empty string if no credentials helper is configured. 291 func (s *CredentialsSource) CredentialsHelperType() string { 292 return s.helperType 293 } 294 295 func (s *CredentialsSource) updateHostCredentials(host svchost.Hostname, new svcauth.HostCredentialsWritable) error { 296 switch loc := s.HostCredentialsLocation(host); loc { 297 case CredentialsInOtherFile: 298 return ErrUnwritableHostCredentials(host) 299 case CredentialsInPrimaryFile, CredentialsNotAvailable: 300 // If the host already has credentials stored locally then we'll update 301 // them locally too, even if there's a credentials helper configured, 302 // because the user might be intentionally retaining this particular 303 // host locally for some reason, e.g. if the credentials helper is 304 // talking to some shared remote service like HashiCorp Vault. 305 return s.updateLocalHostCredentials(host, new) 306 case CredentialsViaHelper: 307 // Delegate entirely to the helper, then. 308 if new == nil { 309 return s.helper.ForgetForHost(host) 310 } 311 return s.helper.StoreForHost(host, new) 312 default: 313 // Should never happen because the above cases are exhaustive 314 return fmt.Errorf("invalid credentials location %#v", loc) 315 } 316 } 317 318 func (s *CredentialsSource) updateLocalHostCredentials(host svchost.Hostname, new svcauth.HostCredentialsWritable) error { 319 // This function updates the local credentials file in particular, 320 // regardless of whether a credentials helper is active. It should be 321 // called only indirectly via updateHostCredentials. 322 323 filename, err := s.CredentialsFilePath() 324 if err != nil { 325 return fmt.Errorf("unable to determine credentials file path: %s", err) 326 } 327 328 oldSrc, err := ioutil.ReadFile(filename) 329 if err != nil && !os.IsNotExist(err) { 330 return fmt.Errorf("cannot read %s: %s", filename, err) 331 } 332 333 var raw map[string]interface{} 334 335 if len(oldSrc) > 0 { 336 // When decoding we use a custom decoder so we can decode any numbers as 337 // json.Number and thus avoid losing any accuracy in our round-trip. 338 dec := json.NewDecoder(bytes.NewReader(oldSrc)) 339 dec.UseNumber() 340 err = dec.Decode(&raw) 341 if err != nil { 342 return fmt.Errorf("cannot read %s: %s", filename, err) 343 } 344 } else { 345 raw = make(map[string]interface{}) 346 } 347 348 rawCredsI, ok := raw["credentials"] 349 if !ok { 350 rawCredsI = make(map[string]interface{}) 351 raw["credentials"] = rawCredsI 352 } 353 rawCredsMap, ok := rawCredsI.(map[string]interface{}) 354 if !ok { 355 return fmt.Errorf("credentials file %s has invalid value for \"credentials\" property: must be a JSON object", filename) 356 } 357 358 // We use display-oriented hostnames in our file to mimick how a human user 359 // would write it, so we need to search for and remove any key that 360 // normalizes to our target hostname so we won't generate something invalid 361 // when the existing entry is slightly different. 362 for givenHost := range rawCredsMap { 363 canonHost, err := svchost.ForComparison(givenHost) 364 if err == nil && canonHost == host { 365 delete(rawCredsMap, givenHost) 366 } 367 } 368 369 // If we have a new object to store we'll write it in now. If the previous 370 // object had the hostname written in a different way then this will 371 // appear to change it into our canonical display form, with all the 372 // letters in lowercase and other transforms from the Internationalized 373 // Domain Names specification. 374 if new != nil { 375 toStore := new.ToStore() 376 rawCredsMap[host.ForDisplay()] = ctyjson.SimpleJSONValue{ 377 Value: toStore, 378 } 379 } 380 381 newSrc, err := json.MarshalIndent(raw, "", " ") 382 if err != nil { 383 return fmt.Errorf("cannot serialize updated credentials file: %s", err) 384 } 385 386 // Now we'll write our new content over the top of the existing file. 387 // Because we updated the data structure surgically here we should not 388 // have disturbed the meaning of any other content in the file, but it 389 // might have a different JSON layout than before. 390 // We'll create a new file with a different name first and then rename 391 // it over the old file in order to make the change as atomically as 392 // the underlying OS/filesystem will allow. 393 { 394 dir, file := filepath.Split(filename) 395 f, err := ioutil.TempFile(dir, file) 396 if err != nil { 397 return fmt.Errorf("cannot create temporary file to update credentials: %s", err) 398 } 399 tmpName := f.Name() 400 moved := false 401 defer func(f *os.File, name string) { 402 // Remove the temporary file if it hasn't been moved yet. We're 403 // ignoring errors here because there's nothing we can do about 404 // them anyway. 405 if !moved { 406 os.Remove(name) 407 } 408 }(f, tmpName) 409 410 // Write the credentials to the temporary file, then immediately close 411 // it, whether or not the write succeeds. 412 _, err = f.Write(newSrc) 413 f.Close() 414 if err != nil { 415 return fmt.Errorf("cannot write to temporary file %s: %s", tmpName, err) 416 } 417 418 // Temporary file now replaces the original file, as atomically as 419 // possible. (At the very least, we should not end up with a file 420 // containing only a partial JSON object.) 421 err = replacefile.AtomicRename(tmpName, filename) 422 if err != nil { 423 return fmt.Errorf("failed to replace %s with temporary file %s: %s", filename, tmpName, err) 424 } 425 426 // Credentials file should be readable only by its owner. (This may 427 // not be effective on all platforms, but should at least work on 428 // Unix-like targets and should be harmless elsewhere.) 429 if err := os.Chmod(filename, 0600); err != nil { 430 return fmt.Errorf("cannot set mode for credentials file %s: %s", filename, err) 431 } 432 433 moved = true 434 } 435 436 if new != nil { 437 s.configured[host] = new.ToStore() 438 } else { 439 delete(s.configured, host) 440 } 441 442 return nil 443 } 444 445 // readHostsInCredentialsFile discovers which hosts have credentials configured 446 // in the credentials file specifically, as opposed to in any other CLI 447 // config file. 448 // 449 // If the credentials file isn't present or is unreadable for any reason then 450 // this returns an empty set, reflecting that effectively no credentials are 451 // stored there. 452 func readHostsInCredentialsFile(filename string) map[svchost.Hostname]struct{} { 453 src, err := ioutil.ReadFile(filename) 454 if err != nil { 455 return nil 456 } 457 458 var raw map[string]interface{} 459 err = json.Unmarshal(src, &raw) 460 if err != nil { 461 return nil 462 } 463 464 rawCredsI, ok := raw["credentials"] 465 if !ok { 466 return nil 467 } 468 rawCredsMap, ok := rawCredsI.(map[string]interface{}) 469 if !ok { 470 return nil 471 } 472 473 ret := make(map[svchost.Hostname]struct{}) 474 for givenHost := range rawCredsMap { 475 host, err := svchost.ForComparison(givenHost) 476 if err != nil { 477 // We expect the config was already validated by the time we get 478 // here, so we'll just ignore invalid hostnames. 479 continue 480 } 481 ret[host] = struct{}{} 482 } 483 return ret 484 } 485 486 // ErrUnwritableHostCredentials is an error type that is returned when a caller 487 // tries to write credentials for a host that has existing credentials configured 488 // in a file that we cannot automatically update. 489 type ErrUnwritableHostCredentials svchost.Hostname 490 491 func (err ErrUnwritableHostCredentials) Error() string { 492 return fmt.Sprintf("cannot change credentials for %s: existing manually-configured credentials in a CLI config file", svchost.Hostname(err).ForDisplay()) 493 } 494 495 // Hostname returns the host that could not be written. 496 func (err ErrUnwritableHostCredentials) Hostname() svchost.Hostname { 497 return svchost.Hostname(err) 498 } 499 500 // CredentialsLocation describes a type of storage used for the credentials 501 // for a particular hostname. 502 type CredentialsLocation rune 503 504 const ( 505 // CredentialsNotAvailable means that we know that there are no credential 506 // available for the host. 507 // 508 // Note that CredentialsViaHelper might also lead to no credentials being 509 // available, depending on how the helper answers when we request credentials 510 // from it. 511 CredentialsNotAvailable CredentialsLocation = 0 512 513 // CredentialsInPrimaryFile means that there is already a credentials object 514 // for the host in the credentials.tfrc.json file. 515 CredentialsInPrimaryFile CredentialsLocation = 'P' 516 517 // CredentialsInOtherFile means that there is already a credentials object 518 // for the host in a CLI config file other than credentials.tfrc.json. 519 CredentialsInOtherFile CredentialsLocation = 'O' 520 521 // CredentialsViaHelper indicates that no statically-configured credentials 522 // are available for the host but a helper program is available that may 523 // or may not have credentials for the host. 524 CredentialsViaHelper CredentialsLocation = 'H' 525 )