Updated vednor files

This commit is contained in:
Serguei Bezverkhi
2018-02-15 08:50:31 -05:00
parent 18a4ce4439
commit 1f1e8cea37
3299 changed files with 834 additions and 1051200 deletions

View File

@ -1,30 +0,0 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["provider.go"],
importpath = "k8s.io/client-go/plugin/pkg/auth/authenticator/token/oidc/testing",
deps = [
"//vendor/github.com/coreos/go-oidc/jose:go_default_library",
"//vendor/github.com/coreos/go-oidc/key:go_default_library",
"//vendor/github.com/coreos/go-oidc/oidc:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -1,200 +0,0 @@
/*
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 testing
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path"
"path/filepath"
"testing"
"time"
"github.com/coreos/go-oidc/jose"
"github.com/coreos/go-oidc/key"
"github.com/coreos/go-oidc/oidc"
)
// NewOIDCProvider provides a bare minimum OIDC IdP Server useful for testing.
func NewOIDCProvider(t *testing.T, issuerPath string) *OIDCProvider {
privKey, err := key.GeneratePrivateKey()
if err != nil {
t.Fatalf("Cannot create OIDC Provider: %v", err)
return nil
}
op := &OIDCProvider{
Mux: http.NewServeMux(),
PrivKey: privKey,
issuerPath: issuerPath,
}
op.Mux.HandleFunc(path.Join(issuerPath, "/.well-known/openid-configuration"), op.handleConfig)
op.Mux.HandleFunc(path.Join(issuerPath, "/keys"), op.handleKeys)
return op
}
type OIDCProvider struct {
Mux *http.ServeMux
PCFG oidc.ProviderConfig
PrivKey *key.PrivateKey
issuerPath string
}
func (op *OIDCProvider) ServeTLSWithKeyPair(cert, key string) (*httptest.Server, error) {
srv := httptest.NewUnstartedServer(op.Mux)
srv.TLS = &tls.Config{Certificates: make([]tls.Certificate, 1)}
var err error
srv.TLS.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
if err != nil {
return nil, fmt.Errorf("Cannot load cert/key pair: %v", err)
}
srv.StartTLS()
// The issuer's URL is extended by an optional path. This ensures that the plugin can
// handle issuers that use a non-root path for discovery (see kubernetes/kubernetes#29749).
srv.URL = srv.URL + op.issuerPath
u, err := url.Parse(srv.URL)
if err != nil {
return nil, err
}
pathFor := func(p string) *url.URL {
u2 := *u // Shallow copy.
u2.Path = path.Join(u2.Path, p)
return &u2
}
op.PCFG = oidc.ProviderConfig{
Issuer: u,
AuthEndpoint: pathFor("/auth"),
TokenEndpoint: pathFor("/token"),
KeysEndpoint: pathFor("/keys"),
ResponseTypesSupported: []string{"code"},
SubjectTypesSupported: []string{"public"},
IDTokenSigningAlgValues: []string{"RS256"},
}
return srv, nil
}
func (op *OIDCProvider) handleConfig(w http.ResponseWriter, req *http.Request) {
b, err := json.Marshal(&op.PCFG)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(b)
}
func (op *OIDCProvider) handleKeys(w http.ResponseWriter, req *http.Request) {
keys := struct {
Keys []jose.JWK `json:"keys"`
}{
Keys: []jose.JWK{op.PrivKey.JWK()},
}
b, err := json.Marshal(keys)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", int(time.Hour.Seconds())))
w.Header().Set("Expires", time.Now().Add(time.Hour).Format(time.RFC1123))
w.Header().Set("Content-Type", "application/json")
w.Write(b)
}
// generateSelfSignedCert generates a self-signed cert/key pairs and writes to the certPath/keyPath.
// This method is mostly identical to crypto.GenerateSelfSignedCert except for the 'IsCA' and 'KeyUsage'
// in the certificate template. (Maybe we can merge these two methods).
func GenerateSelfSignedCert(t *testing.T, host, certPath, keyPath string) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()),
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}
if ip := net.ParseIP(host); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, host)
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
t.Fatal(err)
}
// Generate cert
certBuffer := bytes.Buffer{}
if err := pem.Encode(&certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
t.Fatal(err)
}
// Generate key
keyBuffer := bytes.Buffer{}
if err := pem.Encode(&keyBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
t.Fatal(err)
}
// Write cert
if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0755)); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(certPath, certBuffer.Bytes(), os.FileMode(0644)); err != nil {
t.Fatal(err)
}
// Write key
if err := os.MkdirAll(filepath.Dir(keyPath), os.FileMode(0755)); err != nil {
t.Fatal(err)
}
if err := ioutil.WriteFile(keyPath, keyBuffer.Bytes(), os.FileMode(0600)); err != nil {
t.Fatal(err)
}
}

View File

@ -1,37 +0,0 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["plugins.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth",
deps = [
"//vendor/k8s.io/client-go/plugin/pkg/client/auth/azure:go_default_library",
"//vendor/k8s.io/client-go/plugin/pkg/client/auth/gcp:go_default_library",
"//vendor/k8s.io/client-go/plugin/pkg/client/auth/oidc:go_default_library",
"//vendor/k8s.io/client-go/plugin/pkg/client/auth/openstack:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [
":package-srcs",
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth/azure:all-srcs",
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth/gcp:all-srcs",
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth/oidc:all-srcs",
"//staging/src/k8s.io/client-go/plugin/pkg/client/auth/openstack:all-srcs",
],
tags = ["automanaged"],
)

View File

@ -1,42 +0,0 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["azure_test.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/azure",
library = ":go_default_library",
deps = ["//vendor/github.com/Azure/go-autorest/autorest/adal:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = ["azure.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/azure",
deps = [
"//vendor/github.com/Azure/go-autorest/autorest:go_default_library",
"//vendor/github.com/Azure/go-autorest/autorest/adal:go_default_library",
"//vendor/github.com/Azure/go-autorest/autorest/azure:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -1,48 +0,0 @@
# Azure Active Directory plugin for client authentication
This plugin provides an integration with Azure Active Directory device flow. If no tokens are present in the kubectl configuration, it will prompt a device code which can be used to login in a browser. After login it will automatically fetch the tokens and stored them in the kubectl configuration. In addition it will refresh and update the tokens in configuration when expired.
## Usage
1. Create an Azure Active Directory *Web App / API* application for `apiserver` following these [instructions](https://docs.microsoft.com/en-us/azure/active-directory/active-directory-app-registration)
2. Create a second Azure Active Directory native application for `kubectl`
3. On `kubectl` application's configuration page in Azure portal grant permissions to `apiserver` application by clicking on *Required Permissions*, click the *Add* button and search for the apiserver application created in step 1. Select "Access apiserver" under the *DELEGATED PERMISSIONS*. Once added click the *Grant Permissions* button to apply the changes
4. Configure the `apiserver` to use the Azure Active Directory as an OIDC provider with following options
```
--oidc-client-id="spn:APISERVER_APPLICATION_ID" \
--oidc-issuer-url="https://sts.windows.net/TENANT_ID/"
--oidc-username-claim="sub"
```
* Replace the `APISERVER_APPLICATION_ID` with the application ID of `apiserver` application
* Replace `TENANT_ID` with your tenant ID.
5. Configure the `kubectl` to use the `azure` authentication provider
```
kubectl config set-credentials "USER_NAME" --auth-provider=azure \
--auth-provider-arg=environment=AzurePublicCloud \
--auth-provider-arg=client-id=APPLICATION_ID \
--auth-provider-arg=tenant-id=TENANT_ID \
--auth-provider-arg=apiserver-id=APISERVER_APPLICATION_ID
```
* Supported environments: `AzurePublicCloud`, `AzureUSGovernmentCloud`, `AzureChinaCloud`, `AzureGermanCloud`
* Replace `USER_NAME` and `TENANT_ID` with your user name and tenant ID
* Replace `APPLICATION_ID` with the application ID of your`kubectl` application ID
* Replace `APISERVER_APPLICATION_ID` with the application ID of your `apiserver` application ID
6. The access token is acquired when first `kubectl` command is executed
```
kubectl get pods
To sign in, use a web browser to open the page https://aka.ms/devicelogin and enter the code DEC7D48GA to authenticate.
```
* After signing in a web browser, the token is stored in the configuration, and it will be reused when executing next commands.

View File

@ -1,359 +0,0 @@
/*
Copyright 2017 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 azure
import (
"errors"
"fmt"
"net/http"
"os"
"sync"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/util/net"
restclient "k8s.io/client-go/rest"
)
const (
azureTokenKey = "azureTokenKey"
tokenType = "Bearer"
authHeader = "Authorization"
cfgClientID = "client-id"
cfgTenantID = "tenant-id"
cfgAccessToken = "access-token"
cfgRefreshToken = "refresh-token"
cfgExpiresIn = "expires-in"
cfgExpiresOn = "expires-on"
cfgEnvironment = "environment"
cfgApiserverID = "apiserver-id"
)
func init() {
if err := restclient.RegisterAuthProviderPlugin("azure", newAzureAuthProvider); err != nil {
glog.Fatalf("Failed to register azure auth plugin: %v", err)
}
}
var cache = newAzureTokenCache()
type azureTokenCache struct {
lock sync.Mutex
cache map[string]*azureToken
}
func newAzureTokenCache() *azureTokenCache {
return &azureTokenCache{cache: make(map[string]*azureToken)}
}
func (c *azureTokenCache) getToken(tokenKey string) *azureToken {
c.lock.Lock()
defer c.lock.Unlock()
return c.cache[tokenKey]
}
func (c *azureTokenCache) setToken(tokenKey string, token *azureToken) {
c.lock.Lock()
defer c.lock.Unlock()
c.cache[tokenKey] = token
}
func newAzureAuthProvider(_ string, cfg map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
var ts tokenSource
environment, err := azure.EnvironmentFromName(cfg[cfgEnvironment])
if err != nil {
environment = azure.PublicCloud
}
ts, err = newAzureTokenSourceDeviceCode(environment, cfg[cfgClientID], cfg[cfgTenantID], cfg[cfgApiserverID])
if err != nil {
return nil, fmt.Errorf("creating a new azure token source for device code authentication: %v", err)
}
cacheSource := newAzureTokenSource(ts, cache, cfg, persister)
return &azureAuthProvider{
tokenSource: cacheSource,
}, nil
}
type azureAuthProvider struct {
tokenSource tokenSource
}
func (p *azureAuthProvider) Login() error {
return errors.New("not yet implemented")
}
func (p *azureAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
return &azureRoundTripper{
tokenSource: p.tokenSource,
roundTripper: rt,
}
}
type azureRoundTripper struct {
tokenSource tokenSource
roundTripper http.RoundTripper
}
var _ net.RoundTripperWrapper = &azureRoundTripper{}
func (r *azureRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if len(req.Header.Get(authHeader)) != 0 {
return r.roundTripper.RoundTrip(req)
}
token, err := r.tokenSource.Token()
if err != nil {
glog.Errorf("Failed to acquire a token: %v", err)
return nil, fmt.Errorf("acquiring a token for authorization header: %v", err)
}
// clone the request in order to avoid modifying the headers of the original request
req2 := new(http.Request)
*req2 = *req
req2.Header = make(http.Header, len(req.Header))
for k, s := range req.Header {
req2.Header[k] = append([]string(nil), s...)
}
req2.Header.Set(authHeader, fmt.Sprintf("%s %s", tokenType, token.token.AccessToken))
return r.roundTripper.RoundTrip(req2)
}
func (r *azureRoundTripper) WrappedRoundTripper() http.RoundTripper { return r.roundTripper }
type azureToken struct {
token adal.Token
clientID string
tenantID string
apiserverID string
}
type tokenSource interface {
Token() (*azureToken, error)
}
type azureTokenSource struct {
source tokenSource
cache *azureTokenCache
lock sync.Mutex
cfg map[string]string
persister restclient.AuthProviderConfigPersister
}
func newAzureTokenSource(source tokenSource, cache *azureTokenCache, cfg map[string]string, persister restclient.AuthProviderConfigPersister) tokenSource {
return &azureTokenSource{
source: source,
cache: cache,
cfg: cfg,
persister: persister,
}
}
// Token fetches a token from the cache of configuration if present otherwise
// acquires a new token from the configured source. Automatically refreshes
// the token if expired.
func (ts *azureTokenSource) Token() (*azureToken, error) {
ts.lock.Lock()
defer ts.lock.Unlock()
var err error
token := ts.cache.getToken(azureTokenKey)
if token == nil {
token, err = ts.retrieveTokenFromCfg()
if err != nil {
token, err = ts.source.Token()
if err != nil {
return nil, fmt.Errorf("acquiring a new fresh token: %v", err)
}
}
if !token.token.IsExpired() {
ts.cache.setToken(azureTokenKey, token)
err = ts.storeTokenInCfg(token)
if err != nil {
return nil, fmt.Errorf("storing the token in configuration: %v", err)
}
}
}
if token.token.IsExpired() {
token, err = ts.refreshToken(token)
if err != nil {
return nil, fmt.Errorf("refreshing the expired token: %v", err)
}
ts.cache.setToken(azureTokenKey, token)
err = ts.storeTokenInCfg(token)
if err != nil {
return nil, fmt.Errorf("storing the refreshed token in configuration: %v", err)
}
}
return token, nil
}
func (ts *azureTokenSource) retrieveTokenFromCfg() (*azureToken, error) {
accessToken := ts.cfg[cfgAccessToken]
if accessToken == "" {
return nil, fmt.Errorf("no access token in cfg: %s", cfgAccessToken)
}
refreshToken := ts.cfg[cfgRefreshToken]
if refreshToken == "" {
return nil, fmt.Errorf("no refresh token in cfg: %s", cfgRefreshToken)
}
clientID := ts.cfg[cfgClientID]
if clientID == "" {
return nil, fmt.Errorf("no client ID in cfg: %s", cfgClientID)
}
tenantID := ts.cfg[cfgTenantID]
if tenantID == "" {
return nil, fmt.Errorf("no tenant ID in cfg: %s", cfgTenantID)
}
apiserverID := ts.cfg[cfgApiserverID]
if apiserverID == "" {
return nil, fmt.Errorf("no apiserver ID in cfg: %s", apiserverID)
}
expiresIn := ts.cfg[cfgExpiresIn]
if expiresIn == "" {
return nil, fmt.Errorf("no expiresIn in cfg: %s", cfgExpiresIn)
}
expiresOn := ts.cfg[cfgExpiresOn]
if expiresOn == "" {
return nil, fmt.Errorf("no expiresOn in cfg: %s", cfgExpiresOn)
}
return &azureToken{
token: adal.Token{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: expiresIn,
ExpiresOn: expiresOn,
NotBefore: expiresOn,
Resource: fmt.Sprintf("spn:%s", apiserverID),
Type: tokenType,
},
clientID: clientID,
tenantID: tenantID,
apiserverID: apiserverID,
}, nil
}
func (ts *azureTokenSource) storeTokenInCfg(token *azureToken) error {
newCfg := make(map[string]string)
newCfg[cfgAccessToken] = token.token.AccessToken
newCfg[cfgRefreshToken] = token.token.RefreshToken
newCfg[cfgClientID] = token.clientID
newCfg[cfgTenantID] = token.tenantID
newCfg[cfgApiserverID] = token.apiserverID
newCfg[cfgExpiresIn] = token.token.ExpiresIn
newCfg[cfgExpiresOn] = token.token.ExpiresOn
err := ts.persister.Persist(newCfg)
if err != nil {
return fmt.Errorf("persisting the configuration: %v", err)
}
ts.cfg = newCfg
return nil
}
func (ts *azureTokenSource) refreshToken(token *azureToken) (*azureToken, error) {
oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, token.tenantID)
if err != nil {
return nil, fmt.Errorf("building the OAuth configuration for token refresh: %v", err)
}
callback := func(t adal.Token) error {
return nil
}
spt, err := adal.NewServicePrincipalTokenFromManualToken(
*oauthConfig,
token.clientID,
token.apiserverID,
token.token,
callback)
if err != nil {
return nil, fmt.Errorf("creating new service principal for token refresh: %v", err)
}
if err := spt.Refresh(); err != nil {
return nil, fmt.Errorf("refreshing token: %v", err)
}
return &azureToken{
token: spt.Token,
clientID: token.clientID,
tenantID: token.tenantID,
apiserverID: token.apiserverID,
}, nil
}
type azureTokenSourceDeviceCode struct {
environment azure.Environment
clientID string
tenantID string
apiserverID string
}
func newAzureTokenSourceDeviceCode(environment azure.Environment, clientID string, tenantID string, apiserverID string) (tokenSource, error) {
if clientID == "" {
return nil, errors.New("client-id is empty")
}
if tenantID == "" {
return nil, errors.New("tenant-id is empty")
}
if apiserverID == "" {
return nil, errors.New("apiserver-id is empty")
}
return &azureTokenSourceDeviceCode{
environment: environment,
clientID: clientID,
tenantID: tenantID,
apiserverID: apiserverID,
}, nil
}
func (ts *azureTokenSourceDeviceCode) Token() (*azureToken, error) {
oauthConfig, err := adal.NewOAuthConfig(ts.environment.ActiveDirectoryEndpoint, ts.tenantID)
if err != nil {
return nil, fmt.Errorf("building the OAuth configuration for device code authentication: %v", err)
}
client := &autorest.Client{}
deviceCode, err := adal.InitiateDeviceAuth(client, *oauthConfig, ts.clientID, ts.apiserverID)
if err != nil {
return nil, fmt.Errorf("initialing the device code authentication: %v", err)
}
_, err = fmt.Fprintln(os.Stderr, *deviceCode.Message)
if err != nil {
return nil, fmt.Errorf("prompting the device code message: %v", err)
}
token, err := adal.WaitForUserCompletion(client, deviceCode)
if err != nil {
return nil, fmt.Errorf("waiting for device code authentication to complete: %v", err)
}
return &azureToken{
token: *token,
clientID: ts.clientID,
tenantID: ts.tenantID,
apiserverID: ts.apiserverID,
}, nil
}

View File

@ -1,133 +0,0 @@
/*
Copyright 2017 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 azure
import (
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/Azure/go-autorest/autorest/adal"
)
func TestAzureTokenSource(t *testing.T) {
fakeAccessToken := "fake token 1"
fakeSource := fakeTokenSource{
accessToken: fakeAccessToken,
expiresOn: strconv.FormatInt(time.Now().Add(3600*time.Second).Unix(), 10),
}
cfg := make(map[string]string)
persiter := &fakePersister{cache: make(map[string]string)}
tokenCache := newAzureTokenCache()
tokenSource := newAzureTokenSource(&fakeSource, tokenCache, cfg, persiter)
token, err := tokenSource.Token()
if err != nil {
t.Errorf("failed to retrieve the token form cache: %v", err)
}
wantCacheLen := 1
if len(tokenCache.cache) != wantCacheLen {
t.Errorf("Token() cache length error: got %v, want %v", len(tokenCache.cache), wantCacheLen)
}
if token != tokenCache.cache[azureTokenKey] {
t.Error("Token() returned token != cached token")
}
wantCfg := token2Cfg(token)
persistedCfg := persiter.Cache()
for k, v := range persistedCfg {
if strings.Compare(v, wantCfg[k]) != 0 {
t.Errorf("Token() persisted cfg %s: got %v, want %v", k, v, wantCfg[k])
}
}
fakeSource.accessToken = "fake token 2"
token, err = tokenSource.Token()
if err != nil {
t.Errorf("failed to retrieve the cached token: %v", err)
}
if token.token.AccessToken != fakeAccessToken {
t.Errorf("Token() didn't return the cached token")
}
}
type fakePersister struct {
lock sync.Mutex
cache map[string]string
}
func (p *fakePersister) Persist(cache map[string]string) error {
p.lock.Lock()
defer p.lock.Unlock()
p.cache = map[string]string{}
for k, v := range cache {
p.cache[k] = v
}
return nil
}
func (p *fakePersister) Cache() map[string]string {
ret := map[string]string{}
p.lock.Lock()
defer p.lock.Unlock()
for k, v := range p.cache {
ret[k] = v
}
return ret
}
type fakeTokenSource struct {
expiresOn string
accessToken string
}
func (ts *fakeTokenSource) Token() (*azureToken, error) {
return &azureToken{
token: newFackeAzureToken(ts.accessToken, ts.expiresOn),
clientID: "fake",
tenantID: "fake",
apiserverID: "fake",
}, nil
}
func token2Cfg(token *azureToken) map[string]string {
cfg := make(map[string]string)
cfg[cfgAccessToken] = token.token.AccessToken
cfg[cfgRefreshToken] = token.token.RefreshToken
cfg[cfgClientID] = token.clientID
cfg[cfgTenantID] = token.tenantID
cfg[cfgApiserverID] = token.apiserverID
cfg[cfgExpiresIn] = token.token.ExpiresIn
cfg[cfgExpiresOn] = token.token.ExpiresOn
return cfg
}
func newFackeAzureToken(accessToken string, expiresOn string) adal.Token {
return adal.Token{
AccessToken: accessToken,
RefreshToken: "fake",
ExpiresIn: "3600",
ExpiresOn: expiresOn,
NotBefore: expiresOn,
Resource: "fake",
Type: "fake",
}
}

View File

@ -1,44 +0,0 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["gcp_test.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/gcp",
library = ":go_default_library",
deps = ["//vendor/golang.org/x/oauth2:go_default_library"],
)
go_library(
name = "go_default_library",
srcs = ["gcp.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/gcp",
deps = [
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/golang.org/x/net/context:go_default_library",
"//vendor/golang.org/x/oauth2:go_default_library",
"//vendor/golang.org/x/oauth2/google:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
"//vendor/k8s.io/client-go/util/jsonpath:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -1,6 +0,0 @@
approvers:
- cjcullen
- jlowdermilk
reviewers:
- cjcullen
- jlowdermilk

View File

@ -1,315 +0,0 @@
/*
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 (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os/exec"
"strings"
"sync"
"time"
"github.com/golang/glog"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
"k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/pkg/util/yaml"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/util/jsonpath"
)
func init() {
if err := restclient.RegisterAuthProviderPlugin("gcp", newGCPAuthProvider); err != nil {
glog.Fatalf("Failed to register gcp auth plugin: %v", err)
}
}
// Stubbable for testing
var execCommand = exec.Command
// gcpAuthProvider is an auth provider plugin that uses GCP credentials to provide
// tokens for kubectl to authenticate itself to the apiserver. A sample json config
// is provided below with all recognized options described.
//
// {
// 'auth-provider': {
// # Required
// "name": "gcp",
//
// 'config': {
// # Caching options
//
// # Raw string data representing cached access token.
// "access-token": "ya29.CjWdA4GiBPTt",
// # RFC3339Nano expiration timestamp for cached access token.
// "expiry": "2016-10-31 22:31:9.123",
//
// # Command execution options
// # These options direct the plugin to execute a specified command and parse
// # token and expiry time from the output of the command.
//
// # Command to execute for access token. Command output will be parsed as JSON.
// # If "cmd-args" is not present, this value will be split on whitespace, with
// # the first element interpreted as the command, remaining elements as args.
// "cmd-path": "/usr/bin/gcloud",
//
// # Arguments to pass to command to execute for access token.
// "cmd-args": "config config-helper --output=json"
//
// # JSONPath to the string field that represents the access token in
// # command output. If omitted, defaults to "{.access_token}".
// "token-key": "{.credential.access_token}",
//
// # JSONPath to the string field that represents expiration timestamp
// # of the access token in the command output. If omitted, defaults to
// # "{.token_expiry}"
// "expiry-key": ""{.credential.token_expiry}",
//
// # golang reference time in the format that the expiration timestamp uses.
// # If omitted, defaults to time.RFC3339Nano
// "time-fmt": "2006-01-02 15:04:05.999999999"
// }
// }
// }
//
type gcpAuthProvider struct {
tokenSource oauth2.TokenSource
persister restclient.AuthProviderConfigPersister
}
func newGCPAuthProvider(_ string, gcpConfig map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
var ts oauth2.TokenSource
var err error
if cmd, useCmd := gcpConfig["cmd-path"]; useCmd {
if len(cmd) == 0 {
return nil, fmt.Errorf("missing access token cmd")
}
var args []string
if cmdArgs, ok := gcpConfig["cmd-args"]; ok {
args = strings.Fields(cmdArgs)
} else {
fields := strings.Fields(cmd)
cmd = fields[0]
args = fields[1:]
}
ts = newCmdTokenSource(cmd, args, 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
}
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 {
return &conditionalTransport{&oauth2.Transport{Source: g.tokenSource, Base: rt}, g.persister}
}
func (g *gcpAuthProvider) Login() error { return nil }
type cachedTokenSource struct {
lk sync.Mutex
source oauth2.TokenSource
accessToken string
expiry time.Time
persister restclient.AuthProviderConfigPersister
cache map[string]string
}
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
}
if cache == nil {
cache = make(map[string]string)
}
return &cachedTokenSource{
source: ts,
accessToken: accessToken,
expiry: expiryTime,
persister: persister,
cache: cache,
}, nil
}
func (t *cachedTokenSource) Token() (*oauth2.Token, error) {
tok := t.cachedToken()
if tok.Valid() && !tok.Expiry.IsZero() {
return tok, nil
}
tok, err := t.source.Token()
if err != nil {
return nil, err
}
cache := t.update(tok)
if t.persister != nil {
if err := t.persister.Persist(cache); err != nil {
glog.V(4).Infof("Failed to persist token: %v", err)
}
}
return tok, nil
}
func (t *cachedTokenSource) cachedToken() *oauth2.Token {
t.lk.Lock()
defer t.lk.Unlock()
return &oauth2.Token{
AccessToken: t.accessToken,
TokenType: "Bearer",
Expiry: t.expiry,
}
}
func (t *cachedTokenSource) update(tok *oauth2.Token) map[string]string {
t.lk.Lock()
defer t.lk.Unlock()
t.accessToken = tok.AccessToken
t.expiry = tok.Expiry
ret := map[string]string{}
for k, v := range t.cache {
ret[k] = v
}
ret["access-token"] = t.accessToken
ret["expiry"] = t.expiry.Format(time.RFC3339Nano)
return ret
}
type commandTokenSource struct {
cmd string
args []string
tokenKey string
expiryKey string
timeFmt string
}
func newCmdTokenSource(cmd string, args []string, tokenKey, expiryKey, timeFmt string) *commandTokenSource {
if len(timeFmt) == 0 {
timeFmt = time.RFC3339Nano
}
if len(tokenKey) == 0 {
tokenKey = "{.access_token}"
}
if len(expiryKey) == 0 {
expiryKey = "{.token_expiry}"
}
return &commandTokenSource{
cmd: cmd,
args: args,
tokenKey: tokenKey,
expiryKey: expiryKey,
timeFmt: timeFmt,
}
}
func (c *commandTokenSource) Token() (*oauth2.Token, error) {
fullCmd := strings.Join(append([]string{c.cmd}, c.args...), " ")
cmd := execCommand(c.cmd, c.args...)
var stderr bytes.Buffer
cmd.Stderr = &stderr
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("error executing access token command %q: err=%v output=%s stderr=%s", fullCmd, err, output, string(stderr.Bytes()))
}
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 from %q: %v", c.tokenKey, string(output), err)
}
expiryStr, err := parseJSONPath(data, "expiry-key", c.expiryKey)
if err != nil {
return nil, fmt.Errorf("error parsing expiry-key %q from %q: %v", c.expiryKey, string(output), 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
}
type conditionalTransport struct {
oauthTransport *oauth2.Transport
persister restclient.AuthProviderConfigPersister
}
var _ net.RoundTripperWrapper = &conditionalTransport{}
func (t *conditionalTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if len(req.Header.Get("Authorization")) != 0 {
return t.oauthTransport.Base.RoundTrip(req)
}
res, err := t.oauthTransport.RoundTrip(req)
if err != nil {
return nil, err
}
if res.StatusCode == 401 {
glog.V(4).Infof("The credentials that were supplied are invalid for the target cluster")
emptyCache := make(map[string]string)
t.persister.Persist(emptyCache)
}
return res, nil
}
func (t *conditionalTransport) WrappedRoundTripper() http.RoundTripper { return t.oauthTransport.Base }

View File

@ -1,388 +0,0 @@
/*
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"
"net/http"
"os"
"os/exec"
"reflect"
"strings"
"sync"
"testing"
"time"
"golang.org/x/oauth2"
)
type fakeOutput struct {
args []string
output string
}
var (
wantCmd []string
// Output for fakeExec, keyed by command
execOutputs = map[string]fakeOutput{
"/default/no/args": {
args: []string{},
output: `{
"access_token": "faketoken",
"token_expiry": "2016-10-31T22:31:09.123000000Z"
}`},
"/default/legacy/args": {
args: []string{"arg1", "arg2", "arg3"},
output: `{
"access_token": "faketoken",
"token_expiry": "2016-10-31T22:31:09.123000000Z"
}`},
"/space in path/customkeys": {
args: []string{"can", "haz", "auth"},
output: `{
"token": "faketoken",
"token_expiry": {
"datetime": "2016-10-31 22:31:09.123"
}
}`},
"missing/tokenkey/noargs": {
args: []string{},
output: `{
"broken": "faketoken",
"token_expiry": {
"datetime": "2016-10-31 22:31:09.123000000Z"
}
}`},
"missing/expirykey/legacyargs": {
args: []string{"split", "on", "whitespace"},
output: `{
"access_token": "faketoken",
"expires": "2016-10-31T22:31:09.123000000Z"
}`},
"invalid expiry/timestamp": {
args: []string{"foo", "--bar", "--baz=abc,def"},
output: `{
"access_token": "faketoken",
"token_expiry": "sometime soon, idk"
}`},
"badjson": {
args: []string{},
output: `{
"access_token": "faketoken",
"token_expiry": "sometime soon, idk"
------
`},
}
)
func fakeExec(command string, args ...string) *exec.Cmd {
cs := []string{"-test.run=TestHelperProcess", "--", command}
cs = append(cs, args...)
cmd := exec.Command(os.Args[0], cs...)
cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"}
return cmd
}
func TestHelperProcess(t *testing.T) {
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
return
}
// Strip out the leading args used to exec into this function.
gotCmd := os.Args[3]
gotArgs := os.Args[4:]
output, ok := execOutputs[gotCmd]
if !ok {
fmt.Fprintf(os.Stdout, "unexpected call cmd=%q args=%v\n", gotCmd, gotArgs)
os.Exit(1)
} else if !reflect.DeepEqual(output.args, gotArgs) {
fmt.Fprintf(os.Stdout, "call cmd=%q got args %v, want: %v\n", gotCmd, gotArgs, output.args)
os.Exit(1)
}
fmt.Fprintf(os.Stdout, output.output)
os.Exit(0)
}
func errEquiv(got, want error) bool {
if got == want {
return true
}
if got != nil && want != nil {
return strings.Contains(got.Error(), want.Error())
}
return false
}
func TestCmdTokenSource(t *testing.T) {
execCommand = fakeExec
fakeExpiry := time.Date(2016, 10, 31, 22, 31, 9, 123000000, time.UTC)
customFmt := "2006-01-02 15:04:05.999999999"
tests := []struct {
name string
gcpConfig map[string]string
tok *oauth2.Token
newErr, tokenErr error
}{
{
"default",
map[string]string{
"cmd-path": "/default/no/args",
},
&oauth2.Token{
AccessToken: "faketoken",
TokenType: "Bearer",
Expiry: fakeExpiry,
},
nil,
nil,
},
{
"default legacy args",
map[string]string{
"cmd-path": "/default/legacy/args arg1 arg2 arg3",
},
&oauth2.Token{
AccessToken: "faketoken",
TokenType: "Bearer",
Expiry: fakeExpiry,
},
nil,
nil,
},
{
"custom keys",
map[string]string{
"cmd-path": "/space in path/customkeys",
"cmd-args": "can haz auth",
"token-key": "{.token}",
"expiry-key": "{.token_expiry.datetime}",
"time-fmt": customFmt,
},
&oauth2.Token{
AccessToken: "faketoken",
TokenType: "Bearer",
Expiry: fakeExpiry,
},
nil,
nil,
},
{
"missing cmd",
map[string]string{
"cmd-path": "",
},
nil,
fmt.Errorf("missing access token cmd"),
nil,
},
{
"missing token-key",
map[string]string{
"cmd-path": "missing/tokenkey/noargs",
"token-key": "{.token}",
},
nil,
nil,
fmt.Errorf("error parsing token-key %q", "{.token}"),
},
{
"missing expiry-key",
map[string]string{
"cmd-path": "missing/expirykey/legacyargs split on whitespace",
"expiry-key": "{.expiry}",
},
nil,
nil,
fmt.Errorf("error parsing expiry-key %q", "{.expiry}"),
},
{
"invalid expiry timestamp",
map[string]string{
"cmd-path": "invalid expiry/timestamp",
"cmd-args": "foo --bar --baz=abc,def",
},
&oauth2.Token{
AccessToken: "faketoken",
TokenType: "Bearer",
Expiry: time.Time{},
},
nil,
nil,
},
{
"bad JSON",
map[string]string{
"cmd-path": "badjson",
},
nil,
nil,
fmt.Errorf("invalid character '-' after object key:value pair"),
},
}
for _, tc := range tests {
provider, err := newGCPAuthProvider("", tc.gcpConfig, nil /* persister */)
if !errEquiv(err, tc.newErr) {
t.Errorf("%q newGCPAuthProvider error: got %v, want %v", tc.name, err, tc.newErr)
continue
}
if err != nil {
continue
}
ts := provider.(*gcpAuthProvider).tokenSource.(*cachedTokenSource).source.(*commandTokenSource)
wantCmd = append([]string{ts.cmd}, ts.args...)
tok, err := ts.Token()
if !errEquiv(err, tc.tokenErr) {
t.Errorf("%q Token() error: got %v, want %v", tc.name, err, tc.tokenErr)
}
if !reflect.DeepEqual(tok, tc.tok) {
t.Errorf("%q Token() got %v, want %v", tc.name, tok, tc.tok)
}
}
}
type fakePersister struct {
lk sync.Mutex
cache map[string]string
}
func (f *fakePersister) Persist(cache map[string]string) error {
f.lk.Lock()
defer f.lk.Unlock()
f.cache = map[string]string{}
for k, v := range cache {
f.cache[k] = v
}
return nil
}
func (f *fakePersister) read() map[string]string {
ret := map[string]string{}
f.lk.Lock()
defer f.lk.Unlock()
for k, v := range f.cache {
ret[k] = v
}
return ret
}
type fakeTokenSource struct {
token *oauth2.Token
err error
}
func (f *fakeTokenSource) Token() (*oauth2.Token, error) {
return f.token, f.err
}
func TestCachedTokenSource(t *testing.T) {
tok := &oauth2.Token{AccessToken: "fakeaccesstoken"}
persister := &fakePersister{}
source := &fakeTokenSource{
token: tok,
err: nil,
}
cache := map[string]string{
"foo": "bar",
"baz": "bazinga",
}
ts, err := newCachedTokenSource("fakeaccesstoken", "", persister, source, cache)
if err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
wg.Add(10)
for i := 0; i < 10; i++ {
go func() {
_, err := ts.Token()
if err != nil {
t.Errorf("unexpected error: %s", err)
}
wg.Done()
}()
}
wg.Wait()
cache["access-token"] = "fakeaccesstoken"
cache["expiry"] = tok.Expiry.Format(time.RFC3339Nano)
if got := persister.read(); !reflect.DeepEqual(got, cache) {
t.Errorf("got cache %v, want %v", got, cache)
}
}
type MockTransport struct {
res *http.Response
}
func (t *MockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return t.res, nil
}
func TestClearingCredentials(t *testing.T) {
fakeExpiry := time.Now().Add(time.Hour)
cache := map[string]string{
"access-token": "fakeToken",
"expiry": fakeExpiry.String(),
}
cts := cachedTokenSource{
source: nil,
accessToken: cache["access-token"],
expiry: fakeExpiry,
persister: nil,
cache: nil,
}
tests := []struct {
name string
res http.Response
cache map[string]string
}{
{
"Unauthorized",
http.Response{StatusCode: 401},
make(map[string]string),
},
{
"Authorized",
http.Response{StatusCode: 200},
cache,
},
}
persister := &fakePersister{}
req := http.Request{Header: http.Header{}}
for _, tc := range tests {
authProvider := gcpAuthProvider{&cts, persister}
fakeTransport := MockTransport{&tc.res}
transport := (authProvider.WrapTransport(&fakeTransport))
persister.Persist(cache)
transport.RoundTrip(&req)
if got := persister.read(); !reflect.DeepEqual(got, tc.cache) {
t.Errorf("got cache %v, want %v", got, tc.cache)
}
}
}

View File

@ -1,39 +0,0 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["oidc_test.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/oidc",
library = ":go_default_library",
)
go_library(
name = "go_default_library",
srcs = ["oidc.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/oidc",
deps = [
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/golang.org/x/oauth2:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -1,4 +0,0 @@
approvers:
- ericchiang
reviewers:
- ericchiang

View File

@ -1,379 +0,0 @@
/*
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 oidc
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strings"
"sync"
"time"
"github.com/golang/glog"
"golang.org/x/oauth2"
"k8s.io/apimachinery/pkg/util/net"
restclient "k8s.io/client-go/rest"
)
const (
cfgIssuerUrl = "idp-issuer-url"
cfgClientID = "client-id"
cfgClientSecret = "client-secret"
cfgCertificateAuthority = "idp-certificate-authority"
cfgCertificateAuthorityData = "idp-certificate-authority-data"
cfgIDToken = "id-token"
cfgRefreshToken = "refresh-token"
// Unused. Scopes aren't sent during refreshing.
cfgExtraScopes = "extra-scopes"
)
func init() {
if err := restclient.RegisterAuthProviderPlugin("oidc", newOIDCAuthProvider); err != nil {
glog.Fatalf("Failed to register oidc auth plugin: %v", err)
}
}
// expiryDelta determines how earlier a token should be considered
// expired than its actual expiration time. It is used to avoid late
// expirations due to client-server time mismatches.
//
// NOTE(ericchiang): this is take from golang.org/x/oauth2
const expiryDelta = 10 * time.Second
var cache = newClientCache()
// Like TLS transports, keep a cache of OIDC clients indexed by issuer URL. This ensures
// current requests from different clients don't concurrently attempt to refresh the same
// set of credentials.
type clientCache struct {
mu sync.RWMutex
cache map[cacheKey]*oidcAuthProvider
}
func newClientCache() *clientCache {
return &clientCache{cache: make(map[cacheKey]*oidcAuthProvider)}
}
type cacheKey struct {
// Canonical issuer URL string of the provider.
issuerURL string
clientID string
}
func (c *clientCache) getClient(issuer, clientID string) (*oidcAuthProvider, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
client, ok := c.cache[cacheKey{issuer, clientID}]
return client, ok
}
// setClient attempts to put the client in the cache but may return any clients
// with the same keys set before. This is so there's only ever one client for a provider.
func (c *clientCache) setClient(issuer, clientID string, client *oidcAuthProvider) *oidcAuthProvider {
c.mu.Lock()
defer c.mu.Unlock()
key := cacheKey{issuer, clientID}
// If another client has already initialized a client for the given provider we want
// to use that client instead of the one we're trying to set. This is so all transports
// share a client and can coordinate around the same mutex when refreshing and writing
// to the kubeconfig.
if oldClient, ok := c.cache[key]; ok {
return oldClient
}
c.cache[key] = client
return client
}
func newOIDCAuthProvider(_ string, cfg map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
issuer := cfg[cfgIssuerUrl]
if issuer == "" {
return nil, fmt.Errorf("Must provide %s", cfgIssuerUrl)
}
clientID := cfg[cfgClientID]
if clientID == "" {
return nil, fmt.Errorf("Must provide %s", cfgClientID)
}
// Check cache for existing provider.
if provider, ok := cache.getClient(issuer, clientID); ok {
return provider, nil
}
if len(cfg[cfgExtraScopes]) > 0 {
glog.V(2).Infof("%s auth provider field depricated, refresh request don't send scopes",
cfgExtraScopes)
}
var certAuthData []byte
var err error
if cfg[cfgCertificateAuthorityData] != "" {
certAuthData, err = base64.StdEncoding.DecodeString(cfg[cfgCertificateAuthorityData])
if err != nil {
return nil, err
}
}
clientConfig := restclient.Config{
TLSClientConfig: restclient.TLSClientConfig{
CAFile: cfg[cfgCertificateAuthority],
CAData: certAuthData,
},
}
trans, err := restclient.TransportFor(&clientConfig)
if err != nil {
return nil, err
}
hc := &http.Client{Transport: trans}
provider := &oidcAuthProvider{
client: hc,
now: time.Now,
cfg: cfg,
persister: persister,
}
return cache.setClient(issuer, clientID, provider), nil
}
type oidcAuthProvider struct {
client *http.Client
// Method for determining the current time.
now func() time.Time
// Mutex guards persisting to the kubeconfig file and allows synchronized
// updates to the in-memory config. It also ensures concurrent calls to
// the RoundTripper only trigger a single refresh request.
mu sync.Mutex
cfg map[string]string
persister restclient.AuthProviderConfigPersister
}
func (p *oidcAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
return &roundTripper{
wrapped: rt,
provider: p,
}
}
func (p *oidcAuthProvider) Login() error {
return errors.New("not yet implemented")
}
type roundTripper struct {
provider *oidcAuthProvider
wrapped http.RoundTripper
}
var _ net.RoundTripperWrapper = &roundTripper{}
func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if len(req.Header.Get("Authorization")) != 0 {
return r.wrapped.RoundTrip(req)
}
token, err := r.provider.idToken()
if err != nil {
return nil, err
}
// shallow copy of the struct
r2 := new(http.Request)
*r2 = *req
// deep copy of the Header so we don't modify the original
// request's Header (as per RoundTripper contract).
r2.Header = make(http.Header)
for k, s := range req.Header {
r2.Header[k] = s
}
r2.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
return r.wrapped.RoundTrip(r2)
}
func (t *roundTripper) WrappedRoundTripper() http.RoundTripper { return t.wrapped }
func (p *oidcAuthProvider) idToken() (string, error) {
p.mu.Lock()
defer p.mu.Unlock()
if idToken, ok := p.cfg[cfgIDToken]; ok && len(idToken) > 0 {
valid, err := idTokenExpired(p.now, idToken)
if err != nil {
return "", err
}
if valid {
// If the cached id token is still valid use it.
return idToken, nil
}
}
// Try to request a new token using the refresh token.
rt, ok := p.cfg[cfgRefreshToken]
if !ok || len(rt) == 0 {
return "", errors.New("No valid id-token, and cannot refresh without refresh-token")
}
// Determine provider's OAuth2 token endpoint.
tokenURL, err := tokenEndpoint(p.client, p.cfg[cfgIssuerUrl])
if err != nil {
return "", err
}
config := oauth2.Config{
ClientID: p.cfg[cfgClientID],
ClientSecret: p.cfg[cfgClientSecret],
Endpoint: oauth2.Endpoint{TokenURL: tokenURL},
}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, p.client)
token, err := config.TokenSource(ctx, &oauth2.Token{RefreshToken: rt}).Token()
if err != nil {
return "", fmt.Errorf("failed to refresh token: %v", err)
}
idToken, ok := token.Extra("id_token").(string)
if !ok {
// id_token isn't a required part of a refresh token response, so some
// providers (Okta) don't return this value.
//
// See https://github.com/kubernetes/kubernetes/issues/36847
return "", fmt.Errorf("token response did not contain an id_token, either the scope \"openid\" wasn't requested upon login, or the provider doesn't support id_tokens as part of the refresh response.")
}
// Create a new config to persist.
newCfg := make(map[string]string)
for key, val := range p.cfg {
newCfg[key] = val
}
// Update the refresh token if the server returned another one.
if token.RefreshToken != "" && token.RefreshToken != rt {
newCfg[cfgRefreshToken] = token.RefreshToken
}
newCfg[cfgIDToken] = idToken
// Persist new config and if successful, update the in memory config.
if err = p.persister.Persist(newCfg); err != nil {
return "", fmt.Errorf("could not perist new tokens: %v", err)
}
p.cfg = newCfg
return idToken, nil
}
// tokenEndpoint uses OpenID Connect discovery to determine the OAuth2 token
// endpoint for the provider, the endpoint the client will use the refresh
// token against.
func tokenEndpoint(client *http.Client, issuer string) (string, error) {
// Well known URL for getting OpenID Connect metadata.
//
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
resp, err := client.Get(wellKnown)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
// Don't produce an error that's too huge (e.g. if we get HTML back for some reason).
const n = 80
if len(body) > n {
body = append(body[:n], []byte("...")...)
}
return "", fmt.Errorf("oidc: failed to query metadata endpoint %s: %q", resp.Status, body)
}
// Metadata object. We only care about the token_endpoint, the thing endpoint
// we'll be refreshing against.
//
// https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
var metadata struct {
TokenURL string `json:"token_endpoint"`
}
if err := json.Unmarshal(body, &metadata); err != nil {
return "", fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
}
if metadata.TokenURL == "" {
return "", fmt.Errorf("oidc: discovery object doesn't contain a token_endpoint")
}
return metadata.TokenURL, nil
}
func idTokenExpired(now func() time.Time, idToken string) (bool, error) {
parts := strings.Split(idToken, ".")
if len(parts) != 3 {
return false, fmt.Errorf("ID Token is not a valid JWT")
}
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return false, err
}
var claims struct {
Expiry jsonTime `json:"exp"`
}
if err := json.Unmarshal(payload, &claims); err != nil {
return false, fmt.Errorf("parsing claims: %v", err)
}
return now().Add(expiryDelta).Before(time.Time(claims.Expiry)), nil
}
// jsonTime is a json.Unmarshaler that parses a unix timestamp.
// Because JSON numbers don't differentiate between ints and floats,
// we want to ensure we can parse either.
type jsonTime time.Time
func (j *jsonTime) UnmarshalJSON(b []byte) error {
var n json.Number
if err := json.Unmarshal(b, &n); err != nil {
return err
}
var unix int64
if t, err := n.Int64(); err == nil {
unix = t
} else {
f, err := n.Float64()
if err != nil {
return err
}
unix = int64(f)
}
*j = jsonTime(time.Unix(unix, 0))
return nil
}
func (j jsonTime) MarshalJSON() ([]byte, error) {
return json.Marshal(time.Time(j).Unix())
}

View File

@ -1,138 +0,0 @@
/*
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 oidc
import (
"encoding/base64"
"encoding/json"
"fmt"
"testing"
"time"
)
func TestJSONTime(t *testing.T) {
data := `{
"t1": 1493851263,
"t2": 1.493851263e9
}`
var v struct {
T1 jsonTime `json:"t1"`
T2 jsonTime `json:"t2"`
}
if err := json.Unmarshal([]byte(data), &v); err != nil {
t.Fatal(err)
}
wantT1 := time.Unix(1493851263, 0)
wantT2 := time.Unix(1493851263, 0)
gotT1 := time.Time(v.T1)
gotT2 := time.Time(v.T2)
if !wantT1.Equal(gotT1) {
t.Errorf("t1 value: wanted %s got %s", wantT1, gotT1)
}
if !wantT2.Equal(gotT2) {
t.Errorf("t2 value: wanted %s got %s", wantT2, gotT2)
}
}
func encodeJWT(header, payload, sig string) string {
e := func(s string) string {
return base64.RawURLEncoding.EncodeToString([]byte(s))
}
return e(header) + "." + e(payload) + "." + e(sig)
}
func TestExpired(t *testing.T) {
now := time.Now()
nowFunc := func() time.Time { return now }
tests := []struct {
name string
idToken string
wantErr bool
wantExpired bool
}{
{
name: "valid",
idToken: encodeJWT(
"{}",
fmt.Sprintf(`{"exp":%d}`, now.Add(time.Hour).Unix()),
"blah", // signature isn't veified.
),
},
{
name: "expired",
idToken: encodeJWT(
"{}",
fmt.Sprintf(`{"exp":%d}`, now.Add(-time.Hour).Unix()),
"blah", // signature isn't veified.
),
wantExpired: true,
},
{
name: "bad exp claim",
idToken: encodeJWT(
"{}",
`{"exp":"foobar"}`,
"blah", // signature isn't veified.
),
wantErr: true,
},
{
name: "not an id token",
idToken: "notanidtoken",
wantErr: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
valid, err := idTokenExpired(nowFunc, test.idToken)
if err != nil {
if !test.wantErr {
t.Errorf("parse error: %v", err)
}
return
}
if test.wantExpired == valid {
t.Errorf("wanted expired %t, got %t", test.wantExpired, !valid)
}
})
}
}
func TestClientCache(t *testing.T) {
cache := newClientCache()
if _, ok := cache.getClient("issuer1", "id1"); ok {
t.Fatalf("got client before putting one in the cache")
}
cli1 := new(oidcAuthProvider)
cli2 := new(oidcAuthProvider)
gotcli := cache.setClient("issuer1", "id1", cli1)
if cli1 != gotcli {
t.Fatalf("set first client and got a different one")
}
gotcli = cache.setClient("issuer1", "id1", cli2)
if cli1 != gotcli {
t.Fatalf("set a second client and didn't get the first")
}
}

View File

@ -1,39 +0,0 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_test(
name = "go_default_test",
srcs = ["openstack_test.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/openstack",
library = ":go_default_library",
)
go_library(
name = "go_default_library",
srcs = ["openstack.go"],
importpath = "k8s.io/client-go/plugin/pkg/client/auth/openstack",
deps = [
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/github.com/gophercloud/gophercloud/openstack:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/net:go_default_library",
"//vendor/k8s.io/client-go/rest:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

View File

@ -1,166 +0,0 @@
/*
Copyright 2017 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 openstack
import (
"fmt"
"net/http"
"sync"
"time"
"github.com/golang/glog"
"github.com/gophercloud/gophercloud/openstack"
"k8s.io/apimachinery/pkg/util/net"
restclient "k8s.io/client-go/rest"
)
func init() {
if err := restclient.RegisterAuthProviderPlugin("openstack", newOpenstackAuthProvider); err != nil {
glog.Fatalf("Failed to register openstack auth plugin: %s", err)
}
}
// DefaultTTLDuration is the time before a token gets expired.
const DefaultTTLDuration = 10 * time.Minute
// openstackAuthProvider is an authprovider for openstack. this provider reads
// the environment variables to determine the client identity, and generates a
// token which will be inserted into the request header later.
type openstackAuthProvider struct {
ttl time.Duration
tokenGetter TokenGetter
}
// TokenGetter returns a bearer token that can be inserted into request.
type TokenGetter interface {
Token() (string, error)
}
type tokenGetter struct{}
// Token creates a token by authenticate with keystone.
func (*tokenGetter) Token() (string, error) {
options, err := openstack.AuthOptionsFromEnv()
if err != nil {
return "", fmt.Errorf("failed to read openstack env vars: %s", err)
}
client, err := openstack.AuthenticatedClient(options)
if err != nil {
return "", fmt.Errorf("authentication failed: %s", err)
}
return client.TokenID, nil
}
// cachedGetter caches a token until it gets expired, after the expiration, it will
// generate another token and cache it.
type cachedGetter struct {
mutex sync.Mutex
tokenGetter TokenGetter
token string
born time.Time
ttl time.Duration
}
// Token returns the current available token, create a new one if expired.
func (c *cachedGetter) Token() (string, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
var err error
// no token or exceeds the TTL
if c.token == "" || time.Now().Sub(c.born) > c.ttl {
c.token, err = c.tokenGetter.Token()
if err != nil {
return "", fmt.Errorf("failed to get token: %s", err)
}
c.born = time.Now()
}
return c.token, nil
}
// tokenRoundTripper implements the RoundTripper interface: adding the bearer token
// into the request header.
type tokenRoundTripper struct {
http.RoundTripper
tokenGetter TokenGetter
}
var _ net.RoundTripperWrapper = &tokenRoundTripper{}
// RoundTrip adds the bearer token into the request.
func (t *tokenRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// if the authorization header already present, use it.
if req.Header.Get("Authorization") != "" {
return t.RoundTripper.RoundTrip(req)
}
token, err := t.tokenGetter.Token()
if err == nil {
req.Header.Set("Authorization", "Bearer "+token)
} else {
glog.V(4).Infof("failed to get token: %s", err)
}
return t.RoundTripper.RoundTrip(req)
}
func (t *tokenRoundTripper) WrappedRoundTripper() http.RoundTripper { return t.RoundTripper }
// newOpenstackAuthProvider creates an auth provider which works with openstack
// environment.
func newOpenstackAuthProvider(clusterAddress string, config map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
var ttlDuration time.Duration
var err error
ttl, found := config["ttl"]
if !found {
ttlDuration = DefaultTTLDuration
// persist to config
config["ttl"] = ttlDuration.String()
if err = persister.Persist(config); err != nil {
return nil, fmt.Errorf("failed to persist config: %s", err)
}
} else {
ttlDuration, err = time.ParseDuration(ttl)
if err != nil {
return nil, fmt.Errorf("failed to parse ttl config: %s", err)
}
}
// TODO: read/persist client configuration(OS_XXX env vars) in config
return &openstackAuthProvider{
ttl: ttlDuration,
tokenGetter: &tokenGetter{},
}, nil
}
func (oap *openstackAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
return &tokenRoundTripper{
RoundTripper: rt,
tokenGetter: &cachedGetter{
tokenGetter: oap.tokenGetter,
ttl: oap.ttl,
},
}
}
func (oap *openstackAuthProvider) Login() error { return nil }

View File

@ -1,116 +0,0 @@
/*
Copyright 2017 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 openstack
import (
"math/rand"
"net/http"
"testing"
"time"
)
// testTokenGetter is a simple random token getter.
type testTokenGetter struct{}
const LetterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
func RandStringBytes(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = LetterBytes[rand.Intn(len(LetterBytes))]
}
return string(b)
}
func (*testTokenGetter) Token() (string, error) {
return RandStringBytes(32), nil
}
// testRoundTripper is mocked roundtripper which responds with unauthorized when
// there is no authorization header, otherwise returns status ok.
type testRoundTripper struct{}
func (trt *testRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
authHeader := req.Header.Get("Authorization")
if authHeader == "" || authHeader == "Bearer " {
return &http.Response{
StatusCode: http.StatusUnauthorized,
}, nil
}
return &http.Response{StatusCode: http.StatusOK}, nil
}
func TestOpenstackAuthProvider(t *testing.T) {
trt := &tokenRoundTripper{
RoundTripper: &testRoundTripper{},
}
tests := []struct {
name string
ttl time.Duration
interval time.Duration
same bool
}{
{
name: "normal",
ttl: 2 * time.Second,
interval: 1 * time.Second,
same: true,
},
{
name: "expire",
ttl: 1 * time.Second,
interval: 2 * time.Second,
same: false,
},
}
for _, test := range tests {
trt.tokenGetter = &cachedGetter{
tokenGetter: &testTokenGetter{},
ttl: test.ttl,
}
req, err := http.NewRequest(http.MethodPost, "https://test-api-server.com", nil)
if err != nil {
t.Errorf("failed to new request: %s", err)
}
trt.RoundTrip(req)
header := req.Header.Get("Authorization")
if header == "" {
t.Errorf("expect to see token in header, but is absent")
}
time.Sleep(test.interval)
req, err = http.NewRequest(http.MethodPost, "https://test-api-server.com", nil)
if err != nil {
t.Errorf("failed to new request: %s", err)
}
trt.RoundTrip(req)
newHeader := req.Header.Get("Authorization")
if newHeader == "" {
t.Errorf("expect to see token in header, but is absent")
}
same := newHeader == header
if same != test.same {
t.Errorf("expect to get %t when compare header, but saw %t", test.same, same)
}
}
}

View File

@ -1,25 +0,0 @@
/*
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 auth
import (
// Initialize all known client auth plugins.
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
_ "k8s.io/client-go/plugin/pkg/client/auth/openstack"
)