From 283bb31ada2991f4b3f6eb3b351722dddce80445 Mon Sep 17 00:00:00 2001 From: Jeff Lowdermilk Date: Tue, 1 Nov 2016 14:26:24 -0700 Subject: [PATCH] Add cmd support to gcp auth provider plugin --- plugin/pkg/client/auth/gcp/BUILD | 10 ++ plugin/pkg/client/auth/gcp/OWNERS | 3 + plugin/pkg/client/auth/gcp/gcp.go | 128 ++++++++++++++++++++-- plugin/pkg/client/auth/gcp/gcp_test.go | 143 +++++++++++++++++++++++++ test/test_owners.csv | 1 + 5 files changed, 274 insertions(+), 11 deletions(-) create mode 100644 plugin/pkg/client/auth/gcp/OWNERS create mode 100644 plugin/pkg/client/auth/gcp/gcp_test.go diff --git a/plugin/pkg/client/auth/gcp/BUILD b/plugin/pkg/client/auth/gcp/BUILD index 191d470fb83..43603b57a92 100644 --- a/plugin/pkg/client/auth/gcp/BUILD +++ b/plugin/pkg/client/auth/gcp/BUILD @@ -16,9 +16,19 @@ go_library( tags = ["automanaged"], deps = [ "//pkg/client/restclient:go_default_library", + "//pkg/util/jsonpath:go_default_library", + "//pkg/util/yaml:go_default_library", "//vendor:github.com/golang/glog", "//vendor:golang.org/x/net/context", "//vendor:golang.org/x/oauth2", "//vendor:golang.org/x/oauth2/google", ], ) + +go_test( + name = "go_default_test", + srcs = ["gcp_test.go"], + library = "go_default_library", + tags = ["automanaged"], + deps = ["//vendor:golang.org/x/oauth2"], +) diff --git a/plugin/pkg/client/auth/gcp/OWNERS b/plugin/pkg/client/auth/gcp/OWNERS new file mode 100644 index 00000000000..d75421c5efd --- /dev/null +++ b/plugin/pkg/client/auth/gcp/OWNERS @@ -0,0 +1,3 @@ +assignees: + - cjcullen + - jlowdermilk diff --git a/plugin/pkg/client/auth/gcp/gcp.go b/plugin/pkg/client/auth/gcp/gcp.go index 57fd98c35e0..97f9912dadb 100644 --- a/plugin/pkg/client/auth/gcp/gcp.go +++ b/plugin/pkg/client/auth/gcp/gcp.go @@ -17,7 +17,12 @@ limitations under the License. package gcp import ( + "bytes" + "encoding/json" + "fmt" "net/http" + "os/exec" + "strings" "time" "github.com/golang/glog" @@ -25,6 +30,8 @@ import ( "golang.org/x/oauth2" "golang.org/x/oauth2/google" "k8s.io/kubernetes/pkg/client/restclient" + "k8s.io/kubernetes/pkg/util/jsonpath" + "k8s.io/kubernetes/pkg/util/yaml" ) func init() { @@ -39,11 +46,22 @@ type gcpAuthProvider struct { } func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) { - ts, err := newCachedTokenSource(gcpConfig["access-token"], gcpConfig["expiry"], persister) + cmd, useCmd := gcpConfig["cmd-path"] + var ts oauth2.TokenSource + var err error + if useCmd { + ts, err = newCmdTokenSource(cmd, gcpConfig["token-key"], gcpConfig["expiry-key"], gcpConfig["time-fmt"]) + } else { + ts, err = google.DefaultTokenSource(context.Background(), "https://www.googleapis.com/auth/cloud-platform") + } if err != nil { return nil, err } - return &gcpAuthProvider{ts, persister}, nil + cts, err := newCachedTokenSource(gcpConfig["access-token"], gcpConfig["expiry"], persister, ts, gcpConfig) + if err != nil { + return nil, err + } + return &gcpAuthProvider{cts, persister}, nil } func (g *gcpAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper { @@ -60,22 +78,23 @@ type cachedTokenSource struct { accessToken string expiry time.Time persister restclient.AuthProviderConfigPersister + cache map[string]string } -func newCachedTokenSource(accessToken, expiry string, persister restclient.AuthProviderConfigPersister) (*cachedTokenSource, error) { +func newCachedTokenSource(accessToken, expiry string, persister restclient.AuthProviderConfigPersister, ts oauth2.TokenSource, cache map[string]string) (*cachedTokenSource, error) { var expiryTime time.Time if parsedTime, err := time.Parse(time.RFC3339Nano, expiry); err == nil { expiryTime = parsedTime } - ts, err := google.DefaultTokenSource(context.Background(), "https://www.googleapis.com/auth/cloud-platform") - if err != nil { - return nil, err + if cache == nil { + cache = make(map[string]string) } return &cachedTokenSource{ source: ts, accessToken: accessToken, expiry: expiryTime, persister: persister, + cache: cache, }, nil } @@ -93,13 +112,100 @@ func (t *cachedTokenSource) Token() (*oauth2.Token, error) { return nil, err } if t.persister != nil { - cached := map[string]string{ - "access-token": tok.AccessToken, - "expiry": tok.Expiry.Format(time.RFC3339Nano), - } - if err := t.persister.Persist(cached); err != nil { + t.cache["access-token"] = tok.AccessToken + t.cache["expiry"] = tok.Expiry.Format(time.RFC3339Nano) + if err := t.persister.Persist(t.cache); err != nil { glog.V(4).Infof("Failed to persist token: %v", err) } } return tok, nil } + +type commandTokenSource struct { + cmd string + args []string + tokenKey string + expiryKey string + timeFmt string +} + +func newCmdTokenSource(cmd, tokenKey, expiryKey, timeFmt string) (*commandTokenSource, error) { + if len(timeFmt) == 0 { + timeFmt = time.RFC3339Nano + } + if len(tokenKey) == 0 { + tokenKey = "{.access_token}" + } + if len(expiryKey) == 0 { + expiryKey = "{.token_expiry}" + } + fields := strings.Fields(cmd) + if len(fields) == 0 { + return nil, fmt.Errorf("missing access token cmd") + } + return &commandTokenSource{ + cmd: fields[0], + args: fields[1:], + tokenKey: tokenKey, + expiryKey: expiryKey, + timeFmt: timeFmt, + }, nil +} + +func (c *commandTokenSource) Token() (*oauth2.Token, error) { + fullCmd := fmt.Sprintf("%s %s", c.cmd, strings.Join(c.args, " ")) + cmd := exec.Command(c.cmd, c.args...) + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("error executing access token command %q: %v", fullCmd, err) + } + token, err := c.parseTokenCmdOutput(output) + if err != nil { + return nil, fmt.Errorf("error parsing output for access token command %q: %v", fullCmd, err) + } + return token, nil +} + +func (c *commandTokenSource) parseTokenCmdOutput(output []byte) (*oauth2.Token, error) { + output, err := yaml.ToJSON(output) + if err != nil { + return nil, err + } + var data interface{} + if err := json.Unmarshal(output, &data); err != nil { + return nil, err + } + + accessToken, err := parseJSONPath(data, "token-key", c.tokenKey) + if err != nil { + return nil, fmt.Errorf("error parsing token-key %q: %v", c.tokenKey, err) + } + expiryStr, err := parseJSONPath(data, "expiry-key", c.expiryKey) + if err != nil { + return nil, fmt.Errorf("error parsing expiry-key %q: %v", c.expiryKey, err) + } + var expiry time.Time + if t, err := time.Parse(c.timeFmt, expiryStr); err != nil { + glog.V(4).Infof("Failed to parse token expiry from %s (fmt=%s): %v", expiryStr, c.timeFmt, err) + } else { + expiry = t + } + + return &oauth2.Token{ + AccessToken: accessToken, + TokenType: "Bearer", + Expiry: expiry, + }, nil +} + +func parseJSONPath(input interface{}, name, template string) (string, error) { + j := jsonpath.New(name) + buf := new(bytes.Buffer) + if err := j.Parse(template); err != nil { + return "", err + } + if err := j.Execute(buf, input); err != nil { + return "", err + } + return buf.String(), nil +} diff --git a/plugin/pkg/client/auth/gcp/gcp_test.go b/plugin/pkg/client/auth/gcp/gcp_test.go new file mode 100644 index 00000000000..dfd25bbf1b7 --- /dev/null +++ b/plugin/pkg/client/auth/gcp/gcp_test.go @@ -0,0 +1,143 @@ +/* +Copyright 2016 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package gcp + +import ( + "fmt" + "reflect" + "strings" + "testing" + "time" + + "golang.org/x/oauth2" +) + +func TestCmdTokenSource(t *testing.T) { + fakeExpiry := time.Date(2016, 10, 31, 22, 31, 9, 123000000, time.UTC) + customFmt := "2006-01-02 15:04:05.999999999" + + tests := []struct { + name string + output []byte + cmd, tokenKey, expiryKey, timeFmt string + tok *oauth2.Token + expectErr error + }{ + { + "defaults", + []byte(`{ + "access_token": "faketoken", + "token_expiry": "2016-10-31T22:31:09.123000000Z" +}`), + "/fake/cmd/path", "", "", "", + &oauth2.Token{ + AccessToken: "faketoken", + TokenType: "Bearer", + Expiry: fakeExpiry, + }, + nil, + }, + { + "custom keys", + []byte(`{ + "token": "faketoken", + "token_expiry": { + "datetime": "2016-10-31 22:31:09.123" + } +}`), + "/fake/cmd/path", "{.token}", "{.token_expiry.datetime}", customFmt, + &oauth2.Token{ + AccessToken: "faketoken", + TokenType: "Bearer", + Expiry: fakeExpiry, + }, + nil, + }, + { + "missing cmd", + nil, + "", "", "", "", + nil, + fmt.Errorf("missing access token cmd"), + }, + { + "missing token-key", + []byte(`{ + "broken": "faketoken", + "token_expiry": { + "datetime": "2016-10-31 22:31:09.123000000Z" + } +}`), + "/fake/cmd/path", "{.token}", "", "", + nil, + fmt.Errorf("error parsing token-key %q", "{.token}"), + }, + { + "missing expiry-key", + []byte(`{ + "access_token": "faketoken", + "expires": "2016-10-31T22:31:09.123000000Z" +}`), + "/fake/cmd/path", "", "{.expiry}", "", + nil, + fmt.Errorf("error parsing expiry-key %q", "{.expiry}"), + }, + { + "invalid expiry timestamp", + []byte(`{ + "access_token": "faketoken", + "token_expiry": "sometime soon, idk" +}`), + "/fake/cmd/path", "", "", "", + &oauth2.Token{ + AccessToken: "faketoken", + TokenType: "Bearer", + Expiry: time.Time{}, + }, + nil, + }, + { + "bad JSON", + []byte(`{ + "access_token": "faketoken", + "token_expiry": "sometime soon, idk" + ------ +`), + "/fake/cmd", "", "", "", + nil, + fmt.Errorf("invalid character '-' after object key:value pair"), + }, + } + + for _, tc := range tests { + ts, err := newCmdTokenSource(tc.cmd, tc.tokenKey, tc.expiryKey, tc.timeFmt) + if err != nil { + if !strings.Contains(err.Error(), tc.expectErr.Error()) { + t.Errorf("%s newCmdTokenSource error: %v, want %v", tc.name, err, tc.expectErr) + } + continue + } + tok, err := ts.parseTokenCmdOutput(tc.output) + + if err != tc.expectErr && !strings.Contains(err.Error(), tc.expectErr.Error()) { + t.Errorf("%s parseCmdTokenSource error: %v, want %v", tc.name, err, tc.expectErr) + } + if !reflect.DeepEqual(tok, tc.tok) { + t.Errorf("%s got token %v, want %v", tc.name, tok, tc.tok) + } + } +} diff --git a/test/test_owners.csv b/test/test_owners.csv index fe2c129d709..f31bebf82d3 100644 --- a/test/test_owners.csv +++ b/test/test_owners.csv @@ -859,6 +859,7 @@ k8s.io/kubernetes/plugin/pkg/auth/authenticator/token/webhook,ghodss,1 k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac,hurf,1 k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/bootstrappolicy,mml,1 k8s.io/kubernetes/plugin/pkg/auth/authorizer/webhook,hurf,1 +k8s.io/kubernetes/plugin/pkg/client/auth/gcp,jlowdermilk,0 k8s.io/kubernetes/plugin/pkg/client/auth/oidc,cjcullen,1 k8s.io/kubernetes/plugin/pkg/scheduler,fgrzadkowski,0 k8s.io/kubernetes/plugin/pkg/scheduler/algorithm/predicates,fgrzadkowski,0