patch webhook authenticator to support token review with arbitrary audiences

pull/58/head
Mike Danese 2018-10-29 20:45:10 -07:00
parent ed17876e52
commit effad15ecc
5 changed files with 200 additions and 49 deletions

View File

@ -179,7 +179,7 @@ func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, er
tokenAuthenticators = append(tokenAuthenticators, oidcAuth) tokenAuthenticators = append(tokenAuthenticators, oidcAuth)
} }
if len(config.WebhookTokenAuthnConfigFile) > 0 { if len(config.WebhookTokenAuthnConfigFile) > 0 {
webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnCacheTTL) webhookTokenAuth, err := newWebhookTokenAuthenticator(config.WebhookTokenAuthnConfigFile, config.WebhookTokenAuthnCacheTTL, config.APIAudiences)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -318,8 +318,8 @@ func newAuthenticatorFromClientCAFile(clientCAFile string) (authenticator.Reques
return x509.New(opts, x509.CommonNameUserConversion), nil return x509.New(opts, x509.CommonNameUserConversion), nil
} }
func newWebhookTokenAuthenticator(webhookConfigFile string, ttl time.Duration) (authenticator.Token, error) { func newWebhookTokenAuthenticator(webhookConfigFile string, ttl time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) {
webhookTokenAuthenticator, err := webhook.New(webhookConfigFile) webhookTokenAuthenticator, err := webhook.New(webhookConfigFile, implicitAuds)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -51,6 +51,8 @@ type DelegatingAuthenticatorConfig struct {
// ClientCAFile is the CA bundle file used to authenticate client certificates // ClientCAFile is the CA bundle file used to authenticate client certificates
ClientCAFile string ClientCAFile string
APIAudiences authenticator.Audiences
RequestHeaderConfig *RequestHeaderConfig RequestHeaderConfig *RequestHeaderConfig
} }
@ -86,7 +88,7 @@ func (c DelegatingAuthenticatorConfig) New() (authenticator.Request, *spec.Secur
} }
if c.TokenAccessReviewClient != nil { if c.TokenAccessReviewClient != nil {
tokenAuth, err := webhooktoken.NewFromInterface(c.TokenAccessReviewClient) tokenAuth, err := webhooktoken.NewFromInterface(c.TokenAccessReviewClient, c.APIAudiences)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -21,8 +21,6 @@ import (
"context" "context"
"time" "time"
"k8s.io/klog"
authentication "k8s.io/api/authentication/v1beta1" authentication "k8s.io/api/authentication/v1beta1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
@ -31,6 +29,7 @@ import (
"k8s.io/apiserver/pkg/util/webhook" "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1" authenticationclient "k8s.io/client-go/kubernetes/typed/authentication/v1beta1"
"k8s.io/klog"
) )
var ( var (
@ -45,38 +44,58 @@ var _ authenticator.Token = (*WebhookTokenAuthenticator)(nil)
type WebhookTokenAuthenticator struct { type WebhookTokenAuthenticator struct {
tokenReview authenticationclient.TokenReviewInterface tokenReview authenticationclient.TokenReviewInterface
initialBackoff time.Duration initialBackoff time.Duration
implicitAuds authenticator.Audiences
} }
// NewFromInterface creates a webhook authenticator using the given tokenReview // NewFromInterface creates a webhook authenticator using the given tokenReview
// client. It is recommend to wrap this authenticator with the token cache // client. It is recommend to wrap this authenticator with the token cache
// authenticator implemented in // authenticator implemented in
// k8s.io/apiserver/pkg/authentication/token/cache. // k8s.io/apiserver/pkg/authentication/token/cache.
func NewFromInterface(tokenReview authenticationclient.TokenReviewInterface) (*WebhookTokenAuthenticator, error) { func NewFromInterface(tokenReview authenticationclient.TokenReviewInterface, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
return newWithBackoff(tokenReview, retryBackoff) return newWithBackoff(tokenReview, retryBackoff, implicitAuds)
} }
// New creates a new WebhookTokenAuthenticator from the provided kubeconfig file. // New creates a new WebhookTokenAuthenticator from the provided kubeconfig
func New(kubeConfigFile string) (*WebhookTokenAuthenticator, error) { // file. It is recommend to wrap this authenticator with the token cache
// authenticator implemented in
// k8s.io/apiserver/pkg/authentication/token/cache.
func New(kubeConfigFile string, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
tokenReview, err := tokenReviewInterfaceFromKubeconfig(kubeConfigFile) tokenReview, err := tokenReviewInterfaceFromKubeconfig(kubeConfigFile)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return newWithBackoff(tokenReview, retryBackoff) return newWithBackoff(tokenReview, retryBackoff, implicitAuds)
} }
// newWithBackoff allows tests to skip the sleep. // newWithBackoff allows tests to skip the sleep.
func newWithBackoff(tokenReview authenticationclient.TokenReviewInterface, initialBackoff time.Duration) (*WebhookTokenAuthenticator, error) { func newWithBackoff(tokenReview authenticationclient.TokenReviewInterface, initialBackoff time.Duration, implicitAuds authenticator.Audiences) (*WebhookTokenAuthenticator, error) {
return &WebhookTokenAuthenticator{tokenReview, initialBackoff}, nil return &WebhookTokenAuthenticator{tokenReview, initialBackoff, implicitAuds}, nil
} }
// AuthenticateToken implements the authenticator.Token interface. // AuthenticateToken implements the authenticator.Token interface.
func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) {
// We take implicit audiences of the API server at WebhookTokenAuthenticator
// construction time. The outline of how we validate audience here is:
//
// * if the ctx is not audience limited, don't do any audience validation.
// * if ctx is audience-limited, add the audiences to the tokenreview spec
// * if the tokenreview returns with audiences in the status that intersect
// with the audiences in the ctx, copy into the response and return success
// * if the tokenreview returns without an audience in the status, ensure
// the ctx audiences intersect with the implicit audiences, and set the
// intersection in the response.
// * otherwise return unauthenticated.
wantAuds, checkAuds := authenticator.AudiencesFrom(ctx)
r := &authentication.TokenReview{ r := &authentication.TokenReview{
Spec: authentication.TokenReviewSpec{Token: token}, Spec: authentication.TokenReviewSpec{
Token: token,
Audiences: wantAuds,
},
} }
var ( var (
result *authentication.TokenReview result *authentication.TokenReview
err error err error
auds authenticator.Audiences
) )
webhook.WithExponentialBackoff(w.initialBackoff, func() error { webhook.WithExponentialBackoff(w.initialBackoff, func() error {
result, err = w.tokenReview.Create(r) result, err = w.tokenReview.Create(r)
@ -87,6 +106,18 @@ func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token
klog.Errorf("Failed to make webhook authenticator request: %v", err) klog.Errorf("Failed to make webhook authenticator request: %v", err)
return nil, false, err return nil, false, err
} }
if checkAuds {
gotAuds := w.implicitAuds
if len(result.Status.Audiences) > 0 {
gotAuds = result.Status.Audiences
}
auds = wantAuds.Intersect(gotAuds)
if len(auds) == 0 {
return nil, false, nil
}
}
r.Status = result.Status r.Status = result.Status
if !r.Status.Authenticated { if !r.Status.Authenticated {
return nil, false, nil return nil, false, nil
@ -107,6 +138,7 @@ func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token
Groups: r.Status.User.Groups, Groups: r.Status.User.Groups,
Extra: extra, Extra: extra,
}, },
Audiences: auds,
}, true, nil }, true, nil
} }

View File

@ -39,6 +39,8 @@ import (
"k8s.io/client-go/tools/clientcmd/api/v1" "k8s.io/client-go/tools/clientcmd/api/v1"
) )
var apiAuds = authenticator.Audiences{"api"}
// Service mocks a remote authentication service. // Service mocks a remote authentication service.
type Service interface { type Service interface {
// Review looks at the TokenReviewSpec and provides an authentication // Review looks at the TokenReviewSpec and provides an authentication
@ -105,6 +107,7 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
type status struct { type status struct {
Authenticated bool `json:"authenticated"` Authenticated bool `json:"authenticated"`
User userInfo `json:"user"` User userInfo `json:"user"`
Audiences []string `json:"audiences"`
} }
var extra map[string][]string var extra map[string][]string
@ -130,6 +133,7 @@ func NewTestServer(s Service, cert, key, caCert []byte) (*httptest.Server, error
Groups: review.Status.User.Groups, Groups: review.Status.User.Groups,
Extra: extra, Extra: extra,
}, },
review.Status.Audiences,
}, },
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
@ -168,7 +172,7 @@ func (m *mockService) HTTPStatusCode() int { return m.statusCode }
// newTokenAuthenticator creates a temporary kubeconfig file from the provided // newTokenAuthenticator creates a temporary kubeconfig file from the provided
// arguments and attempts to load a new WebhookTokenAuthenticator from it. // arguments and attempts to load a new WebhookTokenAuthenticator from it.
func newTokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration) (authenticator.Token, error) { func newTokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, implicitAuds authenticator.Audiences) (authenticator.Token, error) {
tempfile, err := ioutil.TempFile("", "") tempfile, err := ioutil.TempFile("", "")
if err != nil { if err != nil {
return nil, err return nil, err
@ -196,7 +200,7 @@ func newTokenAuthenticator(serverURL string, clientCert, clientKey, ca []byte, c
return nil, err return nil, err
} }
authn, err := newWithBackoff(c, 0) authn, err := newWithBackoff(c, 0, implicitAuds)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -257,7 +261,7 @@ func TestTLSConfig(t *testing.T) {
} }
defer server.Close() defer server.Close()
wh, err := newTokenAuthenticator(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0) wh, err := newTokenAuthenticator(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, nil)
if err != nil { if err != nil {
t.Errorf("%s: failed to create client: %v", tt.test, err) t.Errorf("%s: failed to create client: %v", tt.test, err)
return return
@ -312,23 +316,21 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
} }
defer s.Close() defer s.Close()
wh, err := newTokenAuthenticator(s.URL, clientCert, clientKey, caCert, 0)
if err != nil {
t.Fatal(err)
}
expTypeMeta := metav1.TypeMeta{ expTypeMeta := metav1.TypeMeta{
APIVersion: "authentication.k8s.io/v1beta1", APIVersion: "authentication.k8s.io/v1beta1",
Kind: "TokenReview", Kind: "TokenReview",
} }
tests := []struct { tests := []struct {
description string
implicitAuds, reqAuds authenticator.Audiences
serverResponse v1beta1.TokenReviewStatus serverResponse v1beta1.TokenReviewStatus
expectedAuthenticated bool expectedAuthenticated bool
expectedUser *user.DefaultInfo expectedUser *user.DefaultInfo
expectedAuds authenticator.Audiences
}{ }{
// Successful response should pass through all user info.
{ {
description: "successful response should pass through all user info.",
serverResponse: v1beta1.TokenReviewStatus{ serverResponse: v1beta1.TokenReviewStatus{
Authenticated: true, Authenticated: true,
User: v1beta1.UserInfo{ User: v1beta1.UserInfo{
@ -341,6 +343,7 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
}, },
}, },
{ {
description: "successful response should pass through all user info.",
serverResponse: v1beta1.TokenReviewStatus{ serverResponse: v1beta1.TokenReviewStatus{
Authenticated: true, Authenticated: true,
User: v1beta1.UserInfo{ User: v1beta1.UserInfo{
@ -358,8 +361,8 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
Extra: map[string][]string{"foo": {"bar", "baz"}}, Extra: map[string][]string{"foo": {"bar", "baz"}},
}, },
}, },
// Unauthenticated shouldn't even include extra provided info.
{ {
description: "unauthenticated shouldn't even include extra provided info.",
serverResponse: v1beta1.TokenReviewStatus{ serverResponse: v1beta1.TokenReviewStatus{
Authenticated: false, Authenticated: false,
User: v1beta1.UserInfo{ User: v1beta1.UserInfo{
@ -372,37 +375,151 @@ func TestWebhookTokenAuthenticator(t *testing.T) {
expectedUser: nil, expectedUser: nil,
}, },
{ {
description: "unauthenticated shouldn't even include extra provided info.",
serverResponse: v1beta1.TokenReviewStatus{ serverResponse: v1beta1.TokenReviewStatus{
Authenticated: false, Authenticated: false,
}, },
expectedAuthenticated: false, expectedAuthenticated: false,
expectedUser: nil, expectedUser: nil,
}, },
{
description: "good audience",
implicitAuds: apiAuds,
reqAuds: apiAuds,
serverResponse: v1beta1.TokenReviewStatus{
Authenticated: true,
User: v1beta1.UserInfo{
Username: "somebody",
},
},
expectedAuthenticated: true,
expectedUser: &user.DefaultInfo{
Name: "somebody",
},
expectedAuds: apiAuds,
},
{
description: "good audience",
implicitAuds: append(apiAuds, "other"),
reqAuds: apiAuds,
serverResponse: v1beta1.TokenReviewStatus{
Authenticated: true,
User: v1beta1.UserInfo{
Username: "somebody",
},
},
expectedAuthenticated: true,
expectedUser: &user.DefaultInfo{
Name: "somebody",
},
expectedAuds: apiAuds,
},
{
description: "bad audiences",
implicitAuds: apiAuds,
reqAuds: authenticator.Audiences{"other"},
serverResponse: v1beta1.TokenReviewStatus{
Authenticated: false,
},
expectedAuthenticated: false,
},
{
description: "bad audiences",
implicitAuds: apiAuds,
reqAuds: authenticator.Audiences{"other"},
// webhook authenticator hasn't been upgraded to support audience.
serverResponse: v1beta1.TokenReviewStatus{
Authenticated: true,
User: v1beta1.UserInfo{
Username: "somebody",
},
},
expectedAuthenticated: false,
},
{
description: "audience aware backend",
implicitAuds: apiAuds,
reqAuds: apiAuds,
serverResponse: v1beta1.TokenReviewStatus{
Authenticated: true,
User: v1beta1.UserInfo{
Username: "somebody",
},
Audiences: []string(apiAuds),
},
expectedAuthenticated: true,
expectedUser: &user.DefaultInfo{
Name: "somebody",
},
expectedAuds: apiAuds,
},
{
description: "audience aware backend",
serverResponse: v1beta1.TokenReviewStatus{
Authenticated: true,
User: v1beta1.UserInfo{
Username: "somebody",
},
Audiences: []string(apiAuds),
},
expectedAuthenticated: true,
expectedUser: &user.DefaultInfo{
Name: "somebody",
},
},
{
description: "audience aware backend",
implicitAuds: apiAuds,
reqAuds: apiAuds,
serverResponse: v1beta1.TokenReviewStatus{
Authenticated: true,
User: v1beta1.UserInfo{
Username: "somebody",
},
Audiences: []string{"other"},
},
expectedAuthenticated: false,
},
} }
token := "my-s3cr3t-t0ken" token := "my-s3cr3t-t0ken"
for i, tt := range tests { for _, tt := range tests {
serv.response = tt.serverResponse t.Run(tt.description, func(t *testing.T) {
resp, authenticated, err := wh.AuthenticateToken(context.Background(), token) wh, err := newTokenAuthenticator(s.URL, clientCert, clientKey, caCert, 0, tt.implicitAuds)
if err != nil { if err != nil {
t.Errorf("case %d: authentication failed: %v", i, err) t.Fatal(err)
continue }
}
if serv.lastRequest.Spec.Token != token { ctx := context.Background()
t.Errorf("case %d: Server did not see correct token. Got %q, expected %q.", if tt.reqAuds != nil {
i, serv.lastRequest.Spec.Token, token) ctx = authenticator.WithAudiences(ctx, tt.reqAuds)
} }
if !reflect.DeepEqual(serv.lastRequest.TypeMeta, expTypeMeta) {
t.Errorf("case %d: Server did not see correct TypeMeta. Got %v, expected %v", serv.response = tt.serverResponse
i, serv.lastRequest.TypeMeta, expTypeMeta) resp, authenticated, err := wh.AuthenticateToken(ctx, token)
} if err != nil {
if authenticated != tt.expectedAuthenticated { t.Fatalf("authentication failed: %v", err)
t.Errorf("case %d: Plugin returned incorrect authentication response. Got %t, expected %t.", }
i, authenticated, tt.expectedAuthenticated) if serv.lastRequest.Spec.Token != token {
} t.Errorf("Server did not see correct token. Got %q, expected %q.",
if resp != nil && tt.expectedUser != nil && !reflect.DeepEqual(resp.User, tt.expectedUser) { serv.lastRequest.Spec.Token, token)
t.Errorf("case %d: Plugin returned incorrect user. Got %#v, expected %#v", }
i, resp.User, tt.expectedUser) if !reflect.DeepEqual(serv.lastRequest.TypeMeta, expTypeMeta) {
} t.Errorf("Server did not see correct TypeMeta. Got %v, expected %v",
serv.lastRequest.TypeMeta, expTypeMeta)
}
if authenticated != tt.expectedAuthenticated {
t.Errorf("Plugin returned incorrect authentication response. Got %t, expected %t.",
authenticated, tt.expectedAuthenticated)
}
if resp != nil && tt.expectedUser != nil && !reflect.DeepEqual(resp.User, tt.expectedUser) {
t.Errorf("Plugin returned incorrect user. Got %#v, expected %#v",
resp.User, tt.expectedUser)
}
if resp != nil && tt.expectedAuds != nil && !reflect.DeepEqual(resp.Audiences, tt.expectedAuds) {
t.Errorf("Plugin returned incorrect audiences. Got %#v, expected %#v",
resp.Audiences, tt.expectedAuds)
}
})
} }
} }
@ -440,7 +557,7 @@ func TestWebhookCacheAndRetry(t *testing.T) {
defer s.Close() defer s.Close()
// Create an authenticator that caches successful responses "forever" (100 days). // Create an authenticator that caches successful responses "forever" (100 days).
wh, err := newTokenAuthenticator(s.URL, clientCert, clientKey, caCert, 2400*time.Hour) wh, err := newTokenAuthenticator(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -85,7 +85,7 @@ func getTestWebhookTokenAuth(serverURL string) (authenticator.Request, error) {
if err := json.NewEncoder(kubecfgFile).Encode(config); err != nil { if err := json.NewEncoder(kubecfgFile).Encode(config); err != nil {
return nil, err return nil, err
} }
webhookTokenAuth, err := webhook.New(kubecfgFile.Name()) webhookTokenAuth, err := webhook.New(kubecfgFile.Name(), nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }