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  )