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