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  )