diff --git a/pkg/credentialprovider/gcp/jwt.go b/pkg/credentialprovider/gcp/jwt.go new file mode 100644 index 00000000000..3cf7a7c71bf --- /dev/null +++ b/pkg/credentialprovider/gcp/jwt.go @@ -0,0 +1,111 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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_credentials + +import ( + "io/ioutil" + "time" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider" + "github.com/golang/glog" + "golang.org/x/oauth2" + "golang.org/x/oauth2/google" + "golang.org/x/oauth2/jwt" + + "github.com/spf13/pflag" +) + +const ( + storageReadOnlyScope = "https://www.googleapis.com/auth/devstorage.read_only" +) + +var ( + flagJwtFile = pflag.String("google_json_key", "", + "The Google Cloud Platform Service Account JSON Key to use for authentication.") +) + +// A DockerConfigProvider that reads its configuration from Google +// Compute Engine metadata. +type jwtProvider struct { + path *string + config *jwt.Config + tokenUrl string +} + +// init registers the various means by which credentials may +// be resolved on GCP. +func init() { + credentialprovider.RegisterCredentialProvider("google-jwt-key", + &credentialprovider.CachingDockerConfigProvider{ + Provider: &jwtProvider{ + path: flagJwtFile, + }, + Lifetime: 30 * time.Minute, + }) +} + +// Enabled implements DockerConfigProvider for the JSON Key based implementation. +func (j *jwtProvider) Enabled() bool { + if *j.path == "" { + return false + } + + data, err := ioutil.ReadFile(*j.path) + if err != nil { + glog.Errorf("while reading file %s got %v", *j.path, err) + return false + } + config, err := google.JWTConfigFromJSON(data, storageReadOnlyScope) + if err != nil { + glog.Errorf("while parsing %s data got %v", *j.path, err) + return false + } + + j.config = config + if j.tokenUrl != "" { + j.config.TokenURL = j.tokenUrl + } + return true +} + +// Provide implements DockerConfigProvider +func (j *jwtProvider) Provide() credentialprovider.DockerConfig { + cfg := credentialprovider.DockerConfig{} + + ts := j.config.TokenSource(oauth2.NoContext) + token, err := ts.Token() + if err != nil { + glog.Errorf("while exchanging json key %s for access token %v", *j.path, err) + return cfg + } + if !token.Valid() { + glog.Errorf("Got back invalid token: %v", token) + return cfg + } + + entry := credentialprovider.DockerConfigEntry{ + Username: "_token", + Password: token.AccessToken, + Email: j.config.Email, + } + + // Add our entry for each of the supported container registry URLs + for _, k := range containerRegistryUrls { + cfg[k] = entry + } + return cfg +} diff --git a/pkg/credentialprovider/gcp/jwt_test.go b/pkg/credentialprovider/gcp/jwt_test.go new file mode 100644 index 00000000000..fe44f9551fc --- /dev/null +++ b/pkg/credentialprovider/gcp/jwt_test.go @@ -0,0 +1,121 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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_credentials + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/credentialprovider" +) + +const email = "foo@bar.com" + +// From oauth2/jwt_test.go +var ( + dummyPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAx4fm7dngEmOULNmAs1IGZ9Apfzh+BkaQ1dzkmbUgpcoghucE +DZRnAGd2aPyB6skGMXUytWQvNYav0WTR00wFtX1ohWTfv68HGXJ8QXCpyoSKSSFY +fuP9X36wBSkSX9J5DVgiuzD5VBdzUISSmapjKm+DcbRALjz6OUIPEWi1Tjl6p5RK +1w41qdbmt7E5/kGhKLDuT7+M83g4VWhgIvaAXtnhklDAggilPPa8ZJ1IFe31lNlr +k4DRk38nc6sEutdf3RL7QoH7FBusI7uXV03DC6dwN1kP4GE7bjJhcRb/7jYt7CQ9 +/E9Exz3c0yAp0yrTg0Fwh+qxfH9dKwN52S7SBwIDAQABAoIBAQCaCs26K07WY5Jt +3a2Cw3y2gPrIgTCqX6hJs7O5ByEhXZ8nBwsWANBUe4vrGaajQHdLj5OKfsIDrOvn +2NI1MqflqeAbu/kR32q3tq8/Rl+PPiwUsW3E6Pcf1orGMSNCXxeducF2iySySzh3 +nSIhCG5uwJDWI7a4+9KiieFgK1pt/Iv30q1SQS8IEntTfXYwANQrfKUVMmVF9aIK +6/WZE2yd5+q3wVVIJ6jsmTzoDCX6QQkkJICIYwCkglmVy5AeTckOVwcXL0jqw5Kf +5/soZJQwLEyBoQq7Kbpa26QHq+CJONetPP8Ssy8MJJXBT+u/bSseMb3Zsr5cr43e +DJOhwsThAoGBAPY6rPKl2NT/K7XfRCGm1sbWjUQyDShscwuWJ5+kD0yudnT/ZEJ1 +M3+KS/iOOAoHDdEDi9crRvMl0UfNa8MAcDKHflzxg2jg/QI+fTBjPP5GOX0lkZ9g +z6VePoVoQw2gpPFVNPPTxKfk27tEzbaffvOLGBEih0Kb7HTINkW8rIlzAoGBAM9y +1yr+jvfS1cGFtNU+Gotoihw2eMKtIqR03Yn3n0PK1nVCDKqwdUqCypz4+ml6cxRK +J8+Pfdh7D+ZJd4LEG6Y4QRDLuv5OA700tUoSHxMSNn3q9As4+T3MUyYxWKvTeu3U +f2NWP9ePU0lV8ttk7YlpVRaPQmc1qwooBA/z/8AdAoGAW9x0HWqmRICWTBnpjyxx +QGlW9rQ9mHEtUotIaRSJ6K/F3cxSGUEkX1a3FRnp6kPLcckC6NlqdNgNBd6rb2rA +cPl/uSkZP42Als+9YMoFPU/xrrDPbUhu72EDrj3Bllnyb168jKLa4VBOccUvggxr +Dm08I1hgYgdN5huzs7y6GeUCgYEAj+AZJSOJ6o1aXS6rfV3mMRve9bQ9yt8jcKXw +5HhOCEmMtaSKfnOF1Ziih34Sxsb7O2428DiX0mV/YHtBnPsAJidL0SdLWIapBzeg +KHArByIRkwE6IvJvwpGMdaex1PIGhx5i/3VZL9qiq/ElT05PhIb+UXgoWMabCp84 +OgxDK20CgYAeaFo8BdQ7FmVX2+EEejF+8xSge6WVLtkaon8bqcn6P0O8lLypoOhd +mJAYH8WU+UAy9pecUnDZj14LAGNVmYcse8HFX71MoshnvCTFEPVo4rZxIAGwMpeJ +5jgQ3slYLpqrGlcbLgUXBUgzEO684Wk/UV9DFPlHALVqCfXQ9dpJPg== +-----END RSA PRIVATE KEY-----` + + jsonKey = fmt.Sprintf(`{"private_key":"%[1]s", "client_email":"%[2]s"}`, + strings.Replace(dummyPrivateKey, "\n", "\\n", -1), email) +) + +func TestJwtProvider(t *testing.T) { + token := "asdhflkjsdfkjhsdf" + + // Modeled after oauth2/jwt_test.go + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(fmt.Sprintf(`{ + "access_token": "%[1]s", + "scope": "user", + "token_type": "bearer", + "expires_in": 3600 + }`, token))) + })) + defer ts.Close() + + file, err := ioutil.TempFile(os.TempDir(), "temp") + if err != nil { + t.Fatalf("Error creating temp file: %v", err) + } + + filename := file.Name() + _, err = file.WriteString(jsonKey) + if err != nil { + t.Fatalf("Error writing temp file: %v", err) + } + + provider := &jwtProvider{ + path: &filename, + tokenUrl: ts.URL, + } + if !provider.Enabled() { + t.Fatalf("Provider is unexpectedly disabled") + } + + keyring := &credentialprovider.BasicDockerKeyring{} + keyring.Add(provider.Provide()) + + // Verify that we get the expected username/password combo for + // a gcr.io image name. + registryUrl := "gcr.io/foo/bar" + val, ok := keyring.Lookup(registryUrl) + if !ok { + t.Errorf("Didn't find expected URL: %s", registryUrl) + } + + if "_token" != val.Username { + t.Errorf("Unexpected username value, want: _token, got: %s", val.Username) + } + if token != val.Password { + t.Errorf("Unexpected password value, want: %s, got: %s", token, val.Password) + } + if email != val.Email { + t.Errorf("Unexpected email value, want: %s, got: %s", email, val.Email) + } +}