github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/command/cliconfig/credentials_test.go (about)

     1  package cliconfig
     2  
     3  import (
     4  	"net/http"
     5  	"os"
     6  	"path/filepath"
     7  	"testing"
     8  
     9  	"github.com/google/go-cmp/cmp"
    10  	"github.com/zclconf/go-cty/cty"
    11  
    12  	svchost "github.com/hashicorp/terraform-svchost"
    13  	svcauth "github.com/hashicorp/terraform-svchost/auth"
    14  )
    15  
    16  func TestCredentialsForHost(t *testing.T) {
    17  	credSrc := &CredentialsSource{
    18  		configured: map[svchost.Hostname]cty.Value{
    19  			"configured.example.com": cty.ObjectVal(map[string]cty.Value{
    20  				"token": cty.StringVal("configured"),
    21  			}),
    22  			"unused.example.com": cty.ObjectVal(map[string]cty.Value{
    23  				"token": cty.StringVal("incorrectly-configured"),
    24  			}),
    25  		},
    26  
    27  		// We'll use a static source to stand in for what would normally be
    28  		// a credentials helper program, since we're only testing the logic
    29  		// for choosing when to delegate to the helper here. The logic for
    30  		// interacting with a helper program is tested in the svcauth package.
    31  		helper: svcauth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
    32  			"from-helper.example.com": {
    33  				"token": "from-helper",
    34  			},
    35  
    36  			// This should be shadowed by the "configured" entry with the same
    37  			// hostname above.
    38  			"configured.example.com": {
    39  				"token": "incorrectly-from-helper",
    40  			},
    41  		}),
    42  		helperType: "fake",
    43  	}
    44  
    45  	testReqAuthHeader := func(t *testing.T, creds svcauth.HostCredentials) string {
    46  		t.Helper()
    47  
    48  		if creds == nil {
    49  			return ""
    50  		}
    51  
    52  		req, err := http.NewRequest("GET", "http://example.com/", nil)
    53  		if err != nil {
    54  			t.Fatalf("cannot construct HTTP request: %s", err)
    55  		}
    56  		creds.PrepareRequest(req)
    57  		return req.Header.Get("Authorization")
    58  	}
    59  
    60  	t.Run("configured", func(t *testing.T) {
    61  		creds, err := credSrc.ForHost(svchost.Hostname("configured.example.com"))
    62  		if err != nil {
    63  			t.Fatalf("unexpected error: %s", err)
    64  		}
    65  		if got, want := testReqAuthHeader(t, creds), "Bearer configured"; got != want {
    66  			t.Errorf("wrong result\ngot:  %s\nwant: %s", got, want)
    67  		}
    68  	})
    69  	t.Run("from helper", func(t *testing.T) {
    70  		creds, err := credSrc.ForHost(svchost.Hostname("from-helper.example.com"))
    71  		if err != nil {
    72  			t.Fatalf("unexpected error: %s", err)
    73  		}
    74  		if got, want := testReqAuthHeader(t, creds), "Bearer from-helper"; got != want {
    75  			t.Errorf("wrong result\ngot:  %s\nwant: %s", got, want)
    76  		}
    77  	})
    78  	t.Run("not available", func(t *testing.T) {
    79  		creds, err := credSrc.ForHost(svchost.Hostname("unavailable.example.com"))
    80  		if err != nil {
    81  			t.Fatalf("unexpected error: %s", err)
    82  		}
    83  		if got, want := testReqAuthHeader(t, creds), ""; got != want {
    84  			t.Errorf("wrong result\ngot:  %s\nwant: %s", got, want)
    85  		}
    86  	})
    87  	t.Run("set in environment", func(t *testing.T) {
    88  		envName := "TF_TOKEN_configured_example_com"
    89  		t.Cleanup(func() {
    90  			os.Unsetenv(envName)
    91  		})
    92  
    93  		expectedToken := "configured-by-env"
    94  		os.Setenv(envName, expectedToken)
    95  
    96  		creds, err := credSrc.ForHost(svchost.Hostname("configured.example.com"))
    97  		if err != nil {
    98  			t.Fatalf("unexpected error: %s", err)
    99  		}
   100  
   101  		if creds == nil {
   102  			t.Fatal("no credentials found")
   103  		}
   104  
   105  		if got := creds.Token(); got != expectedToken {
   106  			t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken)
   107  		}
   108  	})
   109  
   110  	t.Run("punycode name set in environment", func(t *testing.T) {
   111  		envName := "TF_TOKEN_env_xn--eckwd4c7cu47r2wf_com"
   112  		t.Cleanup(func() {
   113  			os.Unsetenv(envName)
   114  		})
   115  
   116  		expectedToken := "configured-by-env"
   117  		os.Setenv(envName, expectedToken)
   118  
   119  		hostname, _ := svchost.ForComparison("env.ドメイン名例.com")
   120  		creds, err := credSrc.ForHost(hostname)
   121  
   122  		if err != nil {
   123  			t.Fatalf("unexpected error: %s", err)
   124  		}
   125  
   126  		if creds == nil {
   127  			t.Fatal("no credentials found")
   128  		}
   129  
   130  		if got := creds.Token(); got != expectedToken {
   131  			t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken)
   132  		}
   133  	})
   134  
   135  	t.Run("hyphens can be encoded as double underscores", func(t *testing.T) {
   136  		envName := "TF_TOKEN_env_xn____caf__dma_fr"
   137  		expectedToken := "configured-by-fallback"
   138  		t.Cleanup(func() {
   139  			os.Unsetenv(envName)
   140  		})
   141  
   142  		os.Setenv(envName, expectedToken)
   143  
   144  		hostname, _ := svchost.ForComparison("env.café.fr")
   145  		creds, err := credSrc.ForHost(hostname)
   146  
   147  		if err != nil {
   148  			t.Fatalf("unexpected error: %s", err)
   149  		}
   150  
   151  		if creds == nil {
   152  			t.Fatal("no credentials found")
   153  		}
   154  
   155  		if got := creds.Token(); got != expectedToken {
   156  			t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken)
   157  		}
   158  	})
   159  
   160  	t.Run("periods are ok", func(t *testing.T) {
   161  		envName := "TF_TOKEN_configured.example.com"
   162  		expectedToken := "configured-by-env"
   163  		t.Cleanup(func() {
   164  			os.Unsetenv(envName)
   165  		})
   166  
   167  		os.Setenv(envName, expectedToken)
   168  
   169  		hostname, _ := svchost.ForComparison("configured.example.com")
   170  		creds, err := credSrc.ForHost(hostname)
   171  
   172  		if err != nil {
   173  			t.Fatalf("unexpected error: %s", err)
   174  		}
   175  
   176  		if creds == nil {
   177  			t.Fatal("no credentials found")
   178  		}
   179  
   180  		if got := creds.Token(); got != expectedToken {
   181  			t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken)
   182  		}
   183  	})
   184  
   185  	t.Run("casing is insensitive", func(t *testing.T) {
   186  		envName := "TF_TOKEN_CONFIGUREDUPPERCASE_EXAMPLE_COM"
   187  		expectedToken := "configured-by-env"
   188  
   189  		os.Setenv(envName, expectedToken)
   190  		t.Cleanup(func() {
   191  			os.Unsetenv(envName)
   192  		})
   193  
   194  		hostname, _ := svchost.ForComparison("configureduppercase.example.com")
   195  		creds, err := credSrc.ForHost(hostname)
   196  
   197  		if err != nil {
   198  			t.Fatalf("unexpected error: %s", err)
   199  		}
   200  
   201  		if creds == nil {
   202  			t.Fatal("no credentials found")
   203  		}
   204  
   205  		if got := creds.Token(); got != expectedToken {
   206  			t.Errorf("wrong result\ngot: %s\nwant: %s", got, expectedToken)
   207  		}
   208  	})
   209  }
   210  
   211  func TestCredentialsStoreForget(t *testing.T) {
   212  	d := t.TempDir()
   213  
   214  	mockCredsFilename := filepath.Join(d, "credentials.tfrc.json")
   215  
   216  	cfg := &Config{
   217  		// This simulates there being a credentials block manually configured
   218  		// in some file _other than_ credentials.tfrc.json.
   219  		Credentials: map[string]map[string]interface{}{
   220  			"manually-configured.example.com": {
   221  				"token": "manually-configured",
   222  			},
   223  		},
   224  	}
   225  
   226  	// We'll initially use a credentials source with no credentials helper at
   227  	// all, and thus with credentials stored in the credentials file.
   228  	credSrc := cfg.credentialsSource(
   229  		"", nil,
   230  		mockCredsFilename,
   231  	)
   232  
   233  	testReqAuthHeader := func(t *testing.T, creds svcauth.HostCredentials) string {
   234  		t.Helper()
   235  
   236  		if creds == nil {
   237  			return ""
   238  		}
   239  
   240  		req, err := http.NewRequest("GET", "http://example.com/", nil)
   241  		if err != nil {
   242  			t.Fatalf("cannot construct HTTP request: %s", err)
   243  		}
   244  		creds.PrepareRequest(req)
   245  		return req.Header.Get("Authorization")
   246  	}
   247  
   248  	// Because these store/forget calls have side-effects, we'll bail out with
   249  	// t.Fatal (or equivalent) as soon as anything unexpected happens.
   250  	// Otherwise downstream tests might fail in confusing ways.
   251  	{
   252  		err := credSrc.StoreForHost(
   253  			svchost.Hostname("manually-configured.example.com"),
   254  			svcauth.HostCredentialsToken("not-manually-configured"),
   255  		)
   256  		if err == nil {
   257  			t.Fatalf("successfully stored for manually-configured; want error")
   258  		}
   259  		if _, ok := err.(ErrUnwritableHostCredentials); !ok {
   260  			t.Fatalf("wrong error type %T; want ErrUnwritableHostCredentials", err)
   261  		}
   262  	}
   263  	{
   264  		err := credSrc.ForgetForHost(
   265  			svchost.Hostname("manually-configured.example.com"),
   266  		)
   267  		if err == nil {
   268  			t.Fatalf("successfully forgot for manually-configured; want error")
   269  		}
   270  		if _, ok := err.(ErrUnwritableHostCredentials); !ok {
   271  			t.Fatalf("wrong error type %T; want ErrUnwritableHostCredentials", err)
   272  		}
   273  	}
   274  	{
   275  		// We don't have a credentials file at all yet, so this first call
   276  		// must create it.
   277  		err := credSrc.StoreForHost(
   278  			svchost.Hostname("stored-locally.example.com"),
   279  			svcauth.HostCredentialsToken("stored-locally"),
   280  		)
   281  		if err != nil {
   282  			t.Fatalf("unexpected error storing locally: %s", err)
   283  		}
   284  
   285  		creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com"))
   286  		if err != nil {
   287  			t.Fatalf("failed to read back stored-locally credentials: %s", err)
   288  		}
   289  
   290  		if got, want := testReqAuthHeader(t, creds), "Bearer stored-locally"; got != want {
   291  			t.Fatalf("wrong header value for stored-locally\ngot:  %s\nwant: %s", got, want)
   292  		}
   293  
   294  		got := readHostsInCredentialsFile(mockCredsFilename)
   295  		want := map[svchost.Hostname]struct{}{
   296  			svchost.Hostname("stored-locally.example.com"): struct{}{},
   297  		}
   298  		if diff := cmp.Diff(want, got); diff != "" {
   299  			t.Fatalf("wrong credentials file content\n%s", diff)
   300  		}
   301  	}
   302  
   303  	// Now we'll switch to having a credential helper active.
   304  	// If we were loading the real CLI config from disk here then this
   305  	// entry would already be in cfg.Credentials, but we need to fake that
   306  	// in the test because we're constructing this *Config value directly.
   307  	cfg.Credentials["stored-locally.example.com"] = map[string]interface{}{
   308  		"token": "stored-locally",
   309  	}
   310  	mockHelper := &mockCredentialsHelper{current: make(map[svchost.Hostname]cty.Value)}
   311  	credSrc = cfg.credentialsSource(
   312  		"mock", mockHelper,
   313  		mockCredsFilename,
   314  	)
   315  	{
   316  		err := credSrc.StoreForHost(
   317  			svchost.Hostname("manually-configured.example.com"),
   318  			svcauth.HostCredentialsToken("not-manually-configured"),
   319  		)
   320  		if err == nil {
   321  			t.Fatalf("successfully stored for manually-configured with helper active; want error")
   322  		}
   323  	}
   324  	{
   325  		err := credSrc.StoreForHost(
   326  			svchost.Hostname("stored-in-helper.example.com"),
   327  			svcauth.HostCredentialsToken("stored-in-helper"),
   328  		)
   329  		if err != nil {
   330  			t.Fatalf("unexpected error storing in helper: %s", err)
   331  		}
   332  
   333  		creds, err := credSrc.ForHost(svchost.Hostname("stored-in-helper.example.com"))
   334  		if err != nil {
   335  			t.Fatalf("failed to read back stored-in-helper credentials: %s", err)
   336  		}
   337  
   338  		if got, want := testReqAuthHeader(t, creds), "Bearer stored-in-helper"; got != want {
   339  			t.Fatalf("wrong header value for stored-in-helper\ngot:  %s\nwant: %s", got, want)
   340  		}
   341  
   342  		// Nothing should have changed in the saved credentials file
   343  		got := readHostsInCredentialsFile(mockCredsFilename)
   344  		want := map[svchost.Hostname]struct{}{
   345  			svchost.Hostname("stored-locally.example.com"): struct{}{},
   346  		}
   347  		if diff := cmp.Diff(want, got); diff != "" {
   348  			t.Fatalf("wrong credentials file content\n%s", diff)
   349  		}
   350  	}
   351  	{
   352  		// Because stored-locally is already in the credentials file, a new
   353  		// store should be sent there rather than to the credentials helper.
   354  		err := credSrc.StoreForHost(
   355  			svchost.Hostname("stored-locally.example.com"),
   356  			svcauth.HostCredentialsToken("stored-locally-again"),
   357  		)
   358  		if err != nil {
   359  			t.Fatalf("unexpected error storing locally again: %s", err)
   360  		}
   361  
   362  		creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com"))
   363  		if err != nil {
   364  			t.Fatalf("failed to read back stored-locally credentials: %s", err)
   365  		}
   366  
   367  		if got, want := testReqAuthHeader(t, creds), "Bearer stored-locally-again"; got != want {
   368  			t.Fatalf("wrong header value for stored-locally\ngot:  %s\nwant: %s", got, want)
   369  		}
   370  	}
   371  	{
   372  		// Forgetting a host already in the credentials file should remove it
   373  		// from the credentials file, not from the helper.
   374  		err := credSrc.ForgetForHost(
   375  			svchost.Hostname("stored-locally.example.com"),
   376  		)
   377  		if err != nil {
   378  			t.Fatalf("unexpected error forgetting locally: %s", err)
   379  		}
   380  
   381  		creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com"))
   382  		if err != nil {
   383  			t.Fatalf("failed to read back stored-locally credentials: %s", err)
   384  		}
   385  
   386  		if got, want := testReqAuthHeader(t, creds), ""; got != want {
   387  			t.Fatalf("wrong header value for stored-locally\ngot:  %s\nwant: %s", got, want)
   388  		}
   389  
   390  		// Should not be present in the credentials file anymore
   391  		got := readHostsInCredentialsFile(mockCredsFilename)
   392  		want := map[svchost.Hostname]struct{}{}
   393  		if diff := cmp.Diff(want, got); diff != "" {
   394  			t.Fatalf("wrong credentials file content\n%s", diff)
   395  		}
   396  	}
   397  	{
   398  		err := credSrc.ForgetForHost(
   399  			svchost.Hostname("stored-in-helper.example.com"),
   400  		)
   401  		if err != nil {
   402  			t.Fatalf("unexpected error forgetting in helper: %s", err)
   403  		}
   404  
   405  		creds, err := credSrc.ForHost(svchost.Hostname("stored-in-helper.example.com"))
   406  		if err != nil {
   407  			t.Fatalf("failed to read back stored-in-helper credentials: %s", err)
   408  		}
   409  
   410  		if got, want := testReqAuthHeader(t, creds), ""; got != want {
   411  			t.Fatalf("wrong header value for stored-in-helper\ngot:  %s\nwant: %s", got, want)
   412  		}
   413  	}
   414  
   415  	{
   416  		// Finally, the log in our mock helper should show that it was only
   417  		// asked to deal with stored-in-helper, not stored-locally.
   418  		got := mockHelper.log
   419  		want := []mockCredentialsHelperChange{
   420  			{
   421  				Host:   svchost.Hostname("stored-in-helper.example.com"),
   422  				Action: "store",
   423  			},
   424  			{
   425  				Host:   svchost.Hostname("stored-in-helper.example.com"),
   426  				Action: "forget",
   427  			},
   428  		}
   429  		if diff := cmp.Diff(want, got); diff != "" {
   430  			t.Errorf("unexpected credentials helper operation log\n%s", diff)
   431  		}
   432  	}
   433  }
   434  
   435  type mockCredentialsHelperChange struct {
   436  	Host   svchost.Hostname
   437  	Action string
   438  }
   439  
   440  type mockCredentialsHelper struct {
   441  	current map[svchost.Hostname]cty.Value
   442  	log     []mockCredentialsHelperChange
   443  }
   444  
   445  // Assertion that mockCredentialsHelper implements svcauth.CredentialsSource
   446  var _ svcauth.CredentialsSource = (*mockCredentialsHelper)(nil)
   447  
   448  func (s *mockCredentialsHelper) ForHost(hostname svchost.Hostname) (svcauth.HostCredentials, error) {
   449  	v, ok := s.current[hostname]
   450  	if !ok {
   451  		return nil, nil
   452  	}
   453  	return svcauth.HostCredentialsFromObject(v), nil
   454  }
   455  
   456  func (s *mockCredentialsHelper) StoreForHost(hostname svchost.Hostname, new svcauth.HostCredentialsWritable) error {
   457  	s.log = append(s.log, mockCredentialsHelperChange{
   458  		Host:   hostname,
   459  		Action: "store",
   460  	})
   461  	s.current[hostname] = new.ToStore()
   462  	return nil
   463  }
   464  
   465  func (s *mockCredentialsHelper) ForgetForHost(hostname svchost.Hostname) error {
   466  	s.log = append(s.log, mockCredentialsHelperChange{
   467  		Host:   hostname,
   468  		Action: "forget",
   469  	})
   470  	delete(s.current, hostname)
   471  	return nil
   472  }