1/* 2Copyright 2016 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package gcp 18 19import ( 20 "bytes" 21 "context" 22 "encoding/json" 23 "fmt" 24 "net/http" 25 "os/exec" 26 "strings" 27 "sync" 28 "time" 29 30 "github.com/golang/glog" 31 "golang.org/x/oauth2" 32 "golang.org/x/oauth2/google" 33 "k8s.io/apimachinery/pkg/util/net" 34 "k8s.io/apimachinery/pkg/util/yaml" 35 restclient "k8s.io/client-go/rest" 36 "k8s.io/client-go/util/jsonpath" 37) 38 39func init() { 40 if err := restclient.RegisterAuthProviderPlugin("gcp", newGCPAuthProvider); err != nil { 41 glog.Fatalf("Failed to register gcp auth plugin: %v", err) 42 } 43} 44 45var ( 46 // Stubbable for testing 47 execCommand = exec.Command 48 49 // defaultScopes: 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"} 56) 57 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. 61// 62// { 63// 'auth-provider': { 64// # Required 65// "name": "gcp", 66// 67// 'config': { 68// # Authentication options 69// # These options are used while getting a token. 70// 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" 75// 76// # Caching options 77// 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", 82// 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. 86// 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", 91// 92// # Arguments to pass to command to execute for access token. 93// "cmd-args": "config config-helper --output=json" 94// 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}", 98// 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}", 103// 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" 107// } 108// } 109// } 110// 111type gcpAuthProvider struct { 112 tokenSource oauth2.TokenSource 113 persister restclient.AuthProviderConfigPersister 114} 115 116func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) { 117 ts, err := tokenSource(isCmdTokenSource(gcpConfig), gcpConfig) 118 if err != nil { 119 return nil, err 120 } 121 cts, err := newCachedTokenSource(gcpConfig["access-token"], gcpConfig["expiry"], persister, ts, gcpConfig) 122 if err != nil { 123 return nil, err 124 } 125 return &gcpAuthProvider{cts, persister}, nil 126} 127 128func isCmdTokenSource(gcpConfig map[string]string) bool { 129 _, ok := gcpConfig["cmd-path"] 130 return ok 131} 132 133func tokenSource(isCmd bool, gcpConfig map[string]string) (oauth2.TokenSource, error) { 134 // Command-based token source 135 if isCmd { 136 cmd := gcpConfig["cmd-path"] 137 if len(cmd) == 0 { 138 return nil, fmt.Errorf("missing access token cmd") 139 } 140 if gcpConfig["scopes"] != "" { 141 return nil, fmt.Errorf("scopes can only be used when kubectl is using a gcp service account key") 142 } 143 var args []string 144 if cmdArgs, ok := gcpConfig["cmd-args"]; ok { 145 args = strings.Fields(cmdArgs) 146 } else { 147 fields := strings.Fields(cmd) 148 cmd = fields[0] 149 args = fields[1:] 150 } 151 return newCmdTokenSource(cmd, args, gcpConfig["token-key"], gcpConfig["expiry-key"], gcpConfig["time-fmt"]), nil 152 } 153 154 // Google Application Credentials-based token source 155 scopes := parseScopes(gcpConfig) 156 ts, err := google.DefaultTokenSource(context.Background(), scopes...) 157 if err != nil { 158 return nil, fmt.Errorf("cannot construct google default token source: %v", err) 159 } 160 return ts, nil 161} 162 163// parseScopes constructs a list of scopes that should be included in token source 164// from the config map. 165func parseScopes(gcpConfig map[string]string) []string { 166 scopes, ok := gcpConfig["scopes"] 167 if !ok { 168 return defaultScopes 169 } 170 if scopes == "" { 171 return []string{} 172 } 173 return strings.Split(gcpConfig["scopes"], ",") 174} 175 176func (g *gcpAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper { 177 return &conditionalTransport{&oauth2.Transport{Source: g.tokenSource, Base: rt}, g.persister} 178} 179 180func (g *gcpAuthProvider) Login() error { return nil } 181 182type cachedTokenSource struct { 183 lk sync.Mutex 184 source oauth2.TokenSource 185 accessToken string 186 expiry time.Time 187 persister restclient.AuthProviderConfigPersister 188 cache map[string]string 189} 190 191func newCachedTokenSource(accessToken, expiry string, persister restclient.AuthProviderConfigPersister, ts oauth2.TokenSource, cache map[string]string) (*cachedTokenSource, error) { 192 var expiryTime time.Time 193 if parsedTime, err := time.Parse(time.RFC3339Nano, expiry); err == nil { 194 expiryTime = parsedTime 195 } 196 if cache == nil { 197 cache = make(map[string]string) 198 } 199 return &cachedTokenSource{ 200 source: ts, 201 accessToken: accessToken, 202 expiry: expiryTime, 203 persister: persister, 204 cache: cache, 205 }, nil 206} 207 208func (t *cachedTokenSource) Token() (*oauth2.Token, error) { 209 tok := t.cachedToken() 210 if tok.Valid() && !tok.Expiry.IsZero() { 211 return tok, nil 212 } 213 tok, err := t.source.Token() 214 if err != nil { 215 return nil, err 216 } 217 cache := t.update(tok) 218 if t.persister != nil { 219 if err := t.persister.Persist(cache); err != nil { 220 glog.V(4).Infof("Failed to persist token: %v", err) 221 } 222 } 223 return tok, nil 224} 225 226func (t *cachedTokenSource) cachedToken() *oauth2.Token { 227 t.lk.Lock() 228 defer t.lk.Unlock() 229 return &oauth2.Token{ 230 AccessToken: t.accessToken, 231 TokenType: "Bearer", 232 Expiry: t.expiry, 233 } 234} 235 236func (t *cachedTokenSource) update(tok *oauth2.Token) map[string]string { 237 t.lk.Lock() 238 defer t.lk.Unlock() 239 t.accessToken = tok.AccessToken 240 t.expiry = tok.Expiry 241 ret := map[string]string{} 242 for k, v := range t.cache { 243 ret[k] = v 244 } 245 ret["access-token"] = t.accessToken 246 ret["expiry"] = t.expiry.Format(time.RFC3339Nano) 247 return ret 248} 249 250type commandTokenSource struct { 251 cmd string 252 args []string 253 tokenKey string 254 expiryKey string 255 timeFmt string 256} 257 258func newCmdTokenSource(cmd string, args []string, tokenKey, expiryKey, timeFmt string) *commandTokenSource { 259 if len(timeFmt) == 0 { 260 timeFmt = time.RFC3339Nano 261 } 262 if len(tokenKey) == 0 { 263 tokenKey = "{.access_token}" 264 } 265 if len(expiryKey) == 0 { 266 expiryKey = "{.token_expiry}" 267 } 268 return &commandTokenSource{ 269 cmd: cmd, 270 args: args, 271 tokenKey: tokenKey, 272 expiryKey: expiryKey, 273 timeFmt: timeFmt, 274 } 275} 276 277func (c *commandTokenSource) Token() (*oauth2.Token, error) { 278 fullCmd := strings.Join(append([]string{c.cmd}, c.args...), " ") 279 cmd := execCommand(c.cmd, c.args...) 280 var stderr bytes.Buffer 281 cmd.Stderr = &stderr 282 output, err := cmd.Output() 283 if err != nil { 284 return nil, fmt.Errorf("error executing access token command %q: err=%v output=%s stderr=%s", fullCmd, err, output, string(stderr.Bytes())) 285 } 286 token, err := c.parseTokenCmdOutput(output) 287 if err != nil { 288 return nil, fmt.Errorf("error parsing output for access token command %q: %v", fullCmd, err) 289 } 290 return token, nil 291} 292 293func (c *commandTokenSource) parseTokenCmdOutput(output []byte) (*oauth2.Token, error) { 294 output, err := yaml.ToJSON(output) 295 if err != nil { 296 return nil, err 297 } 298 var data interface{} 299 if err := json.Unmarshal(output, &data); err != nil { 300 return nil, err 301 } 302 303 accessToken, err := parseJSONPath(data, "token-key", c.tokenKey) 304 if err != nil { 305 return nil, fmt.Errorf("error parsing token-key %q from %q: %v", c.tokenKey, string(output), err) 306 } 307 expiryStr, err := parseJSONPath(data, "expiry-key", c.expiryKey) 308 if err != nil { 309 return nil, fmt.Errorf("error parsing expiry-key %q from %q: %v", c.expiryKey, string(output), err) 310 } 311 var expiry time.Time 312 if t, err := time.Parse(c.timeFmt, expiryStr); err != nil { 313 glog.V(4).Infof("Failed to parse token expiry from %s (fmt=%s): %v", expiryStr, c.timeFmt, err) 314 } else { 315 expiry = t 316 } 317 318 return &oauth2.Token{ 319 AccessToken: accessToken, 320 TokenType: "Bearer", 321 Expiry: expiry, 322 }, nil 323} 324 325func parseJSONPath(input interface{}, name, template string) (string, error) { 326 j := jsonpath.New(name) 327 buf := new(bytes.Buffer) 328 if err := j.Parse(template); err != nil { 329 return "", err 330 } 331 if err := j.Execute(buf, input); err != nil { 332 return "", err 333 } 334 return buf.String(), nil 335} 336 337type conditionalTransport struct { 338 oauthTransport *oauth2.Transport 339 persister restclient.AuthProviderConfigPersister 340} 341 342var _ net.RoundTripperWrapper = &conditionalTransport{} 343 344func (t *conditionalTransport) RoundTrip(req *http.Request) (*http.Response, error) { 345 if len(req.Header.Get("Authorization")) != 0 { 346 return t.oauthTransport.Base.RoundTrip(req) 347 } 348 349 res, err := t.oauthTransport.RoundTrip(req) 350 351 if err != nil { 352 return nil, err 353 } 354 355 if res.StatusCode == 401 { 356 glog.V(4).Infof("The credentials that were supplied are invalid for the target cluster") 357 emptyCache := make(map[string]string) 358 t.persister.Persist(emptyCache) 359 } 360 361 return res, nil 362} 363 364func (t *conditionalTransport) WrappedRoundTripper() http.RoundTripper { return t.oauthTransport.Base } 365