k8s.io/client-go@v0.22.2/plugin/pkg/client/auth/gcp/gcp_test.go (about)

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package gcp
    18  
    19  import (
    20  	"fmt"
    21  	"io/ioutil"
    22  	"net/http"
    23  	"os"
    24  	"os/exec"
    25  	"reflect"
    26  	"strings"
    27  	"sync"
    28  	"testing"
    29  	"time"
    30  
    31  	"golang.org/x/oauth2"
    32  )
    33  
    34  type fakeOutput struct {
    35  	args   []string
    36  	output string
    37  }
    38  
    39  var (
    40  	wantCmd []string
    41  	// Output for fakeExec, keyed by command
    42  	execOutputs = map[string]fakeOutput{
    43  		"/default/no/args": {
    44  			args: []string{},
    45  			output: `{
    46    "access_token": "faketoken",
    47    "token_expiry": "2016-10-31T22:31:09.123000000Z"
    48  }`},
    49  		"/default/legacy/args": {
    50  			args: []string{"arg1", "arg2", "arg3"},
    51  			output: `{
    52    "access_token": "faketoken",
    53    "token_expiry": "2016-10-31T22:31:09.123000000Z"
    54  }`},
    55  		"/space in path/customkeys": {
    56  			args: []string{"can", "haz", "auth"},
    57  			output: `{
    58    "token": "faketoken",
    59    "token_expiry": {
    60      "datetime": "2016-10-31 22:31:09.123"
    61    }
    62  }`},
    63  		"missing/tokenkey/noargs": {
    64  			args: []string{},
    65  			output: `{
    66    "broken": "faketoken",
    67    "token_expiry": {
    68      "datetime": "2016-10-31 22:31:09.123000000Z"
    69    }
    70  }`},
    71  		"missing/expirykey/legacyargs": {
    72  			args: []string{"split", "on", "whitespace"},
    73  			output: `{
    74    "access_token": "faketoken",
    75    "expires": "2016-10-31T22:31:09.123000000Z"
    76  }`},
    77  		"invalid expiry/timestamp": {
    78  			args: []string{"foo", "--bar", "--baz=abc,def"},
    79  			output: `{
    80    "access_token": "faketoken",
    81    "token_expiry": "sometime soon, idk"
    82  }`},
    83  		"badjson": {
    84  			args: []string{},
    85  			output: `{
    86    "access_token": "faketoken",
    87    "token_expiry": "sometime soon, idk"
    88    ------
    89  `},
    90  	}
    91  )
    92  
    93  func fakeExec(command string, args ...string) *exec.Cmd {
    94  	cs := []string{"-test.run=TestHelperProcess", "--", command}
    95  	cs = append(cs, args...)
    96  	cmd := exec.Command(os.Args[0], cs...)
    97  	cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
    98  	return cmd
    99  }
   100  
   101  func TestHelperProcess(t *testing.T) {
   102  	if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
   103  		return
   104  	}
   105  	// Strip out the leading args used to exec into this function.
   106  	gotCmd := os.Args[3]
   107  	gotArgs := os.Args[4:]
   108  	output, ok := execOutputs[gotCmd]
   109  	if !ok {
   110  		fmt.Fprintf(os.Stdout, "unexpected call cmd=%q args=%v\n", gotCmd, gotArgs)
   111  		os.Exit(1)
   112  	} else if !reflect.DeepEqual(output.args, gotArgs) {
   113  		fmt.Fprintf(os.Stdout, "call cmd=%q got args %v, want: %v\n", gotCmd, gotArgs, output.args)
   114  		os.Exit(1)
   115  	}
   116  	fmt.Fprintf(os.Stdout, output.output)
   117  	os.Exit(0)
   118  }
   119  
   120  func Test_isCmdTokenSource(t *testing.T) {
   121  	c1 := map[string]string{"cmd-path": "foo"}
   122  	if v := isCmdTokenSource(c1); !v {
   123  		t.Fatalf("cmd-path present in config (%+v), but got %v", c1, v)
   124  	}
   125  
   126  	c2 := map[string]string{"cmd-args": "foo bar"}
   127  	if v := isCmdTokenSource(c2); v {
   128  		t.Fatalf("cmd-path not present in config (%+v), but got %v", c2, v)
   129  	}
   130  }
   131  
   132  func Test_tokenSource_cmd(t *testing.T) {
   133  	if _, err := tokenSource(true, map[string]string{}); err == nil {
   134  		t.Fatalf("expected error, cmd-args not present in config")
   135  	}
   136  
   137  	c := map[string]string{
   138  		"cmd-path": "foo",
   139  		"cmd-args": "bar"}
   140  	ts, err := tokenSource(true, c)
   141  	if err != nil {
   142  		t.Fatalf("failed to return cmd token source: %+v", err)
   143  	}
   144  	if ts == nil {
   145  		t.Fatal("returned nil token source")
   146  	}
   147  	if _, ok := ts.(*commandTokenSource); !ok {
   148  		t.Fatalf("returned token source type:(%T) expected:(*commandTokenSource)", ts)
   149  	}
   150  }
   151  
   152  func Test_tokenSource_cmdCannotBeUsedWithScopes(t *testing.T) {
   153  	c := map[string]string{
   154  		"cmd-path": "foo",
   155  		"scopes":   "A,B"}
   156  	if _, err := tokenSource(true, c); err == nil {
   157  		t.Fatal("expected error when scopes is used with cmd-path")
   158  	}
   159  }
   160  
   161  func Test_tokenSource_applicationDefaultCredentials_fails(t *testing.T) {
   162  	// try to use empty ADC file
   163  	fakeTokenFile, err := ioutil.TempFile("", "adctoken")
   164  	if err != nil {
   165  		t.Fatalf("failed to create fake token file: +%v", err)
   166  	}
   167  	fakeTokenFile.Close()
   168  	defer os.Remove(fakeTokenFile.Name())
   169  
   170  	os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", fakeTokenFile.Name())
   171  	defer os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS")
   172  	if _, err := tokenSource(false, map[string]string{}); err == nil {
   173  		t.Fatalf("expected error because specified ADC token file is not a JSON")
   174  	}
   175  }
   176  
   177  func Test_tokenSource_applicationDefaultCredentials(t *testing.T) {
   178  	fakeTokenFile, err := ioutil.TempFile("", "adctoken")
   179  	if err != nil {
   180  		t.Fatalf("failed to create fake token file: +%v", err)
   181  	}
   182  	fakeTokenFile.Close()
   183  	defer os.Remove(fakeTokenFile.Name())
   184  	if err := ioutil.WriteFile(fakeTokenFile.Name(), []byte(`{"type":"service_account"}`), 0600); err != nil {
   185  		t.Fatalf("failed to write to fake token file: %+v", err)
   186  	}
   187  
   188  	os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", fakeTokenFile.Name())
   189  	defer os.Unsetenv("GOOGLE_APPLICATION_CREDENTIALS")
   190  	ts, err := tokenSource(false, map[string]string{})
   191  	if err != nil {
   192  		t.Fatalf("failed to get a token source: %+v", err)
   193  	}
   194  	if ts == nil {
   195  		t.Fatal("returned nil token source")
   196  	}
   197  }
   198  
   199  func Test_parseScopes(t *testing.T) {
   200  	cases := []struct {
   201  		in  map[string]string
   202  		out []string
   203  	}{
   204  		{
   205  			map[string]string{},
   206  			[]string{
   207  				"https://www.googleapis.com/auth/cloud-platform",
   208  				"https://www.googleapis.com/auth/userinfo.email"},
   209  		},
   210  		{
   211  			map[string]string{"scopes": ""},
   212  			[]string{},
   213  		},
   214  		{
   215  			map[string]string{"scopes": "A,B,C"},
   216  			[]string{"A", "B", "C"},
   217  		},
   218  	}
   219  
   220  	for _, c := range cases {
   221  		got := parseScopes(c.in)
   222  		if !reflect.DeepEqual(got, c.out) {
   223  			t.Errorf("expected=%v, got=%v", c.out, got)
   224  		}
   225  	}
   226  }
   227  
   228  func errEquiv(got, want error) bool {
   229  	if got == want {
   230  		return true
   231  	}
   232  	if got != nil && want != nil {
   233  		return strings.Contains(got.Error(), want.Error())
   234  	}
   235  	return false
   236  }
   237  
   238  func TestCmdTokenSource(t *testing.T) {
   239  	execCommand = fakeExec
   240  	fakeExpiry := time.Date(2016, 10, 31, 22, 31, 9, 123000000, time.UTC)
   241  	customFmt := "2006-01-02 15:04:05.999999999"
   242  
   243  	tests := []struct {
   244  		name             string
   245  		gcpConfig        map[string]string
   246  		tok              *oauth2.Token
   247  		newErr, tokenErr error
   248  	}{
   249  		{
   250  			"default",
   251  			map[string]string{
   252  				"cmd-path": "/default/no/args",
   253  			},
   254  			&oauth2.Token{
   255  				AccessToken: "faketoken",
   256  				TokenType:   "Bearer",
   257  				Expiry:      fakeExpiry,
   258  			},
   259  			nil,
   260  			nil,
   261  		},
   262  		{
   263  			"default legacy args",
   264  			map[string]string{
   265  				"cmd-path": "/default/legacy/args arg1 arg2 arg3",
   266  			},
   267  			&oauth2.Token{
   268  				AccessToken: "faketoken",
   269  				TokenType:   "Bearer",
   270  				Expiry:      fakeExpiry,
   271  			},
   272  			nil,
   273  			nil,
   274  		},
   275  
   276  		{
   277  			"custom keys",
   278  			map[string]string{
   279  				"cmd-path":   "/space in path/customkeys",
   280  				"cmd-args":   "can haz auth",
   281  				"token-key":  "{.token}",
   282  				"expiry-key": "{.token_expiry.datetime}",
   283  				"time-fmt":   customFmt,
   284  			},
   285  			&oauth2.Token{
   286  				AccessToken: "faketoken",
   287  				TokenType:   "Bearer",
   288  				Expiry:      fakeExpiry,
   289  			},
   290  			nil,
   291  			nil,
   292  		},
   293  		{
   294  			"missing cmd",
   295  			map[string]string{
   296  				"cmd-path": "",
   297  			},
   298  			nil,
   299  			fmt.Errorf("missing access token cmd"),
   300  			nil,
   301  		},
   302  		{
   303  			"missing token-key",
   304  			map[string]string{
   305  				"cmd-path":  "missing/tokenkey/noargs",
   306  				"token-key": "{.token}",
   307  			},
   308  			nil,
   309  			nil,
   310  			fmt.Errorf("error parsing token-key %q", "{.token}"),
   311  		},
   312  
   313  		{
   314  			"missing expiry-key",
   315  			map[string]string{
   316  				"cmd-path":   "missing/expirykey/legacyargs split on whitespace",
   317  				"expiry-key": "{.expiry}",
   318  			},
   319  			nil,
   320  			nil,
   321  			fmt.Errorf("error parsing expiry-key %q", "{.expiry}"),
   322  		},
   323  		{
   324  			"invalid expiry timestamp",
   325  			map[string]string{
   326  				"cmd-path": "invalid expiry/timestamp",
   327  				"cmd-args": "foo --bar --baz=abc,def",
   328  			},
   329  			&oauth2.Token{
   330  				AccessToken: "faketoken",
   331  				TokenType:   "Bearer",
   332  				Expiry:      time.Time{},
   333  			},
   334  			nil,
   335  			nil,
   336  		},
   337  		{
   338  			"bad JSON",
   339  			map[string]string{
   340  				"cmd-path": "badjson",
   341  			},
   342  			nil,
   343  			nil,
   344  			fmt.Errorf("invalid character '-' after object key:value pair"),
   345  		},
   346  	}
   347  
   348  	for _, tc := range tests {
   349  		provider, err := newGCPAuthProvider("", tc.gcpConfig, nil /* persister */)
   350  		if !errEquiv(err, tc.newErr) {
   351  			t.Errorf("%q newGCPAuthProvider error: got %v, want %v", tc.name, err, tc.newErr)
   352  			continue
   353  		}
   354  		if err != nil {
   355  			continue
   356  		}
   357  		ts := provider.(*gcpAuthProvider).tokenSource.(*cachedTokenSource).source.(*commandTokenSource)
   358  		wantCmd = append([]string{ts.cmd}, ts.args...)
   359  		tok, err := ts.Token()
   360  		if !errEquiv(err, tc.tokenErr) {
   361  			t.Errorf("%q Token() error: got %v, want %v", tc.name, err, tc.tokenErr)
   362  		}
   363  		if !reflect.DeepEqual(tok, tc.tok) {
   364  			t.Errorf("%q Token() got %v, want %v", tc.name, tok, tc.tok)
   365  		}
   366  	}
   367  }
   368  
   369  type fakePersister struct {
   370  	lk    sync.Mutex
   371  	cache map[string]string
   372  }
   373  
   374  func (f *fakePersister) Persist(cache map[string]string) error {
   375  	f.lk.Lock()
   376  	defer f.lk.Unlock()
   377  	f.cache = map[string]string{}
   378  	for k, v := range cache {
   379  		f.cache[k] = v
   380  	}
   381  	return nil
   382  }
   383  
   384  func (f *fakePersister) read() map[string]string {
   385  	ret := map[string]string{}
   386  	f.lk.Lock()
   387  	defer f.lk.Unlock()
   388  	for k, v := range f.cache {
   389  		ret[k] = v
   390  	}
   391  	return ret
   392  }
   393  
   394  type fakeTokenSource struct {
   395  	token *oauth2.Token
   396  	err   error
   397  }
   398  
   399  func (f *fakeTokenSource) Token() (*oauth2.Token, error) {
   400  	return f.token, f.err
   401  }
   402  
   403  func TestCachedTokenSource(t *testing.T) {
   404  	tok := &oauth2.Token{AccessToken: "fakeaccesstoken"}
   405  	persister := &fakePersister{}
   406  	source := &fakeTokenSource{
   407  		token: tok,
   408  		err:   nil,
   409  	}
   410  	cache := map[string]string{
   411  		"foo": "bar",
   412  		"baz": "bazinga",
   413  	}
   414  	ts, err := newCachedTokenSource("fakeaccesstoken", "", persister, source, cache)
   415  	if err != nil {
   416  		t.Fatal(err)
   417  	}
   418  	var wg sync.WaitGroup
   419  	wg.Add(10)
   420  	for i := 0; i < 10; i++ {
   421  		go func() {
   422  			_, err := ts.Token()
   423  			if err != nil {
   424  				t.Errorf("unexpected error: %s", err)
   425  			}
   426  			wg.Done()
   427  		}()
   428  	}
   429  	wg.Wait()
   430  	cache["access-token"] = "fakeaccesstoken"
   431  	cache["expiry"] = tok.Expiry.Format(time.RFC3339Nano)
   432  	if got := persister.read(); !reflect.DeepEqual(got, cache) {
   433  		t.Errorf("got cache %v, want %v", got, cache)
   434  	}
   435  }
   436  
   437  type MockTransport struct {
   438  	res *http.Response
   439  }
   440  
   441  func (t *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
   442  	return t.res, nil
   443  }
   444  
   445  func Test_cmdTokenSource_roundTrip(t *testing.T) {
   446  
   447  	accessToken := "fakeToken"
   448  	fakeExpiry := time.Now().Add(time.Hour)
   449  	fakeExpiryStr := fakeExpiry.Format(time.RFC3339Nano)
   450  	fs := &fakeTokenSource{
   451  		token: &oauth2.Token{
   452  			AccessToken: accessToken,
   453  			Expiry:      fakeExpiry,
   454  		},
   455  	}
   456  
   457  	cmdCache := map[string]string{
   458  		"cmd-path": "/path/to/tokensource/cmd",
   459  		"cmd-args": "--output=json",
   460  	}
   461  	cmdCacheUpdated := map[string]string{
   462  		"cmd-path":     "/path/to/tokensource/cmd",
   463  		"cmd-args":     "--output=json",
   464  		"access-token": accessToken,
   465  		"expiry":       fakeExpiryStr,
   466  	}
   467  	simpleCacheUpdated := map[string]string{
   468  		"access-token": accessToken,
   469  		"expiry":       fakeExpiryStr,
   470  	}
   471  
   472  	tests := []struct {
   473  		name                     string
   474  		res                      http.Response
   475  		baseCache, expectedCache map[string]string
   476  	}{
   477  		{
   478  			"Unauthorized",
   479  			http.Response{StatusCode: http.StatusUnauthorized},
   480  			make(map[string]string),
   481  			make(map[string]string),
   482  		},
   483  		{
   484  			"Unauthorized, nonempty defaultCache",
   485  			http.Response{StatusCode: http.StatusUnauthorized},
   486  			cmdCache,
   487  			cmdCache,
   488  		},
   489  		{
   490  			"Authorized",
   491  			http.Response{StatusCode: http.StatusOK},
   492  			make(map[string]string),
   493  			simpleCacheUpdated,
   494  		},
   495  		{
   496  			"Authorized, nonempty defaultCache",
   497  			http.Response{StatusCode: http.StatusOK},
   498  			cmdCache,
   499  			cmdCacheUpdated,
   500  		},
   501  	}
   502  
   503  	persister := &fakePersister{}
   504  	req := http.Request{Header: http.Header{}}
   505  
   506  	for _, tc := range tests {
   507  		cts, err := newCachedTokenSource(accessToken, fakeExpiry.String(), persister, fs, tc.baseCache)
   508  		if err != nil {
   509  			t.Fatalf("unexpected error from newCachedTokenSource: %v", err)
   510  		}
   511  		authProvider := gcpAuthProvider{cts, persister}
   512  
   513  		fakeTransport := MockTransport{&tc.res}
   514  		transport := (authProvider.WrapTransport(&fakeTransport))
   515  		// call Token to persist/update cache
   516  		if _, err := cts.Token(); err != nil {
   517  			t.Fatalf("unexpected error from cachedTokenSource.Token(): %v", err)
   518  		}
   519  
   520  		transport.RoundTrip(&req)
   521  
   522  		if got := persister.read(); !reflect.DeepEqual(got, tc.expectedCache) {
   523  			t.Errorf("got cache %v, want %v", got, tc.expectedCache)
   524  		}
   525  	}
   526  
   527  }