Merge pull request #36012 from jlowdermilk/cmd-auth-provider

Automatic merge from submit-queue

Add cmd support to gcp auth provider plugin

**What this PR does / why we need it**:

Adds ability for gcp auth provider plugin to get access token by shelling out to an external command. We need this because for GKE, kubectl should be using gcloud credentials. It currently uses google application default credentials, which causes confusion if user has configured both with different permissions (previously the two were almost always identical).

**Which issue this PR fixes**:
Addresses #35530 with gcp-only solution, as generic cmd plugin was deemed not useful for other providers.

**Special notes for your reviewer**:

Configuration options are to support whatever future command gcloud provides for printing access token of active user. Also works with existing command (`gcloud auth print-access-token`)

```release-note
```
This commit is contained in:
Kubernetes Submit Queue 2016-11-06 01:45:48 -08:00 committed by GitHub
commit 741ef71fa9
5 changed files with 274 additions and 11 deletions

View File

@ -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"],
)

View File

@ -0,0 +1,3 @@
assignees:
- cjcullen
- jlowdermilk

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -875,6 +875,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

1 name owner auto-assigned
875 k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac hurf 1
876 k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac/bootstrappolicy mml 1
877 k8s.io/kubernetes/plugin/pkg/auth/authorizer/webhook hurf 1
878 k8s.io/kubernetes/plugin/pkg/client/auth/gcp jlowdermilk 0
879 k8s.io/kubernetes/plugin/pkg/client/auth/oidc cjcullen 1
880 k8s.io/kubernetes/plugin/pkg/scheduler fgrzadkowski 0
881 k8s.io/kubernetes/plugin/pkg/scheduler/algorithm/predicates fgrzadkowski 0