2 Copyright 2016 The Kubernetes Authors.
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
8 http://www.apache.org/licenses/LICENSE-2.0
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.
31 "golang.org/x/oauth2/google"
32 "k8s.io/apimachinery/pkg/util/net"
33 "k8s.io/apimachinery/pkg/util/yaml"
34 restclient "k8s.io/client-go/rest"
35 "k8s.io/client-go/util/jsonpath"
40 if err := restclient.RegisterAuthProviderPlugin("gcp", newGCPAuthProvider); err != nil {
41 klog.Fatalf("Failed to register gcp auth plugin: %v", err)
46 // Stubbable for testing
47 execCommand = exec.Command
50 // - cloud-platform is the base scope to authenticate to GCP.
51 // - userinfo.email is used to authenticate to GKE APIs with gserviceaccount
52 // email instead of numeric uniqueID.
53 defaultScopes = []string{
54 "https://www.googleapis.com/auth/cloud-platform",
55 "https://www.googleapis.com/auth/userinfo.email"}
58 // gcpAuthProvider is an auth provider plugin that uses GCP credentials to provide
59 // tokens for kubectl to authenticate itself to the apiserver. A sample json config
60 // is provided below with all recognized options described.
68 // # Authentication options
69 // # These options are used while getting a token.
71 // # comma-separated list of GCP API scopes. default value of this field
72 // # is "https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/userinfo.email".
73 // # to override the API scopes, specify this field explicitly.
74 // "scopes": "https://www.googleapis.com/auth/cloud-platform"
78 // # Raw string data representing cached access token.
79 // "access-token": "ya29.CjWdA4GiBPTt",
80 // # RFC3339Nano expiration timestamp for cached access token.
81 // "expiry": "2016-10-31 22:31:9.123",
83 // # Command execution options
84 // # These options direct the plugin to execute a specified command and parse
85 // # token and expiry time from the output of the command.
87 // # Command to execute for access token. Command output will be parsed as JSON.
88 // # If "cmd-args" is not present, this value will be split on whitespace, with
89 // # the first element interpreted as the command, remaining elements as args.
90 // "cmd-path": "/usr/bin/gcloud",
92 // # Arguments to pass to command to execute for access token.
93 // "cmd-args": "config config-helper --output=json"
95 // # JSONPath to the string field that represents the access token in
96 // # command output. If omitted, defaults to "{.access_token}".
97 // "token-key": "{.credential.access_token}",
99 // # JSONPath to the string field that represents expiration timestamp
100 // # of the access token in the command output. If omitted, defaults to
101 // # "{.token_expiry}"
102 // "expiry-key": ""{.credential.token_expiry}",
104 // # golang reference time in the format that the expiration timestamp uses.
105 // # If omitted, defaults to time.RFC3339Nano
106 // "time-fmt": "2006-01-02 15:04:05.999999999"
111 type gcpAuthProvider struct {
112 tokenSource oauth2.TokenSource
113 persister restclient.AuthProviderConfigPersister
116 func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
117 ts, err := tokenSource(isCmdTokenSource(gcpConfig), gcpConfig)
121 cts, err := newCachedTokenSource(gcpConfig["access-token"], gcpConfig["expiry"], persister, ts, gcpConfig)
125 return &gcpAuthProvider{cts, persister}, nil
128 func isCmdTokenSource(gcpConfig map[string]string) bool {
129 _, ok := gcpConfig["cmd-path"]
133 func tokenSource(isCmd bool, gcpConfig map[string]string) (oauth2.TokenSource, error) {
134 // Command-based token source
136 cmd := gcpConfig["cmd-path"]
138 return nil, fmt.Errorf("missing access token cmd")
140 if gcpConfig["scopes"] != "" {
141 return nil, fmt.Errorf("scopes can only be used when kubectl is using a gcp service account key")
144 if cmdArgs, ok := gcpConfig["cmd-args"]; ok {
145 args = strings.Fields(cmdArgs)
147 fields := strings.Fields(cmd)
151 return newCmdTokenSource(cmd, args, gcpConfig["token-key"], gcpConfig["expiry-key"], gcpConfig["time-fmt"]), nil
154 // Google Application Credentials-based token source
155 scopes := parseScopes(gcpConfig)
156 ts, err := google.DefaultTokenSource(context.Background(), scopes...)
158 return nil, fmt.Errorf("cannot construct google default token source: %v", err)
163 // parseScopes constructs a list of scopes that should be included in token source
164 // from the config map.
165 func parseScopes(gcpConfig map[string]string) []string {
166 scopes, ok := gcpConfig["scopes"]
173 return strings.Split(gcpConfig["scopes"], ",")
176 func (g *gcpAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
177 var resetCache map[string]string
178 if cts, ok := g.tokenSource.(*cachedTokenSource); ok {
179 resetCache = cts.baseCache()
181 resetCache = make(map[string]string)
183 return &conditionalTransport{&oauth2.Transport{Source: g.tokenSource, Base: rt}, g.persister, resetCache}
186 func (g *gcpAuthProvider) Login() error { return nil }
188 type cachedTokenSource struct {
190 source oauth2.TokenSource
193 persister restclient.AuthProviderConfigPersister
194 cache map[string]string
197 func newCachedTokenSource(accessToken, expiry string, persister restclient.AuthProviderConfigPersister, ts oauth2.TokenSource, cache map[string]string) (*cachedTokenSource, error) {
198 var expiryTime time.Time
199 if parsedTime, err := time.Parse(time.RFC3339Nano, expiry); err == nil {
200 expiryTime = parsedTime
203 cache = make(map[string]string)
205 return &cachedTokenSource{
207 accessToken: accessToken,
209 persister: persister,
214 func (t *cachedTokenSource) Token() (*oauth2.Token, error) {
215 tok := t.cachedToken()
216 if tok.Valid() && !tok.Expiry.IsZero() {
219 tok, err := t.source.Token()
223 cache := t.update(tok)
224 if t.persister != nil {
225 if err := t.persister.Persist(cache); err != nil {
226 klog.V(4).Infof("Failed to persist token: %v", err)
232 func (t *cachedTokenSource) cachedToken() *oauth2.Token {
235 return &oauth2.Token{
236 AccessToken: t.accessToken,
242 func (t *cachedTokenSource) update(tok *oauth2.Token) map[string]string {
245 t.accessToken = tok.AccessToken
246 t.expiry = tok.Expiry
247 ret := map[string]string{}
248 for k, v := range t.cache {
251 ret["access-token"] = t.accessToken
252 ret["expiry"] = t.expiry.Format(time.RFC3339Nano)
256 // baseCache is the base configuration value for this TokenSource, without any cached ephemeral tokens.
257 func (t *cachedTokenSource) baseCache() map[string]string {
260 ret := map[string]string{}
261 for k, v := range t.cache {
264 delete(ret, "access-token")
265 delete(ret, "expiry")
269 type commandTokenSource struct {
277 func newCmdTokenSource(cmd string, args []string, tokenKey, expiryKey, timeFmt string) *commandTokenSource {
278 if len(timeFmt) == 0 {
279 timeFmt = time.RFC3339Nano
281 if len(tokenKey) == 0 {
282 tokenKey = "{.access_token}"
284 if len(expiryKey) == 0 {
285 expiryKey = "{.token_expiry}"
287 return &commandTokenSource{
291 expiryKey: expiryKey,
296 func (c *commandTokenSource) Token() (*oauth2.Token, error) {
297 fullCmd := strings.Join(append([]string{c.cmd}, c.args...), " ")
298 cmd := execCommand(c.cmd, c.args...)
299 var stderr bytes.Buffer
301 output, err := cmd.Output()
303 return nil, fmt.Errorf("error executing access token command %q: err=%v output=%s stderr=%s", fullCmd, err, output, string(stderr.Bytes()))
305 token, err := c.parseTokenCmdOutput(output)
307 return nil, fmt.Errorf("error parsing output for access token command %q: %v", fullCmd, err)
312 func (c *commandTokenSource) parseTokenCmdOutput(output []byte) (*oauth2.Token, error) {
313 output, err := yaml.ToJSON(output)
318 if err := json.Unmarshal(output, &data); err != nil {
322 accessToken, err := parseJSONPath(data, "token-key", c.tokenKey)
324 return nil, fmt.Errorf("error parsing token-key %q from %q: %v", c.tokenKey, string(output), err)
326 expiryStr, err := parseJSONPath(data, "expiry-key", c.expiryKey)
328 return nil, fmt.Errorf("error parsing expiry-key %q from %q: %v", c.expiryKey, string(output), err)
331 if t, err := time.Parse(c.timeFmt, expiryStr); err != nil {
332 klog.V(4).Infof("Failed to parse token expiry from %s (fmt=%s): %v", expiryStr, c.timeFmt, err)
337 return &oauth2.Token{
338 AccessToken: accessToken,
344 func parseJSONPath(input interface{}, name, template string) (string, error) {
345 j := jsonpath.New(name)
346 buf := new(bytes.Buffer)
347 if err := j.Parse(template); err != nil {
350 if err := j.Execute(buf, input); err != nil {
353 return buf.String(), nil
356 type conditionalTransport struct {
357 oauthTransport *oauth2.Transport
358 persister restclient.AuthProviderConfigPersister
359 resetCache map[string]string
362 var _ net.RoundTripperWrapper = &conditionalTransport{}
364 func (t *conditionalTransport) RoundTrip(req *http.Request) (*http.Response, error) {
365 if len(req.Header.Get("Authorization")) != 0 {
366 return t.oauthTransport.Base.RoundTrip(req)
369 res, err := t.oauthTransport.RoundTrip(req)
375 if res.StatusCode == 401 {
376 klog.V(4).Infof("The credentials that were supplied are invalid for the target cluster")
377 t.persister.Persist(t.resetCache)
383 func (t *conditionalTransport) WrappedRoundTripper() http.RoundTripper { return t.oauthTransport.Base }