github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/command/cliconfig/credentials_test.go (about)

     1  package cliconfig
     2  
     3  import (
     4  	"io/ioutil"
     5  	"net/http"
     6  	"os"
     7  	"path/filepath"
     8  	"testing"
     9  
    10  	"github.com/google/go-cmp/cmp"
    11  	"github.com/zclconf/go-cty/cty"
    12  
    13  	"github.com/hashicorp/terraform-svchost"
    14  	svcauth "github.com/hashicorp/terraform-svchost/auth"
    15  )
    16  
    17  func TestCredentialsForHost(t *testing.T) {
    18  	credSrc := &CredentialsSource{
    19  		configured: map[svchost.Hostname]cty.Value{
    20  			"configured.example.com": cty.ObjectVal(map[string]cty.Value{
    21  				"token": cty.StringVal("configured"),
    22  			}),
    23  			"unused.example.com": cty.ObjectVal(map[string]cty.Value{
    24  				"token": cty.StringVal("incorrectly-configured"),
    25  			}),
    26  		},
    27  
    28  		// We'll use a static source to stand in for what would normally be
    29  		// a credentials helper program, since we're only testing the logic
    30  		// for choosing when to delegate to the helper here. The logic for
    31  		// interacting with a helper program is tested in the svcauth package.
    32  		helper: svcauth.StaticCredentialsSource(map[svchost.Hostname]map[string]interface{}{
    33  			"from-helper.example.com": {
    34  				"token": "from-helper",
    35  			},
    36  
    37  			// This should be shadowed by the "configured" entry with the same
    38  			// hostname above.
    39  			"configured.example.com": {
    40  				"token": "incorrectly-from-helper",
    41  			},
    42  		}),
    43  		helperType: "fake",
    44  	}
    45  
    46  	testReqAuthHeader := func(t *testing.T, creds svcauth.HostCredentials) string {
    47  		t.Helper()
    48  
    49  		if creds == nil {
    50  			return ""
    51  		}
    52  
    53  		req, err := http.NewRequest("GET", "http://example.com/", nil)
    54  		if err != nil {
    55  			t.Fatalf("cannot construct HTTP request: %s", err)
    56  		}
    57  		creds.PrepareRequest(req)
    58  		return req.Header.Get("Authorization")
    59  	}
    60  
    61  	t.Run("configured", func(t *testing.T) {
    62  		creds, err := credSrc.ForHost(svchost.Hostname("configured.example.com"))
    63  		if err != nil {
    64  			t.Fatalf("unexpected error: %s", err)
    65  		}
    66  		if got, want := testReqAuthHeader(t, creds), "Bearer configured"; got != want {
    67  			t.Errorf("wrong result\ngot:  %s\nwant: %s", got, want)
    68  		}
    69  	})
    70  	t.Run("from helper", func(t *testing.T) {
    71  		creds, err := credSrc.ForHost(svchost.Hostname("from-helper.example.com"))
    72  		if err != nil {
    73  			t.Fatalf("unexpected error: %s", err)
    74  		}
    75  		if got, want := testReqAuthHeader(t, creds), "Bearer from-helper"; got != want {
    76  			t.Errorf("wrong result\ngot:  %s\nwant: %s", got, want)
    77  		}
    78  	})
    79  	t.Run("not available", func(t *testing.T) {
    80  		creds, err := credSrc.ForHost(svchost.Hostname("unavailable.example.com"))
    81  		if err != nil {
    82  			t.Fatalf("unexpected error: %s", err)
    83  		}
    84  		if got, want := testReqAuthHeader(t, creds), ""; got != want {
    85  			t.Errorf("wrong result\ngot:  %s\nwant: %s", got, want)
    86  		}
    87  	})
    88  }
    89  
    90  func TestCredentialsStoreForget(t *testing.T) {
    91  	d, err := ioutil.TempDir("", "terraform-cliconfig-test")
    92  	if err != nil {
    93  		t.Fatal(err)
    94  	}
    95  	defer os.RemoveAll(d)
    96  
    97  	mockCredsFilename := filepath.Join(d, "credentials.tfrc.json")
    98  
    99  	cfg := &Config{
   100  		// This simulates there being a credentials block manually configured
   101  		// in some file _other than_ credentials.tfrc.json.
   102  		Credentials: map[string]map[string]interface{}{
   103  			"manually-configured.example.com": {
   104  				"token": "manually-configured",
   105  			},
   106  		},
   107  	}
   108  
   109  	// We'll initially use a credentials source with no credentials helper at
   110  	// all, and thus with credentials stored in the credentials file.
   111  	credSrc := cfg.credentialsSource(
   112  		"", nil,
   113  		mockCredsFilename,
   114  	)
   115  
   116  	testReqAuthHeader := func(t *testing.T, creds svcauth.HostCredentials) string {
   117  		t.Helper()
   118  
   119  		if creds == nil {
   120  			return ""
   121  		}
   122  
   123  		req, err := http.NewRequest("GET", "http://example.com/", nil)
   124  		if err != nil {
   125  			t.Fatalf("cannot construct HTTP request: %s", err)
   126  		}
   127  		creds.PrepareRequest(req)
   128  		return req.Header.Get("Authorization")
   129  	}
   130  
   131  	// Because these store/forget calls have side-effects, we'll bail out with
   132  	// t.Fatal (or equivalent) as soon as anything unexpected happens.
   133  	// Otherwise downstream tests might fail in confusing ways.
   134  	{
   135  		err := credSrc.StoreForHost(
   136  			svchost.Hostname("manually-configured.example.com"),
   137  			svcauth.HostCredentialsToken("not-manually-configured"),
   138  		)
   139  		if err == nil {
   140  			t.Fatalf("successfully stored for manually-configured; want error")
   141  		}
   142  		if _, ok := err.(ErrUnwritableHostCredentials); !ok {
   143  			t.Fatalf("wrong error type %T; want ErrUnwritableHostCredentials", err)
   144  		}
   145  	}
   146  	{
   147  		err := credSrc.ForgetForHost(
   148  			svchost.Hostname("manually-configured.example.com"),
   149  		)
   150  		if err == nil {
   151  			t.Fatalf("successfully forgot for manually-configured; want error")
   152  		}
   153  		if _, ok := err.(ErrUnwritableHostCredentials); !ok {
   154  			t.Fatalf("wrong error type %T; want ErrUnwritableHostCredentials", err)
   155  		}
   156  	}
   157  	{
   158  		// We don't have a credentials file at all yet, so this first call
   159  		// must create it.
   160  		err := credSrc.StoreForHost(
   161  			svchost.Hostname("stored-locally.example.com"),
   162  			svcauth.HostCredentialsToken("stored-locally"),
   163  		)
   164  		if err != nil {
   165  			t.Fatalf("unexpected error storing locally: %s", err)
   166  		}
   167  
   168  		creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com"))
   169  		if err != nil {
   170  			t.Fatalf("failed to read back stored-locally credentials: %s", err)
   171  		}
   172  
   173  		if got, want := testReqAuthHeader(t, creds), "Bearer stored-locally"; got != want {
   174  			t.Fatalf("wrong header value for stored-locally\ngot:  %s\nwant: %s", got, want)
   175  		}
   176  
   177  		got := readHostsInCredentialsFile(mockCredsFilename)
   178  		want := map[svchost.Hostname]struct{}{
   179  			svchost.Hostname("stored-locally.example.com"): struct{}{},
   180  		}
   181  		if diff := cmp.Diff(want, got); diff != "" {
   182  			t.Fatalf("wrong credentials file content\n%s", diff)
   183  		}
   184  	}
   185  
   186  	// Now we'll switch to having a credential helper active.
   187  	// If we were loading the real CLI config from disk here then this
   188  	// entry would already be in cfg.Credentials, but we need to fake that
   189  	// in the test because we're constructing this *Config value directly.
   190  	cfg.Credentials["stored-locally.example.com"] = map[string]interface{}{
   191  		"token": "stored-locally",
   192  	}
   193  	mockHelper := &mockCredentialsHelper{current: make(map[svchost.Hostname]cty.Value)}
   194  	credSrc = cfg.credentialsSource(
   195  		"mock", mockHelper,
   196  		mockCredsFilename,
   197  	)
   198  	{
   199  		err := credSrc.StoreForHost(
   200  			svchost.Hostname("manually-configured.example.com"),
   201  			svcauth.HostCredentialsToken("not-manually-configured"),
   202  		)
   203  		if err == nil {
   204  			t.Fatalf("successfully stored for manually-configured with helper active; want error")
   205  		}
   206  	}
   207  	{
   208  		err := credSrc.StoreForHost(
   209  			svchost.Hostname("stored-in-helper.example.com"),
   210  			svcauth.HostCredentialsToken("stored-in-helper"),
   211  		)
   212  		if err != nil {
   213  			t.Fatalf("unexpected error storing in helper: %s", err)
   214  		}
   215  
   216  		creds, err := credSrc.ForHost(svchost.Hostname("stored-in-helper.example.com"))
   217  		if err != nil {
   218  			t.Fatalf("failed to read back stored-in-helper credentials: %s", err)
   219  		}
   220  
   221  		if got, want := testReqAuthHeader(t, creds), "Bearer stored-in-helper"; got != want {
   222  			t.Fatalf("wrong header value for stored-in-helper\ngot:  %s\nwant: %s", got, want)
   223  		}
   224  
   225  		// Nothing should have changed in the saved credentials file
   226  		got := readHostsInCredentialsFile(mockCredsFilename)
   227  		want := map[svchost.Hostname]struct{}{
   228  			svchost.Hostname("stored-locally.example.com"): struct{}{},
   229  		}
   230  		if diff := cmp.Diff(want, got); diff != "" {
   231  			t.Fatalf("wrong credentials file content\n%s", diff)
   232  		}
   233  	}
   234  	{
   235  		// Because stored-locally is already in the credentials file, a new
   236  		// store should be sent there rather than to the credentials helper.
   237  		err := credSrc.StoreForHost(
   238  			svchost.Hostname("stored-locally.example.com"),
   239  			svcauth.HostCredentialsToken("stored-locally-again"),
   240  		)
   241  		if err != nil {
   242  			t.Fatalf("unexpected error storing locally again: %s", err)
   243  		}
   244  
   245  		creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com"))
   246  		if err != nil {
   247  			t.Fatalf("failed to read back stored-locally credentials: %s", err)
   248  		}
   249  
   250  		if got, want := testReqAuthHeader(t, creds), "Bearer stored-locally-again"; got != want {
   251  			t.Fatalf("wrong header value for stored-locally\ngot:  %s\nwant: %s", got, want)
   252  		}
   253  	}
   254  	{
   255  		// Forgetting a host already in the credentials file should remove it
   256  		// from the credentials file, not from the helper.
   257  		err := credSrc.ForgetForHost(
   258  			svchost.Hostname("stored-locally.example.com"),
   259  		)
   260  		if err != nil {
   261  			t.Fatalf("unexpected error forgetting locally: %s", err)
   262  		}
   263  
   264  		creds, err := credSrc.ForHost(svchost.Hostname("stored-locally.example.com"))
   265  		if err != nil {
   266  			t.Fatalf("failed to read back stored-locally credentials: %s", err)
   267  		}
   268  
   269  		if got, want := testReqAuthHeader(t, creds), ""; got != want {
   270  			t.Fatalf("wrong header value for stored-locally\ngot:  %s\nwant: %s", got, want)
   271  		}
   272  
   273  		// Should not be present in the credentials file anymore
   274  		got := readHostsInCredentialsFile(mockCredsFilename)
   275  		want := map[svchost.Hostname]struct{}{}
   276  		if diff := cmp.Diff(want, got); diff != "" {
   277  			t.Fatalf("wrong credentials file content\n%s", diff)
   278  		}
   279  	}
   280  	{
   281  		err := credSrc.ForgetForHost(
   282  			svchost.Hostname("stored-in-helper.example.com"),
   283  		)
   284  		if err != nil {
   285  			t.Fatalf("unexpected error forgetting in helper: %s", err)
   286  		}
   287  
   288  		creds, err := credSrc.ForHost(svchost.Hostname("stored-in-helper.example.com"))
   289  		if err != nil {
   290  			t.Fatalf("failed to read back stored-in-helper credentials: %s", err)
   291  		}
   292  
   293  		if got, want := testReqAuthHeader(t, creds), ""; got != want {
   294  			t.Fatalf("wrong header value for stored-in-helper\ngot:  %s\nwant: %s", got, want)
   295  		}
   296  	}
   297  
   298  	{
   299  		// Finally, the log in our mock helper should show that it was only
   300  		// asked to deal with stored-in-helper, not stored-locally.
   301  		got := mockHelper.log
   302  		want := []mockCredentialsHelperChange{
   303  			{
   304  				Host:   svchost.Hostname("stored-in-helper.example.com"),
   305  				Action: "store",
   306  			},
   307  			{
   308  				Host:   svchost.Hostname("stored-in-helper.example.com"),
   309  				Action: "forget",
   310  			},
   311  		}
   312  		if diff := cmp.Diff(want, got); diff != "" {
   313  			t.Errorf("unexpected credentials helper operation log\n%s", diff)
   314  		}
   315  	}
   316  }
   317  
   318  type mockCredentialsHelperChange struct {
   319  	Host   svchost.Hostname
   320  	Action string
   321  }
   322  
   323  type mockCredentialsHelper struct {
   324  	current map[svchost.Hostname]cty.Value
   325  	log     []mockCredentialsHelperChange
   326  }
   327  
   328  // Assertion that mockCredentialsHelper implements svcauth.CredentialsSource
   329  var _ svcauth.CredentialsSource = (*mockCredentialsHelper)(nil)
   330  
   331  func (s *mockCredentialsHelper) ForHost(hostname svchost.Hostname) (svcauth.HostCredentials, error) {
   332  	v, ok := s.current[hostname]
   333  	if !ok {
   334  		return nil, nil
   335  	}
   336  	return svcauth.HostCredentialsFromObject(v), nil
   337  }
   338  
   339  func (s *mockCredentialsHelper) StoreForHost(hostname svchost.Hostname, new svcauth.HostCredentialsWritable) error {
   340  	s.log = append(s.log, mockCredentialsHelperChange{
   341  		Host:   hostname,
   342  		Action: "store",
   343  	})
   344  	s.current[hostname] = new.ToStore()
   345  	return nil
   346  }
   347  
   348  func (s *mockCredentialsHelper) ForgetForHost(hostname svchost.Hostname) error {
   349  	s.log = append(s.log, mockCredentialsHelperChange{
   350  		Host:   hostname,
   351  		Action: "forget",
   352  	})
   353  	delete(s.current, hostname)
   354  	return nil
   355  }