diff --git a/pkg/authentication/group/group_adder.go b/pkg/authentication/group/group_adder.go new file mode 100644 index 000000000..1f71429b4 --- /dev/null +++ b/pkg/authentication/group/group_adder.go @@ -0,0 +1,50 @@ +/* +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 group + +import ( + "net/http" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +// GroupAdder adds groups to an authenticated user.Info +type GroupAdder struct { + // Authenticator is delegated to make the authentication decision + Authenticator authenticator.Request + // Groups are additional groups to add to the user.Info from a successful authentication + Groups []string +} + +// NewGroupAdder wraps a request authenticator, and adds the specified groups to the returned user when authentication succeeds +func NewGroupAdder(auth authenticator.Request, groups []string) authenticator.Request { + return &GroupAdder{auth, groups} +} + +func (g *GroupAdder) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + u, ok, err := g.Authenticator.AuthenticateRequest(req) + if err != nil || !ok { + return nil, ok, err + } + return &user.DefaultInfo{ + Name: u.GetName(), + UID: u.GetUID(), + Groups: append(u.GetGroups(), g.Groups...), + Extra: u.GetExtra(), + }, true, nil +} diff --git a/pkg/authentication/group/group_adder_test.go b/pkg/authentication/group/group_adder_test.go new file mode 100644 index 000000000..886f07c36 --- /dev/null +++ b/pkg/authentication/group/group_adder_test.go @@ -0,0 +1,42 @@ +/* +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 group + +import ( + "net/http" + "reflect" + "testing" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +func TestGroupAdder(t *testing.T) { + adder := authenticator.Request( + NewGroupAdder( + authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) { + return &user.DefaultInfo{Name: "user", Groups: []string{"original"}}, true, nil + }), + []string{"added"}, + ), + ) + + user, _, _ := adder.AuthenticateRequest(nil) + if !reflect.DeepEqual(user.GetGroups(), []string{"original", "added"}) { + t.Errorf("Expected original,added groups, got %#v", user.GetGroups()) + } +} diff --git a/pkg/authentication/request/anonymous/anonymous.go b/pkg/authentication/request/anonymous/anonymous.go new file mode 100644 index 000000000..a6d22942a --- /dev/null +++ b/pkg/authentication/request/anonymous/anonymous.go @@ -0,0 +1,36 @@ +/* +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 anonymous + +import ( + "net/http" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +const ( + anonymousUser = user.Anonymous + + unauthenticatedGroup = user.AllUnauthenticated +) + +func NewAuthenticator() authenticator.Request { + return authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) { + return &user.DefaultInfo{Name: anonymousUser, Groups: []string{unauthenticatedGroup}}, true, nil + }) +} diff --git a/pkg/authentication/request/anonymous/anonymous_test.go b/pkg/authentication/request/anonymous/anonymous_test.go new file mode 100644 index 000000000..55485e2b8 --- /dev/null +++ b/pkg/authentication/request/anonymous/anonymous_test.go @@ -0,0 +1,42 @@ +/* +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 anonymous + +import ( + "testing" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +func TestAnonymous(t *testing.T) { + var a authenticator.Request = NewAuthenticator() + u, ok, err := a.AuthenticateRequest(nil) + if err != nil { + t.Fatalf("Unexpected error %v", err) + } + if !ok { + t.Fatalf("Unexpectedly unauthenticated") + } + if u.GetName() != user.Anonymous { + t.Fatalf("Expected username %s, got %s", user.Anonymous, u.GetName()) + } + if !sets.NewString(u.GetGroups()...).Equal(sets.NewString(user.AllUnauthenticated)) { + t.Fatalf("Expected group %s, got %v", user.AllUnauthenticated, u.GetGroups()) + } +} diff --git a/pkg/authentication/request/bearertoken/bearertoken.go b/pkg/authentication/request/bearertoken/bearertoken.go new file mode 100644 index 000000000..f4fff22d0 --- /dev/null +++ b/pkg/authentication/request/bearertoken/bearertoken.go @@ -0,0 +1,63 @@ +/* +Copyright 2014 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 bearertoken + +import ( + "errors" + "net/http" + "strings" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +type Authenticator struct { + auth authenticator.Token +} + +func New(auth authenticator.Token) *Authenticator { + return &Authenticator{auth} +} + +var invalidToken = errors.New("invalid bearer token") + +func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + auth := strings.TrimSpace(req.Header.Get("Authorization")) + if auth == "" { + return nil, false, nil + } + parts := strings.Split(auth, " ") + if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" { + return nil, false, nil + } + + token := parts[1] + + // Empty bearer tokens aren't valid + if len(token) == 0 { + return nil, false, nil + } + + user, ok, err := a.auth.AuthenticateToken(token) + + // If the token authenticator didn't error, provide a default error + if !ok && err == nil { + err = invalidToken + } + + return user, ok, err +} diff --git a/pkg/authentication/request/bearertoken/bearertoken_test.go b/pkg/authentication/request/bearertoken/bearertoken_test.go new file mode 100644 index 000000000..f48b1b787 --- /dev/null +++ b/pkg/authentication/request/bearertoken/bearertoken_test.go @@ -0,0 +1,105 @@ +/* +Copyright 2014 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 bearertoken + +import ( + "errors" + "net/http" + "testing" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +func TestAuthenticateRequest(t *testing.T) { + auth := New(authenticator.TokenFunc(func(token string) (user.Info, bool, error) { + if token != "token" { + t.Errorf("unexpected token: %s", token) + } + return &user.DefaultInfo{Name: "user"}, true, nil + })) + user, ok, err := auth.AuthenticateRequest(&http.Request{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + }) + if !ok || user == nil || err != nil { + t.Errorf("expected valid user") + } +} + +func TestAuthenticateRequestTokenInvalid(t *testing.T) { + auth := New(authenticator.TokenFunc(func(token string) (user.Info, bool, error) { + return nil, false, nil + })) + user, ok, err := auth.AuthenticateRequest(&http.Request{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + }) + if ok || user != nil { + t.Errorf("expected not authenticated user") + } + if err != invalidToken { + t.Errorf("expected invalidToken error, got %v", err) + } +} + +func TestAuthenticateRequestTokenInvalidCustomError(t *testing.T) { + customError := errors.New("custom") + auth := New(authenticator.TokenFunc(func(token string) (user.Info, bool, error) { + return nil, false, customError + })) + user, ok, err := auth.AuthenticateRequest(&http.Request{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + }) + if ok || user != nil { + t.Errorf("expected not authenticated user") + } + if err != customError { + t.Errorf("expected custom error, got %v", err) + } +} + +func TestAuthenticateRequestTokenError(t *testing.T) { + auth := New(authenticator.TokenFunc(func(token string) (user.Info, bool, error) { + return nil, false, errors.New("error") + })) + user, ok, err := auth.AuthenticateRequest(&http.Request{ + Header: http.Header{"Authorization": []string{"Bearer token"}}, + }) + if ok || user != nil || err == nil { + t.Errorf("expected error") + } +} + +func TestAuthenticateRequestBadValue(t *testing.T) { + testCases := []struct { + Req *http.Request + }{ + {Req: &http.Request{}}, + {Req: &http.Request{Header: http.Header{"Authorization": []string{"Bearer"}}}}, + {Req: &http.Request{Header: http.Header{"Authorization": []string{"bear token"}}}}, + {Req: &http.Request{Header: http.Header{"Authorization": []string{"Bearer: token"}}}}, + } + for i, testCase := range testCases { + auth := New(authenticator.TokenFunc(func(token string) (user.Info, bool, error) { + t.Errorf("authentication should not have been called") + return nil, false, nil + })) + user, ok, err := auth.AuthenticateRequest(testCase.Req) + if ok || user != nil || err != nil { + t.Errorf("%d: expected not authenticated (no token)", i) + } + } +} diff --git a/pkg/authentication/request/union/union.go b/pkg/authentication/request/union/union.go new file mode 100644 index 000000000..e5b43ab05 --- /dev/null +++ b/pkg/authentication/request/union/union.go @@ -0,0 +1,72 @@ +/* +Copyright 2014 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 union + +import ( + "net/http" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" + utilerrors "k8s.io/client-go/pkg/util/errors" +) + +// unionAuthRequestHandler authenticates requests using a chain of authenticator.Requests +type unionAuthRequestHandler struct { + // Handlers is a chain of request authenticators to delegate to + Handlers []authenticator.Request + // FailOnError determines whether an error returns short-circuits the chain + FailOnError bool +} + +// New returns a request authenticator that validates credentials using a chain of authenticator.Request objects. +// The entire chain is tried until one succeeds. If all fail, an aggregate error is returned. +func New(authRequestHandlers ...authenticator.Request) authenticator.Request { + if len(authRequestHandlers) == 1 { + return authRequestHandlers[0] + } + return &unionAuthRequestHandler{Handlers: authRequestHandlers, FailOnError: false} +} + +// NewFailOnError returns a request authenticator that validates credentials using a chain of authenticator.Request objects. +// The first error short-circuits the chain. +func NewFailOnError(authRequestHandlers ...authenticator.Request) authenticator.Request { + if len(authRequestHandlers) == 1 { + return authRequestHandlers[0] + } + return &unionAuthRequestHandler{Handlers: authRequestHandlers, FailOnError: true} +} + +// AuthenticateRequest authenticates the request using a chain of authenticator.Request objects. +func (authHandler *unionAuthRequestHandler) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + var errlist []error + for _, currAuthRequestHandler := range authHandler.Handlers { + info, ok, err := currAuthRequestHandler.AuthenticateRequest(req) + if err != nil { + if authHandler.FailOnError { + return info, ok, err + } + errlist = append(errlist, err) + continue + } + + if ok { + return info, ok, err + } + } + + return nil, false, utilerrors.NewAggregate(errlist) +} diff --git a/pkg/authentication/request/union/unionauth_test.go b/pkg/authentication/request/union/unionauth_test.go new file mode 100644 index 000000000..0d3f1a7cf --- /dev/null +++ b/pkg/authentication/request/union/unionauth_test.go @@ -0,0 +1,166 @@ +/* +Copyright 2014 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 union + +import ( + "errors" + "net/http" + "reflect" + "strings" + "testing" + + "k8s.io/apiserver/pkg/authentication/user" +) + +type mockAuthRequestHandler struct { + returnUser user.Info + isAuthenticated bool + err error +} + +var ( + user1 = &user.DefaultInfo{Name: "fresh_ferret", UID: "alfa"} + user2 = &user.DefaultInfo{Name: "elegant_sheep", UID: "bravo"} +) + +func (mock *mockAuthRequestHandler) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + return mock.returnUser, mock.isAuthenticated, mock.err +} + +func TestAuthenticateRequestSecondPasses(t *testing.T) { + handler1 := &mockAuthRequestHandler{returnUser: user1} + handler2 := &mockAuthRequestHandler{returnUser: user2, isAuthenticated: true} + authRequestHandler := New(handler1, handler2) + req, _ := http.NewRequest("GET", "http://example.org", nil) + + authenticatedUser, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if !isAuthenticated { + t.Errorf("Unexpectedly unauthenticated: %v", isAuthenticated) + } + if !reflect.DeepEqual(user2, authenticatedUser) { + t.Errorf("Expected %v, got %v", user2, authenticatedUser) + } +} + +func TestAuthenticateRequestFirstPasses(t *testing.T) { + handler1 := &mockAuthRequestHandler{returnUser: user1, isAuthenticated: true} + handler2 := &mockAuthRequestHandler{returnUser: user2} + authRequestHandler := New(handler1, handler2) + req, _ := http.NewRequest("GET", "http://example.org", nil) + + authenticatedUser, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if !isAuthenticated { + t.Errorf("Unexpectedly unauthenticated: %v", isAuthenticated) + } + if !reflect.DeepEqual(user1, authenticatedUser) { + t.Errorf("Expected %v, got %v", user1, authenticatedUser) + } +} + +func TestAuthenticateRequestSuppressUnnecessaryErrors(t *testing.T) { + handler1 := &mockAuthRequestHandler{err: errors.New("first")} + handler2 := &mockAuthRequestHandler{isAuthenticated: true} + authRequestHandler := New(handler1, handler2) + req, _ := http.NewRequest("GET", "http://example.org", nil) + + _, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if !isAuthenticated { + t.Errorf("Unexpectedly unauthenticated: %v", isAuthenticated) + } +} + +func TestAuthenticateRequestNoAuthenticators(t *testing.T) { + authRequestHandler := New() + req, _ := http.NewRequest("GET", "http://example.org", nil) + + authenticatedUser, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if isAuthenticated { + t.Errorf("Unexpectedly authenticated: %v", isAuthenticated) + } + if authenticatedUser != nil { + t.Errorf("Unexpected authenticatedUser: %v", authenticatedUser) + } +} + +func TestAuthenticateRequestNonePass(t *testing.T) { + handler1 := &mockAuthRequestHandler{} + handler2 := &mockAuthRequestHandler{} + authRequestHandler := New(handler1, handler2) + req, _ := http.NewRequest("GET", "http://example.org", nil) + + _, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if isAuthenticated { + t.Errorf("Unexpectedly authenticated: %v", isAuthenticated) + } +} + +func TestAuthenticateRequestAdditiveErrors(t *testing.T) { + handler1 := &mockAuthRequestHandler{err: errors.New("first")} + handler2 := &mockAuthRequestHandler{err: errors.New("second")} + authRequestHandler := New(handler1, handler2) + req, _ := http.NewRequest("GET", "http://example.org", nil) + + _, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req) + if err == nil { + t.Errorf("Expected an error") + } + if !strings.Contains(err.Error(), "first") { + t.Errorf("Expected error containing %v, got %v", "first", err) + } + if !strings.Contains(err.Error(), "second") { + t.Errorf("Expected error containing %v, got %v", "second", err) + } + if isAuthenticated { + t.Errorf("Unexpectedly authenticated: %v", isAuthenticated) + } +} + +func TestAuthenticateRequestFailEarly(t *testing.T) { + handler1 := &mockAuthRequestHandler{err: errors.New("first")} + handler2 := &mockAuthRequestHandler{err: errors.New("second")} + authRequestHandler := NewFailOnError(handler1, handler2) + req, _ := http.NewRequest("GET", "http://example.org", nil) + + _, isAuthenticated, err := authRequestHandler.AuthenticateRequest(req) + if err == nil { + t.Errorf("Expected an error") + } + if !strings.Contains(err.Error(), "first") { + t.Errorf("Expected error containing %v, got %v", "first", err) + } + if strings.Contains(err.Error(), "second") { + t.Errorf("Did not expect second error, got %v", err) + } + if isAuthenticated { + t.Errorf("Unexpectedly authenticated: %v", isAuthenticated) + } +} diff --git a/pkg/authentication/request/x509/doc.go b/pkg/authentication/request/x509/doc.go new file mode 100644 index 000000000..807b016fd --- /dev/null +++ b/pkg/authentication/request/x509/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 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 x509 provides a request authenticator that validates and +// extracts user information from client certificates +package x509 // import "k8s.io/kubernetes/plugin/pkg/auth/authenticator/request/x509" diff --git a/pkg/authentication/request/x509/testdata/client-expired.pem b/pkg/authentication/request/x509/testdata/client-expired.pem new file mode 100644 index 000000000..1c33f4618 --- /dev/null +++ b/pkg/authentication/request/x509/testdata/client-expired.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBpTCCAUugAwIBAgIUPV4LAC5KK8YWY1FegyTuhkGUr3EwCgYIKoZIzj0EAwIw +GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMB4XDTkwMTIzMTIzNTkwMFoXDTkw +MTIzMTIzNTkwMFowFDESMBAGA1UEAxMJTXkgQ2xpZW50MFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEyYUnseNUN87rfHgekrfZu5sj4wlt5LYr3JYZZkfSbsb+BW3/ +RzX02ifjp+8w7mI4qUGg6y6J7oXHGFT3uj9kj6N1MHMwDgYDVR0PAQH/BAQDAgWg +MBMGA1UdJQQMMAoGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFKsX +EnXwDg8j2LIEM1QzmFrE6537MB8GA1UdIwQYMBaAFF+p0JcY31pz+mjNZnjv0Gum +92vZMAoGCCqGSM49BAMCA0gAMEUCIG4FBcb57oqOCoaFiJ+Yx6S0zkaash7bTv3V +CIy9JvFdAiEAy8bf2S9EkvZyURZ6ycgEMnekll57Ebze6rjlPx8+B1Y= +-----END CERTIFICATE----- diff --git a/pkg/authentication/request/x509/testdata/client-valid.pem b/pkg/authentication/request/x509/testdata/client-valid.pem new file mode 100644 index 000000000..620483f8a --- /dev/null +++ b/pkg/authentication/request/x509/testdata/client-valid.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw +GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx +MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb +KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC +BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU +K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q +a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5 +MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps= +-----END CERTIFICATE----- diff --git a/pkg/authentication/request/x509/testdata/client.config.json b/pkg/authentication/request/x509/testdata/client.config.json new file mode 100644 index 000000000..57f012b7a --- /dev/null +++ b/pkg/authentication/request/x509/testdata/client.config.json @@ -0,0 +1,24 @@ +{ + "signing": { + "profiles": { + "valid": { + "expiry": "876000h", + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + }, + "expired": { + "expiry": "1h", + "not_before": "1990-12-31T23:59:00Z", + "not_after": "1990-12-31T23:59:00Z", + "usages": [ + "signing", + "key encipherment", + "client auth" + ] + } + } + } +} \ No newline at end of file diff --git a/pkg/authentication/request/x509/testdata/client.csr.json b/pkg/authentication/request/x509/testdata/client.csr.json new file mode 100644 index 000000000..17b45773c --- /dev/null +++ b/pkg/authentication/request/x509/testdata/client.csr.json @@ -0,0 +1,3 @@ +{ + "CN": "My Client" +} \ No newline at end of file diff --git a/pkg/authentication/request/x509/testdata/generate.sh b/pkg/authentication/request/x509/testdata/generate.sh new file mode 100755 index 000000000..07171057d --- /dev/null +++ b/pkg/authentication/request/x509/testdata/generate.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +# 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. + +cfssl gencert -initca root.csr.json | cfssljson -bare root + +cfssl gencert -initca intermediate.csr.json | cfssljson -bare intermediate +cfssl sign -ca root.pem -ca-key root-key.pem -config intermediate.config.json intermediate.csr | cfssljson -bare intermediate + +cfssl gencert -ca intermediate.pem -ca-key intermediate-key.pem -config client.config.json --profile=valid client.csr.json | cfssljson -bare client-valid +cfssl gencert -ca intermediate.pem -ca-key intermediate-key.pem -config client.config.json --profile=expired client.csr.json | cfssljson -bare client-expired + diff --git a/pkg/authentication/request/x509/testdata/intermediate.config.json b/pkg/authentication/request/x509/testdata/intermediate.config.json new file mode 100644 index 000000000..94f9da4db --- /dev/null +++ b/pkg/authentication/request/x509/testdata/intermediate.config.json @@ -0,0 +1,18 @@ +{ + "signing": { + "default": { + "usages": [ + "digital signature", + "cert sign", + "crl sign", + "signing", + "key encipherment", + "client auth" + ], + "expiry": "876000h", + "ca_constraint": { + "is_ca": true + } + } + } +} \ No newline at end of file diff --git a/pkg/authentication/request/x509/testdata/intermediate.csr.json b/pkg/authentication/request/x509/testdata/intermediate.csr.json new file mode 100644 index 000000000..29d684b8e --- /dev/null +++ b/pkg/authentication/request/x509/testdata/intermediate.csr.json @@ -0,0 +1,6 @@ +{ + "CN": "Intermediate-CA", + "ca": { + "expiry": "876000h" + } +} \ No newline at end of file diff --git a/pkg/authentication/request/x509/testdata/intermediate.pem b/pkg/authentication/request/x509/testdata/intermediate.pem new file mode 100644 index 000000000..7f157d5b3 --- /dev/null +++ b/pkg/authentication/request/x509/testdata/intermediate.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBqDCCAU6gAwIBAgIUfqZtjoFgczZ+oQZbEC/BDSS2J6wwCgYIKoZIzj0EAwIw +EjEQMA4GA1UEAxMHUm9vdC1DQTAgFw0xNjEwMTEwNTA2MDBaGA8yMTE2MDkxNzA1 +MDYwMFowGjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMFkwEwYHKoZIzj0CAQYI +KoZIzj0DAQcDQgAEyWHEMMCctJg8Xa5YWLqaCPbk3MjB+uvXac42JM9pj4k9jedD +kpUJRkWIPzgJI8Zk/3cSzluUTixP6JBSDKtwwaN4MHYwDgYDVR0PAQH/BAQDAgGm +MBMGA1UdJQQMMAoGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FF+p0JcY31pz+mjNZnjv0Gum92vZMB8GA1UdIwQYMBaAFB7P6+i4/pfNjqZgJv/b +dgA7Fe4tMAoGCCqGSM49BAMCA0gAMEUCIQCTT1YWQZaAqfQ2oBxzOkJE2BqLFxhz +3smQlrZ5gCHddwIgcvT7puhYOzAgcvMn9+SZ1JOyZ7edODjshCVCRnuHK2c= +-----END CERTIFICATE----- diff --git a/pkg/authentication/request/x509/testdata/root.csr.json b/pkg/authentication/request/x509/testdata/root.csr.json new file mode 100644 index 000000000..3b509d73e --- /dev/null +++ b/pkg/authentication/request/x509/testdata/root.csr.json @@ -0,0 +1,6 @@ +{ + "CN": "Root-CA", + "ca": { + "expiry": "876000h" + } +} \ No newline at end of file diff --git a/pkg/authentication/request/x509/testdata/root.pem b/pkg/authentication/request/x509/testdata/root.pem new file mode 100644 index 000000000..1eed53878 --- /dev/null +++ b/pkg/authentication/request/x509/testdata/root.pem @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBizCCATGgAwIBAgIUH4plk9qwD61FVXgiOTngFU5FeSkwCgYIKoZIzj0EAwIw +EjEQMA4GA1UEAxMHUm9vdC1DQTAgFw0xNjEwMTEwNTA2MDBaGA8yMTE2MDkxNzA1 +MDYwMFowEjEQMA4GA1UEAxMHUm9vdC1DQTBZMBMGByqGSM49AgEGCCqGSM49AwEH +A0IABI2CsrAnMGT8P2VGU2MLo5pv86Z74kcV9hgkLJUkSaeNyc1s89w7X5V2wvwu +iWEJRGm5RoZJausmyZLZEoKEVXejYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBQez+vouP6XzY6mYCb/23YAOxXuLTAfBgNVHSME +GDAWgBQez+vouP6XzY6mYCb/23YAOxXuLTAKBggqhkjOPQQDAgNIADBFAiBGclts +vJRM+QMVoV/1L9b+hvhgLIp/OupUFsSOReefIwIhALY06hBklyh8eFwuBtyX2VcE +8xlVn4/5idUvc3Xv2h9s +-----END CERTIFICATE----- diff --git a/pkg/authentication/request/x509/x509.go b/pkg/authentication/request/x509/x509.go new file mode 100644 index 000000000..00a701636 --- /dev/null +++ b/pkg/authentication/request/x509/x509.go @@ -0,0 +1,185 @@ +/* +Copyright 2014 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 x509 + +import ( + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "fmt" + "net/http" + + "github.com/golang/glog" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" + utilerrors "k8s.io/client-go/pkg/util/errors" + "k8s.io/client-go/pkg/util/sets" +) + +// UserConversion defines an interface for extracting user info from a client certificate chain +type UserConversion interface { + User(chain []*x509.Certificate) (user.Info, bool, error) +} + +// UserConversionFunc is a function that implements the UserConversion interface. +type UserConversionFunc func(chain []*x509.Certificate) (user.Info, bool, error) + +// User implements x509.UserConversion +func (f UserConversionFunc) User(chain []*x509.Certificate) (user.Info, bool, error) { + return f(chain) +} + +// Authenticator implements request.Authenticator by extracting user info from verified client certificates +type Authenticator struct { + opts x509.VerifyOptions + user UserConversion +} + +// New returns a request.Authenticator that verifies client certificates using the provided +// VerifyOptions, and converts valid certificate chains into user.Info using the provided UserConversion +func New(opts x509.VerifyOptions, user UserConversion) *Authenticator { + return &Authenticator{opts, user} +} + +// AuthenticateRequest authenticates the request using presented client certificates +func (a *Authenticator) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 { + return nil, false, nil + } + + // Use intermediates, if provided + optsCopy := a.opts + if optsCopy.Intermediates == nil && len(req.TLS.PeerCertificates) > 1 { + optsCopy.Intermediates = x509.NewCertPool() + for _, intermediate := range req.TLS.PeerCertificates[1:] { + optsCopy.Intermediates.AddCert(intermediate) + } + } + + chains, err := req.TLS.PeerCertificates[0].Verify(optsCopy) + if err != nil { + return nil, false, err + } + + var errlist []error + for _, chain := range chains { + user, ok, err := a.user.User(chain) + if err != nil { + errlist = append(errlist, err) + continue + } + + if ok { + return user, ok, err + } + } + return nil, false, utilerrors.NewAggregate(errlist) +} + +// Verifier implements request.Authenticator by verifying a client cert on the request, then delegating to the wrapped auth +type Verifier struct { + opts x509.VerifyOptions + auth authenticator.Request + + // allowedCommonNames contains the common names which a verified certificate is allowed to have. + // If empty, all verified certificates are allowed. + allowedCommonNames sets.String +} + +// NewVerifier create a request.Authenticator by verifying a client cert on the request, then delegating to the wrapped auth +func NewVerifier(opts x509.VerifyOptions, auth authenticator.Request, allowedCommonNames sets.String) authenticator.Request { + return &Verifier{opts, auth, allowedCommonNames} +} + +// AuthenticateRequest verifies the presented client certificate, then delegates to the wrapped auth +func (a *Verifier) AuthenticateRequest(req *http.Request) (user.Info, bool, error) { + if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 { + return nil, false, nil + } + + // Use intermediates, if provided + optsCopy := a.opts + if optsCopy.Intermediates == nil && len(req.TLS.PeerCertificates) > 1 { + optsCopy.Intermediates = x509.NewCertPool() + for _, intermediate := range req.TLS.PeerCertificates[1:] { + optsCopy.Intermediates.AddCert(intermediate) + } + } + + if _, err := req.TLS.PeerCertificates[0].Verify(optsCopy); err != nil { + return nil, false, err + } + if err := a.verifySubject(req.TLS.PeerCertificates[0].Subject); err != nil { + return nil, false, err + } + return a.auth.AuthenticateRequest(req) +} + +func (a *Verifier) verifySubject(subject pkix.Name) error { + // No CN restrictions + if len(a.allowedCommonNames) == 0 { + return nil + } + // Enforce CN restrictions + if a.allowedCommonNames.Has(subject.CommonName) { + return nil + } + glog.Warningf("x509: subject with cn=%s is not in the allowed list: %v", subject.CommonName, a.allowedCommonNames.List()) + return fmt.Errorf("x509: subject with cn=%s is not allowed", subject.CommonName) +} + +// DefaultVerifyOptions returns VerifyOptions that use the system root certificates, current time, +// and requires certificates to be valid for client auth (x509.ExtKeyUsageClientAuth) +func DefaultVerifyOptions() x509.VerifyOptions { + return x509.VerifyOptions{ + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } +} + +// CommonNameUserConversion builds user info from a certificate chain using the subject's CommonName +var CommonNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (user.Info, bool, error) { + if len(chain[0].Subject.CommonName) == 0 { + return nil, false, nil + } + return &user.DefaultInfo{ + Name: chain[0].Subject.CommonName, + Groups: chain[0].Subject.Organization, + }, true, nil +}) + +// DNSNameUserConversion builds user info from a certificate chain using the first DNSName on the certificate +var DNSNameUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (user.Info, bool, error) { + if len(chain[0].DNSNames) == 0 { + return nil, false, nil + } + return &user.DefaultInfo{Name: chain[0].DNSNames[0]}, true, nil +}) + +// EmailAddressUserConversion builds user info from a certificate chain using the first EmailAddress on the certificate +var EmailAddressUserConversion = UserConversionFunc(func(chain []*x509.Certificate) (user.Info, bool, error) { + var emailAddressOID asn1.ObjectIdentifier = []int{1, 2, 840, 113549, 1, 9, 1} + if len(chain[0].EmailAddresses) == 0 { + for _, name := range chain[0].Subject.Names { + if name.Type.Equal(emailAddressOID) { + return &user.DefaultInfo{Name: name.Value.(string)}, true, nil + } + } + return nil, false, nil + } + return &user.DefaultInfo{Name: chain[0].EmailAddresses[0]}, true, nil +}) diff --git a/pkg/authentication/request/x509/x509_test.go b/pkg/authentication/request/x509/x509_test.go new file mode 100644 index 000000000..5254e7a77 --- /dev/null +++ b/pkg/authentication/request/x509/x509_test.go @@ -0,0 +1,933 @@ +/* +Copyright 2014 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 x509 + +import ( + "crypto/tls" + "crypto/x509" + "encoding/pem" + "errors" + "io/ioutil" + "net/http" + "reflect" + "sort" + "testing" + "time" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/client-go/pkg/util/sets" +) + +const ( + rootCACert = `-----BEGIN CERTIFICATE----- +MIIDOTCCAqKgAwIBAgIJAOoObf5kuGgZMA0GCSqGSIb3DQEBBQUAMGcxCzAJBgNV +BAYTAlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0G +A1UEChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MRAwDgYDVQQDEwdST09UIENB +MB4XDTE0MTIwODIwMjU1N1oXDTI0MTIwNTIwMjU1N1owZzELMAkGA1UEBhMCVVMx +ETAPBgNVBAgTCE15IFN0YXRlMRAwDgYDVQQHEwdNeSBDaXR5MQ8wDQYDVQQKEwZN +eSBPcmcxEDAOBgNVBAsTB015IFVuaXQxEDAOBgNVBAMTB1JPT1QgQ0EwgZ8wDQYJ +KoZIhvcNAQEBBQADgY0AMIGJAoGBAMfcayGpuF4vwrP8SXKDMCTJ9HV1cvb1NYEc +UgKF0RtcWpK+i0jvhcEs0TPDZIwLSwFw6UMEt5xy4LUlv1K/SHGY3Ym3m/TXMnB9 +gkfrbWlY9LBIm4oVXwrPWyNIe74qAh1Oi03J1492uUPdHhcEmf01RIP6IIqIDuDL +xNNggeIrAgMBAAGjgewwgekwHQYDVR0OBBYEFD3w9zA9O+s6VWj69UPJx6zhPxB4 +MIGZBgNVHSMEgZEwgY6AFD3w9zA9O+s6VWj69UPJx6zhPxB4oWukaTBnMQswCQYD +VQQGEwJVUzERMA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENpdHkxDzAN +BgNVBAoTBk15IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMHUk9PVCBD +QYIJAOoObf5kuGgZMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgEGMBEGCWCGSAGG ++EIBAQQEAwIBBjANBgkqhkiG9w0BAQUFAAOBgQBSrJjMevHUgBKkjaSyeKhOqd8V +XlbA//N/mtJTD3eD/HUZBgyMcBH+sk6hnO8N9ICHtndkTrCElME9N3JA+wg2fHLW +Lj09yrFm7u/0Wd+lcnBnczzoMDhlOjyVqsgIMhisFEw1VVaMoHblYnzY0B+oKNnu +H9oc7u5zhTGXeV8WPg== +-----END CERTIFICATE----- +` + + selfSignedCert = `-----BEGIN CERTIFICATE----- +MIIDEzCCAnygAwIBAgIJAMaPaFbGgJN+MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNV +BAYTAlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0G +A1UEChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MQ4wDAYDVQQDEwVzZWxmMTAe +Fw0xNDEyMDgyMDI1NThaFw0yNDEyMDUyMDI1NThaMGUxCzAJBgNVBAYTAlVTMREw +DwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UEChMGTXkg +T3JnMRAwDgYDVQQLEwdNeSBVbml0MQ4wDAYDVQQDEwVzZWxmMTCBnzANBgkqhkiG +9w0BAQEFAAOBjQAwgYkCgYEA2NAe5AE//Uccy/HSqr4TBhzSe4QD5NYOWuTSKVeX +LLJ0IK2SD3PfnFM/Y0wERx6ORZPGxM0ByPO1RgZe14uFSPEdnD2WTx4lcALK9Jci +IrsvGRyMH0ZT6Q+35ScchAOdOJJYcvXEWf/heZauogzNQAGskwZdYxQB4zwC/es/ +EE0CAwEAAaOByjCBxzAdBgNVHQ4EFgQUfKsCqEU/sCgvcZFSonHu2UArQ3EwgZcG +A1UdIwSBjzCBjIAUfKsCqEU/sCgvcZFSonHu2UArQ3GhaaRnMGUxCzAJBgNVBAYT +AlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UE +ChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MQ4wDAYDVQQDEwVzZWxmMYIJAMaP +aFbGgJN+MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADgYEAxpo9Nyp4d3TT +FnEC4erqQGgbc15fOF47J7bgXxsKK8o8oR/CzQ+08KhoDn3WgV39rEfX2jENDdWp +ze3kOoP+iWSmTySHMSKVMppp0Xnls6t38mrsXtPuY8fGD2GS6VllaizMqc3wShNK +4HADGF3q5z8hZYSV9ICQYHu5T9meF8M= +-----END CERTIFICATE----- +` + + clientCNCert = `Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=ROOT CA + Validity + Not Before: Dec 8 20:25:58 2014 GMT + Not After : Dec 5 20:25:58 2024 GMT + Subject: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=client_cn + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (1024 bit) + Modulus: + 00:a5:30:b3:2b:c0:bd:cb:29:cf:e2:d8:fd:68:b0: + 03:c3:a6:3b:1b:ec:36:73:a1:52:5d:27:ee:02:35: + 5c:51:ed:3d:3b:54:d7:11:f5:38:94:ee:fd:cc:0c: + 22:a8:f8:8e:11:2f:7c:43:5a:aa:07:3f:95:4f:50: + 22:7d:aa:e2:5d:2a:90:3d:02:1a:5b:d2:cf:3f:fb: + dc:58:32:c5:ce:2f:81:58:31:20:eb:35:d3:53:d3: + 42:47:c2:13:68:93:62:58:b6:46:60:48:17:df:d2: + 8c:c3:40:47:cf:67:ea:27:0f:09:78:e9:d5:2a:64: + 1e:c4:33:5a:d6:0d:7a:79:93 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + E7:FB:1F:45:F0:71:77:AF:8C:10:4A:0A:42:03:F5:1F:1F:07:CF:DF + X509v3 Authority Key Identifier: + keyid:3D:F0:F7:30:3D:3B:EB:3A:55:68:FA:F5:43:C9:C7:AC:E1:3F:10:78 + DirName:/C=US/ST=My State/L=My City/O=My Org/OU=My Unit/CN=ROOT CA + serial:EA:0E:6D:FE:64:B8:68:19 + + X509v3 Subject Alternative Name: + + + X509v3 Extended Key Usage: + TLS Web Client Authentication + Netscape Cert Type: + SSL Client + Signature Algorithm: sha256WithRSAEncryption + 08:bc:b4:80:a5:3b:be:9a:78:f9:47:3f:c0:2d:75:e3:10:89: + 61:b1:6a:dd:f4:a4:c4:6a:d3:6f:27:30:7f:2d:07:78:d9:12: + 03:bc:a5:44:68:f3:10:bc:aa:32:e3:3f:6a:16:12:25:eb:82: + ac:ae:30:ef:0d:be:87:11:13:e7:2f:78:69:67:36:62:ba:aa: + 51:8a:ee:6e:1e:ca:35:75:95:25:2d:db:e6:cb:71:70:95:25: + 76:99:13:02:57:99:56:25:a3:33:55:a2:6a:30:87:8b:97:e6: + 68:f3:c1:37:3c:c1:14:26:90:a0:dd:d3:02:3a:e9:c2:9e:59: + d2:44 +-----BEGIN CERTIFICATE----- +MIIDczCCAtygAwIBAgIBATANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzER +MA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENpdHkxDzANBgNVBAoTBk15 +IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMHUk9PVCBDQTAeFw0xNDEy +MDgyMDI1NThaFw0yNDEyMDUyMDI1NThaMGkxCzAJBgNVBAYTAlVTMREwDwYDVQQI +EwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UEChMGTXkgT3JnMRAw +DgYDVQQLEwdNeSBVbml0MRIwEAYDVQQDFAljbGllbnRfY24wgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBAKUwsyvAvcspz+LY/WiwA8OmOxvsNnOhUl0n7gI1XFHt +PTtU1xH1OJTu/cwMIqj4jhEvfENaqgc/lU9QIn2q4l0qkD0CGlvSzz/73Fgyxc4v +gVgxIOs101PTQkfCE2iTYli2RmBIF9/SjMNAR89n6icPCXjp1SpkHsQzWtYNenmT +AgMBAAGjggErMIIBJzAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM +IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQU5/sfRfBxd6+MEEoKQgP1 +Hx8Hz98wgZkGA1UdIwSBkTCBjoAUPfD3MD076zpVaPr1Q8nHrOE/EHiha6RpMGcx +CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0 +eTEPMA0GA1UEChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MRAwDgYDVQQDEwdS +T09UIENBggkA6g5t/mS4aBkwCQYDVR0RBAIwADATBgNVHSUEDDAKBggrBgEFBQcD +AjARBglghkgBhvhCAQEEBAMCB4AwDQYJKoZIhvcNAQELBQADgYEACLy0gKU7vpp4 ++Uc/wC114xCJYbFq3fSkxGrTbycwfy0HeNkSA7ylRGjzELyqMuM/ahYSJeuCrK4w +7w2+hxET5y94aWc2YrqqUYrubh7KNXWVJS3b5stxcJUldpkTAleZViWjM1WiajCH +i5fmaPPBNzzBFCaQoN3TAjrpwp5Z0kQ= +-----END CERTIFICATE-----` + + clientDNSCert = `Certificate: + Data: + Version: 3 (0x2) + Serial Number: 4 (0x4) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=ROOT CA + Validity + Not Before: Dec 8 20:25:58 2014 GMT + Not After : Dec 5 20:25:58 2024 GMT + Subject: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=client_dns + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (1024 bit) + Modulus: + 00:b0:6d:16:6a:fc:28:f7:dc:da:2c:a8:e4:0c:27: + 3c:27:ce:ae:d5:72:d9:3c:eb:af:3d:a3:83:98:5b: + 85:d8:68:f4:bd:53:57:d2:ad:e8:71:b1:18:8e:ae: + 37:8e:02:9c:b2:6c:92:09:cc:5e:e6:74:a1:4b:e1: + 50:41:08:9a:5e:d4:20:0b:6f:c7:c0:34:a8:e6:be: + 77:1d:43:1f:2c:df:dc:ca:9d:1a:0a:9f:a3:6e:0a: + 60:f1:6d:d9:7f:f0:f1:ea:66:9d:4c:f3:de:62:af: + b1:92:70:f1:bb:8a:81:f4:9c:3c:b8:c9:e8:04:18: + 70:2f:77:74:48:d9:cd:e5:af + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 6E:A3:F6:01:52:79:4D:46:78:3C:D0:AB:4A:75:96:AC:7D:6C:08:BE + X509v3 Authority Key Identifier: + keyid:3D:F0:F7:30:3D:3B:EB:3A:55:68:FA:F5:43:C9:C7:AC:E1:3F:10:78 + DirName:/C=US/ST=My State/L=My City/O=My Org/OU=My Unit/CN=ROOT CA + serial:EA:0E:6D:FE:64:B8:68:19 + + X509v3 Subject Alternative Name: + DNS:client_dns.example.com + X509v3 Extended Key Usage: + TLS Web Client Authentication + Netscape Cert Type: + SSL Client + Signature Algorithm: sha256WithRSAEncryption + 69:20:83:0f:16:f8:b6:f5:04:98:56:a4:b2:67:32:e0:82:80: + da:8e:54:06:94:96:cd:56:eb:90:4c:f4:3c:50:80:6a:25:ac: + 3d:e2:81:05:e4:89:2b:55:63:9a:2d:4a:da:3b:c4:97:5e:1a: + e9:6f:83:b8:05:4a:dc:bd:ab:b0:a0:75:d0:1e:b5:c5:8d:f3: + f6:92:f1:52:d2:81:67:fc:6f:74:ee:49:37:73:08:bc:f5:26: + 86:67:f5:82:04:ff:db:5a:9f:f9:6b:df:2f:f5:75:61:f2:a5: + 91:0b:05:56:5b:e8:d1:36:d7:56:7a:ed:7d:e5:5f:2a:08:87: + c2:48 +-----BEGIN CERTIFICATE----- +MIIDjDCCAvWgAwIBAgIBBDANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzER +MA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENpdHkxDzANBgNVBAoTBk15 +IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMHUk9PVCBDQTAeFw0xNDEy +MDgyMDI1NThaFw0yNDEyMDUyMDI1NThaMGoxCzAJBgNVBAYTAlVTMREwDwYDVQQI +EwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UEChMGTXkgT3JnMRAw +DgYDVQQLEwdNeSBVbml0MRMwEQYDVQQDFApjbGllbnRfZG5zMIGfMA0GCSqGSIb3 +DQEBAQUAA4GNADCBiQKBgQCwbRZq/Cj33NosqOQMJzwnzq7Vctk86689o4OYW4XY +aPS9U1fSrehxsRiOrjeOApyybJIJzF7mdKFL4VBBCJpe1CALb8fANKjmvncdQx8s +39zKnRoKn6NuCmDxbdl/8PHqZp1M895ir7GScPG7ioH0nDy4yegEGHAvd3RI2c3l +rwIDAQABo4IBQzCCAT8wCQYDVR0TBAIwADAsBglghkgBhvhCAQ0EHxYdT3BlblNT +TCBHZW5lcmF0ZWQgQ2VydGlmaWNhdGUwHQYDVR0OBBYEFG6j9gFSeU1GeDzQq0p1 +lqx9bAi+MIGZBgNVHSMEgZEwgY6AFD3w9zA9O+s6VWj69UPJx6zhPxB4oWukaTBn +MQswCQYDVQQGEwJVUzERMA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENp +dHkxDzANBgNVBAoTBk15IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMH +Uk9PVCBDQYIJAOoObf5kuGgZMCEGA1UdEQQaMBiCFmNsaWVudF9kbnMuZXhhbXBs +ZS5jb20wEwYDVR0lBAwwCgYIKwYBBQUHAwIwEQYJYIZIAYb4QgEBBAQDAgeAMA0G +CSqGSIb3DQEBCwUAA4GBAGkggw8W+Lb1BJhWpLJnMuCCgNqOVAaUls1W65BM9DxQ +gGolrD3igQXkiStVY5otSto7xJdeGulvg7gFSty9q7CgddAetcWN8/aS8VLSgWf8 +b3TuSTdzCLz1JoZn9YIE/9tan/lr3y/1dWHypZELBVZb6NE211Z67X3lXyoIh8JI +-----END CERTIFICATE-----` + + clientEmailCert = `Certificate: + Data: + Version: 3 (0x2) + Serial Number: 2 (0x2) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=ROOT CA + Validity + Not Before: Dec 8 20:25:58 2014 GMT + Not After : Dec 5 20:25:58 2024 GMT + Subject: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=client_email + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (1024 bit) + Modulus: + 00:bf:f3:c3:d7:50:d5:64:d6:d2:e3:6c:bb:7e:5d: + 4b:41:63:76:9c:c4:c8:33:9a:37:ee:68:24:1e:26: + cf:de:57:79:d6:dc:53:b6:da:12:c6:c0:95:7d:69: + b8:af:1d:4e:8f:a5:83:8b:22:78:e3:94:cc:6e:fe: + 24:e2:05:91:ed:1c:01:b7:e1:53:91:aa:51:53:7a: + 55:6e:fe:0c:ef:c1:66:70:12:0c:85:94:95:c6:3e: + f5:35:58:4d:3f:11:b1:5a:d6:ec:a1:f5:21:c1:e6: + 1f:c1:91:5b:67:89:25:2a:e3:86:27:6b:d8:31:7b: + f1:0d:83:c7:f2:68:70:f0:23 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 76:22:99:CD:3D:BA:90:62:0F:BE:E7:5B:57:8D:31:1D:25:27:C6:6A + X509v3 Authority Key Identifier: + keyid:3D:F0:F7:30:3D:3B:EB:3A:55:68:FA:F5:43:C9:C7:AC:E1:3F:10:78 + DirName:/C=US/ST=My State/L=My City/O=My Org/OU=My Unit/CN=ROOT CA + serial:EA:0E:6D:FE:64:B8:68:19 + + X509v3 Subject Alternative Name: + email:client_email@example.com + X509v3 Extended Key Usage: + TLS Web Client Authentication + Netscape Cert Type: + SSL Client + Signature Algorithm: sha256WithRSAEncryption + 80:70:19:d2:5c:c1:cf:d2:b6:e5:0e:76:cd:8f:c2:8d:a8:19: + 07:86:22:3f:a4:b1:98:c6:98:c1:dc:f8:99:5b:20:5c:6d:17: + 6b:fa:8b:4c:1b:86:14:b4:71:f7:41:22:03:ca:ec:2c:cd:ae: + 77:93:bd:08:06:8c:3c:06:ce:04:2c:b1:ce:79:20:0d:d5:01: + 1c:bd:66:60:38:db:4f:ad:dc:a6:33:8f:07:af:e6:bd:1c:27: + 4b:93:6a:4f:59:e3:cf:df:ff:87:f1:af:02:ad:50:06:f9:50: + c7:59:87:bc:0c:e6:66:cd:d1:c8:df:e6:15:b2:21:b3:04:86: + 8c:89 +-----BEGIN CERTIFICATE----- +MIIDkDCCAvmgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzER +MA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENpdHkxDzANBgNVBAoTBk15 +IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMHUk9PVCBDQTAeFw0xNDEy +MDgyMDI1NThaFw0yNDEyMDUyMDI1NThaMGwxCzAJBgNVBAYTAlVTMREwDwYDVQQI +EwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UEChMGTXkgT3JnMRAw +DgYDVQQLEwdNeSBVbml0MRUwEwYDVQQDFAxjbGllbnRfZW1haWwwgZ8wDQYJKoZI +hvcNAQEBBQADgY0AMIGJAoGBAL/zw9dQ1WTW0uNsu35dS0FjdpzEyDOaN+5oJB4m +z95XedbcU7baEsbAlX1puK8dTo+lg4sieOOUzG7+JOIFke0cAbfhU5GqUVN6VW7+ +DO/BZnASDIWUlcY+9TVYTT8RsVrW7KH1IcHmH8GRW2eJJSrjhidr2DF78Q2Dx/Jo +cPAjAgMBAAGjggFFMIIBQTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVu +U1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUdiKZzT26kGIPvudb +V40xHSUnxmowgZkGA1UdIwSBkTCBjoAUPfD3MD076zpVaPr1Q8nHrOE/EHiha6Rp +MGcxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkg +Q2l0eTEPMA0GA1UEChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MRAwDgYDVQQD +EwdST09UIENBggkA6g5t/mS4aBkwIwYDVR0RBBwwGoEYY2xpZW50X2VtYWlsQGV4 +YW1wbGUuY29tMBMGA1UdJQQMMAoGCCsGAQUFBwMCMBEGCWCGSAGG+EIBAQQEAwIH +gDANBgkqhkiG9w0BAQsFAAOBgQCAcBnSXMHP0rblDnbNj8KNqBkHhiI/pLGYxpjB +3PiZWyBcbRdr+otMG4YUtHH3QSIDyuwsza53k70IBow8Bs4ELLHOeSAN1QEcvWZg +ONtPrdymM48Hr+a9HCdLk2pPWePP3/+H8a8CrVAG+VDHWYe8DOZmzdHI3+YVsiGz +BIaMiQ== +-----END CERTIFICATE----- +` + + serverCert = `Certificate: + Data: + Version: 3 (0x2) + Serial Number: 7 (0x7) + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=ROOT CA + Validity + Not Before: Dec 8 20:25:58 2014 GMT + Not After : Dec 5 20:25:58 2024 GMT + Subject: C=US, ST=My State, L=My City, O=My Org, OU=My Unit, CN=127.0.0.1 + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (1024 bit) + Modulus: + 00:e2:50:d9:1c:ff:03:34:0d:f8:b4:0c:08:70:fc: + 2a:27:2f:42:c9:4b:90:f2:a7:f2:7c:8c:ec:58:a5: + 0f:49:29:0c:77:b5:aa:0a:aa:b7:71:e7:2d:0e:fb: + 73:2c:88:de:70:69:df:d1:b0:7f:3b:2d:28:99:2d: + f1:43:93:13:aa:c9:98:16:05:05:fb:80:64:7b:11: + 19:44:b7:5a:8c:83:20:6f:68:73:4f:ec:78:c2:73: + de:96:68:30:ce:2a:04:03:22:80:21:26:cc:7e:d6: + ec:b5:58:a7:41:bb:ae:fc:2c:29:6a:d1:3a:aa:b9: + 2f:88:f5:62:d8:8e:69:f4:19 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 36:A1:0C:B2:28:0C:77:6C:7F:96:90:11:CA:19:AF:67:1E:92:17:08 + X509v3 Authority Key Identifier: + keyid:3D:F0:F7:30:3D:3B:EB:3A:55:68:FA:F5:43:C9:C7:AC:E1:3F:10:78 + DirName:/C=US/ST=My State/L=My City/O=My Org/OU=My Unit/CN=ROOT CA + serial:EA:0E:6D:FE:64:B8:68:19 + + X509v3 Subject Alternative Name: + + + X509v3 Extended Key Usage: + TLS Web Server Authentication + Netscape Cert Type: + SSL Server + Signature Algorithm: sha256WithRSAEncryption + a9:dd:3d:64:e5:e2:fb:7e:2e:ce:52:7a:85:1d:62:0b:ec:ca: + 1d:78:51:d1:f7:13:36:1c:27:3f:69:59:27:5f:89:ac:41:5e: + 65:c6:ae:dc:18:60:18:85:5b:bb:9a:76:93:df:60:47:96:97: + 58:61:34:98:59:46:ea:d4:ad:01:6c:f7:4e:6c:9d:72:26:4d: + 76:21:1b:7a:a1:f0:e6:e6:88:61:68:f5:cc:2e:40:76:f1:57: + 04:5b:9e:d2:88:c8:ac:9e:49:b5:b4:d6:71:c1:fd:d8:b8:0f: + c7:1a:9c:f3:3f:cc:11:60:ef:54:3a:3d:b8:8d:09:80:fe:be: + f9:ef +-----BEGIN CERTIFICATE----- +MIIDczCCAtygAwIBAgIBBzANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJVUzER +MA8GA1UECBMITXkgU3RhdGUxEDAOBgNVBAcTB015IENpdHkxDzANBgNVBAoTBk15 +IE9yZzEQMA4GA1UECxMHTXkgVW5pdDEQMA4GA1UEAxMHUk9PVCBDQTAeFw0xNDEy +MDgyMDI1NThaFw0yNDEyMDUyMDI1NThaMGkxCzAJBgNVBAYTAlVTMREwDwYDVQQI +EwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0eTEPMA0GA1UEChMGTXkgT3JnMRAw +DgYDVQQLEwdNeSBVbml0MRIwEAYDVQQDEwkxMjcuMC4wLjEwgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBAOJQ2Rz/AzQN+LQMCHD8KicvQslLkPKn8nyM7FilD0kp +DHe1qgqqt3HnLQ77cyyI3nBp39GwfzstKJkt8UOTE6rJmBYFBfuAZHsRGUS3WoyD +IG9oc0/seMJz3pZoMM4qBAMigCEmzH7W7LVYp0G7rvwsKWrROqq5L4j1YtiOafQZ +AgMBAAGjggErMIIBJzAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NM +IEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUNqEMsigMd2x/lpARyhmv +Zx6SFwgwgZkGA1UdIwSBkTCBjoAUPfD3MD076zpVaPr1Q8nHrOE/EHiha6RpMGcx +CzAJBgNVBAYTAlVTMREwDwYDVQQIEwhNeSBTdGF0ZTEQMA4GA1UEBxMHTXkgQ2l0 +eTEPMA0GA1UEChMGTXkgT3JnMRAwDgYDVQQLEwdNeSBVbml0MRAwDgYDVQQDEwdS +T09UIENBggkA6g5t/mS4aBkwCQYDVR0RBAIwADATBgNVHSUEDDAKBggrBgEFBQcD +ATARBglghkgBhvhCAQEEBAMCBkAwDQYJKoZIhvcNAQELBQADgYEAqd09ZOXi+34u +zlJ6hR1iC+zKHXhR0fcTNhwnP2lZJ1+JrEFeZcau3BhgGIVbu5p2k99gR5aXWGE0 +mFlG6tStAWz3TmydciZNdiEbeqHw5uaIYWj1zC5AdvFXBFue0ojIrJ5JtbTWccH9 +2LgPxxqc8z/MEWDvVDo9uI0JgP6++e8= +-----END CERTIFICATE----- +` + + /* + openssl genrsa -out ca.key 4096 + openssl req -new -x509 -days 36500 \ + -sha256 -key ca.key -extensions v3_ca \ + -out ca.crt \ + -subj "/C=US/ST=My State/L=My City/O=My Org/O=My Org 1/O=My Org 2/CN=ROOT CA WITH GROUPS" + openssl x509 -in ca.crt -text + */ + + // A certificate with multiple organizations. + caWithGroups = `Certificate: + Data: + Version: 3 (0x2) + Serial Number: + bc:57:6d:0b:7c:ff:cb:52 + Signature Algorithm: sha256WithRSAEncryption + Issuer: C=US, ST=My State, L=My City, O=My Org, O=My Org 1, O=My Org 2, CN=ROOT CA WITH GROUPS + Validity + Not Before: Aug 10 19:22:03 2016 GMT + Not After : Jul 17 19:22:03 2116 GMT + Subject: C=US, ST=My State, L=My City, O=My Org, O=My Org 1, O=My Org 2, CN=ROOT CA WITH GROUPS + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (4096 bit) + Modulus: + 00:ba:3a:40:34:1a:ba:13:87:0d:c9:c7:bf:e5:8e: + 6a:c7:d5:0f:8f:e3:e1:ac:9e:a5:fd:35:e1:39:52: + 1d:22:77:c1:d2:3f:74:02:2e:23:c6:c1:fc:cd:30: + b4:33:e7:12:04:6f:90:27:e1:be:8e:ec:c8:dc:87: + 91:da:7d:5b:8a:1f:41:fb:62:24:d0:26:98:c6:f7: + f8:ca:8a:56:15:c4:b3:5f:43:86:28:f6:4d:fc:e4: + 03:52:1d:2b:25:f7:19:5c:13:c3:0e:04:91:06:f3: + 29:b6:3f:8b:86:6d:b5:8e:43:2d:69:4e:60:53:5b: + 75:8f:e7:d2:57:8c:db:bb:a1:0b:d7:c7:62:41:bc: + f2:87:be:66:bb:b9:bf:8b:85:97:19:98:18:50:7b: + ee:31:88:47:99:c1:04:e4:12:d2:a6:e2:bf:61:33: + 82:11:79:c3:d5:39:7c:1c:15:9e:d2:61:f7:16:9f: + 97:f1:39:05:8f:b9:f8:e0:5b:16:ca:da:bf:10:45: + 10:0f:14:f9:67:10:66:77:05:f3:fe:21:d6:69:fb: + 1e:dc:fd:f7:97:40:db:0d:59:99:8a:9d:e4:31:a3: + b9:c2:4d:ff:85:ae:ea:da:18:d8:c7:a5:b7:ea:f3: + a8:38:a5:44:1f:3b:23:71:fc:4c:5b:bd:36:6f:e0: + 28:6d:f3:be:e8:c9:74:64:af:89:54:b3:12:c8:2d: + 27:2d:1c:22:23:81:bd:69:b7:8b:76:63:e1:bf:80: + a1:ba:d6:c6:fc:aa:37:2e:44:94:4b:4c:3f:c4:f2: + c3:f8:25:54:ab:1f:0f:4c:19:2f:9c:b6:46:09:db: + 26:52:b4:03:0a:35:75:53:94:33:5d:22:29:48:4a: + 61:9c:d0:5a:6d:91:f5:18:bb:93:99:30:02:5c:6d: + 7c:3f:4d:5a:ea:6f:ee:f7:7a:f9:07:9d:fe:e0:6f: + 75:02:4a:ef:1e:25:c2:d5:8d:2c:57:a2:95:a7:df: + 37:4f:32:60:94:09:85:4d:a7:67:05:e9:29:db:45: + a8:89:ec:1e:e9:3a:49:92:23:17:5b:4a:9c:b8:0d: + 6f:2a:54:ba:47:45:f8:d3:34:30:e8:db:48:6d:c7: + 82:08:01:d5:93:6a:08:7c:4b:43:78:04:df:57:b7: + fe:e3:d7:4c:ec:9c:dc:2d:0b:8c:e4:6f:aa:e2:30: + 66:74:16:10:b9:44:c9:1e:73:53:86:25:25:cc:60: + 3a:94:79:18:f1:c9:31:b0:e1:ca:b9:21:44:75:0a: + 6c:e4:58:c1:37:ee:69:28:d1:d4:b8:78:21:64:ea: + 27:d3:67:25:cf:3a:82:8d:de:27:51:b4:33:a2:85: + db:07:89 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Subject Key Identifier: + AB:3A:46:07:46:0C:68:F0:64:C7:73:A8:7C:8A:20:66:A8:DA:1C:E4 + X509v3 Authority Key Identifier: + keyid:AB:3A:46:07:46:0C:68:F0:64:C7:73:A8:7C:8A:20:66:A8:DA:1C:E4 + + X509v3 Basic Constraints: + CA:TRUE + Signature Algorithm: sha256WithRSAEncryption + 1c:af:04:c9:10:f2:43:03:b6:24:2e:20:2e:47:46:4d:7f:b9: + fa:1c:ea:8d:f0:30:a5:42:93:fe:e0:55:4a:b5:8b:4d:30:f4: + e1:04:1f:20:ec:a1:27:ab:1f:b2:9d:da:58:2e:04:5c:b6:7c: + 69:8c:00:59:42:4f:cc:c7:3c:d4:f7:30:84:2a:14:8e:5d:3a: + 20:91:63:5c:ac:5c:e7:0c:78:fc:28:f3:f9:24:de:3d:30:e3: + 64:ca:5d:a6:86:30:76:5e:53:a4:99:77:a4:7a:c5:52:62:cd: + f9:79:42:69:57:1b:79:25:c5:51:45:42:ed:ae:9c:bc:f2:4c: + 4d:9d:3a:17:73:b1:d2:94:ab:61:4a:90:fa:59:f1:96:c7:7c: + 26:5b:0c:75:4b:94:6f:76:ac:6c:70:8f:68:5c:e3:e7:7b:b9: + 38:c2:0f:f2:e3:2d:96:ec:79:fa:bf:df:33:02:f2:67:a1:19: + d1:7d:ed:c4:3b:14:b8:1f:53:c5:6a:52:ad:19:2d:4c:43:19: + c7:d3:14:75:7f:e7:18:40:38:79:b7:2c:ce:91:6f:cd:16:e3: + d9:8f:87:be:bc:c0:c0:53:1a:93:d6:ff:a9:17:c0:d9:6f:6a: + cc:0b:57:37:b8:da:30:98:4a:fc:e5:e9:dc:49:1a:33:35:f0: + e9:9a:a7:a2:fd:6a:13:9e:85:df:66:a8:15:3f:94:30:4b:ca: + 61:72:7e:1a:b1:83:88:65:21:e8:f6:58:4a:22:48:b5:29:3d: + 00:6c:3e:a2:e5:bd:a5:a3:d9:5a:4d:a9:cb:2a:f8:47:ca:72: + ea:9d:e1:87:e1:d1:75:5d:07:36:ba:ab:fd:7f:5f:d3:66:d0: + 41:86:7c:6b:1e:a7:7c:9f:dc:26:7a:37:70:54:1e:7c:b3:66: + 7f:f1:99:93:f4:8a:aa:81:02:e9:bf:5d:a5:90:94:82:6e:2a: + a6:c8:e1:77:df:66:59:d8:6c:b1:55:a0:77:d6:53:6b:78:aa: + 4b:0d:fc:34:06:5c:52:4e:e6:5e:c7:94:13:19:70:e8:2b:00: + 6d:ea:90:b9:f4:6f:74:3f:cc:e7:1d:3e:22:ec:66:cb:84:19: + 7a:40:3c:7e:38:77:b4:4e:da:8c:4b:af:dc:c2:23:28:9d:60: + a5:4f:5a:c8:9e:17:df:b9:9d:92:bc:d3:c0:20:12:ec:22:d4: + e8:d4:97:9f:da:3c:35:a0:e9:a3:8c:d1:42:7c:c1:27:1f:8a: + 9b:5b:03:3d:2b:9b:df:25:b6:a8:a7:5a:48:0f:e8:1f:26:4b: + 0e:3c:a2:50:0a:cd:02:33:4c:e4:7a:c9:2d:b8:b8:bf:80:5a: + 6e:07:49:c4:c3:23:a0:2e +-----BEGIN CERTIFICATE----- +MIIF5TCCA82gAwIBAgIJALxXbQt8/8tSMA0GCSqGSIb3DQEBCwUAMIGHMQswCQYD +VQQGEwJVUzERMA8GA1UECAwITXkgU3RhdGUxEDAOBgNVBAcMB015IENpdHkxDzAN +BgNVBAoMBk15IE9yZzERMA8GA1UECgwITXkgT3JnIDExETAPBgNVBAoMCE15IE9y +ZyAyMRwwGgYDVQQDDBNST09UIENBIFdJVEggR1JPVVBTMCAXDTE2MDgxMDE5MjIw +M1oYDzIxMTYwNzE3MTkyMjAzWjCBhzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE15 +IFN0YXRlMRAwDgYDVQQHDAdNeSBDaXR5MQ8wDQYDVQQKDAZNeSBPcmcxETAPBgNV +BAoMCE15IE9yZyAxMREwDwYDVQQKDAhNeSBPcmcgMjEcMBoGA1UEAwwTUk9PVCBD +QSBXSVRIIEdST1VQUzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALo6 +QDQauhOHDcnHv+WOasfVD4/j4ayepf014TlSHSJ3wdI/dAIuI8bB/M0wtDPnEgRv +kCfhvo7syNyHkdp9W4ofQftiJNAmmMb3+MqKVhXEs19Dhij2TfzkA1IdKyX3GVwT +ww4EkQbzKbY/i4ZttY5DLWlOYFNbdY/n0leM27uhC9fHYkG88oe+Zru5v4uFlxmY +GFB77jGIR5nBBOQS0qbiv2EzghF5w9U5fBwVntJh9xafl/E5BY+5+OBbFsravxBF +EA8U+WcQZncF8/4h1mn7Htz995dA2w1ZmYqd5DGjucJN/4Wu6toY2Melt+rzqDil +RB87I3H8TFu9Nm/gKG3zvujJdGSviVSzEsgtJy0cIiOBvWm3i3Zj4b+AobrWxvyq +Ny5ElEtMP8Tyw/glVKsfD0wZL5y2RgnbJlK0Awo1dVOUM10iKUhKYZzQWm2R9Ri7 +k5kwAlxtfD9NWupv7vd6+Qed/uBvdQJK7x4lwtWNLFeilaffN08yYJQJhU2nZwXp +KdtFqInsHuk6SZIjF1tKnLgNbypUukdF+NM0MOjbSG3HgggB1ZNqCHxLQ3gE31e3 +/uPXTOyc3C0LjORvquIwZnQWELlEyR5zU4YlJcxgOpR5GPHJMbDhyrkhRHUKbORY +wTfuaSjR1Lh4IWTqJ9NnJc86go3eJ1G0M6KF2weJAgMBAAGjUDBOMB0GA1UdDgQW +BBSrOkYHRgxo8GTHc6h8iiBmqNoc5DAfBgNVHSMEGDAWgBSrOkYHRgxo8GTHc6h8 +iiBmqNoc5DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAcrwTJEPJD +A7YkLiAuR0ZNf7n6HOqN8DClQpP+4FVKtYtNMPThBB8g7KEnqx+yndpYLgRctnxp +jABZQk/MxzzU9zCEKhSOXTogkWNcrFznDHj8KPP5JN49MONkyl2mhjB2XlOkmXek +esVSYs35eUJpVxt5JcVRRULtrpy88kxNnToXc7HSlKthSpD6WfGWx3wmWwx1S5Rv +dqxscI9oXOPne7k4wg/y4y2W7Hn6v98zAvJnoRnRfe3EOxS4H1PFalKtGS1MQxnH +0xR1f+cYQDh5tyzOkW/NFuPZj4e+vMDAUxqT1v+pF8DZb2rMC1c3uNowmEr85enc +SRozNfDpmqei/WoTnoXfZqgVP5QwS8phcn4asYOIZSHo9lhKIki1KT0AbD6i5b2l +o9laTanLKvhHynLqneGH4dF1XQc2uqv9f1/TZtBBhnxrHqd8n9wmejdwVB58s2Z/ +8ZmT9IqqgQLpv12lkJSCbiqmyOF332ZZ2GyxVaB31lNreKpLDfw0BlxSTuZex5QT +GXDoKwBt6pC59G90P8znHT4i7GbLhBl6QDx+OHe0TtqMS6/cwiMonWClT1rInhff +uZ2SvNPAIBLsItTo1Jef2jw1oOmjjNFCfMEnH4qbWwM9K5vfJbaop1pID+gfJksO +PKJQCs0CM0zkesktuLi/gFpuB0nEwyOgLg== +-----END CERTIFICATE-----` +) + +func TestX509(t *testing.T) { + multilevelOpts := DefaultVerifyOptions() + multilevelOpts.Roots = x509.NewCertPool() + multilevelOpts.Roots.AddCert(getCertsFromFile(t, "root")[0]) + + testCases := map[string]struct { + Insecure bool + Certs []*x509.Certificate + + Opts x509.VerifyOptions + User UserConversion + + ExpectUserName string + ExpectGroups []string + ExpectOK bool + ExpectErr bool + }{ + "non-tls": { + Insecure: true, + + ExpectOK: false, + ExpectErr: false, + }, + + "tls, no certs": { + ExpectOK: false, + ExpectErr: false, + }, + + "self signed": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, selfSignedCert), + User: CommonNameUserConversion, + + ExpectErr: true, + }, + + "server cert": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, serverCert), + User: CommonNameUserConversion, + + ExpectErr: true, + }, + "server cert allowing non-client cert usages": { + Opts: x509.VerifyOptions{Roots: getRootCertPool(t)}, + Certs: getCerts(t, serverCert), + User: CommonNameUserConversion, + + ExpectUserName: "127.0.0.1", + ExpectGroups: []string{"My Org"}, + ExpectOK: true, + ExpectErr: false, + }, + + "common name": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, clientCNCert), + User: CommonNameUserConversion, + + ExpectUserName: "client_cn", + ExpectGroups: []string{"My Org"}, + ExpectOK: true, + ExpectErr: false, + }, + "ca with multiple organizations": { + Opts: x509.VerifyOptions{ + Roots: getRootCertPoolFor(t, caWithGroups), + }, + Certs: getCerts(t, caWithGroups), + User: CommonNameUserConversion, + + ExpectUserName: "ROOT CA WITH GROUPS", + ExpectGroups: []string{"My Org", "My Org 1", "My Org 2"}, + ExpectOK: true, + ExpectErr: false, + }, + "empty dns": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, clientCNCert), + User: DNSNameUserConversion, + + ExpectOK: false, + ExpectErr: false, + }, + "dns": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, clientDNSCert), + User: DNSNameUserConversion, + + ExpectUserName: "client_dns.example.com", + ExpectOK: true, + ExpectErr: false, + }, + + "empty email": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, clientCNCert), + User: EmailAddressUserConversion, + + ExpectOK: false, + ExpectErr: false, + }, + "email": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, clientEmailCert), + User: EmailAddressUserConversion, + + ExpectUserName: "client_email@example.com", + ExpectOK: true, + ExpectErr: false, + }, + + "custom conversion error": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, clientCNCert), + User: UserConversionFunc(func(chain []*x509.Certificate) (user.Info, bool, error) { + return nil, false, errors.New("custom error") + }), + + ExpectOK: false, + ExpectErr: true, + }, + "custom conversion success": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, clientCNCert), + User: UserConversionFunc(func(chain []*x509.Certificate) (user.Info, bool, error) { + return &user.DefaultInfo{Name: "custom"}, true, nil + }), + + ExpectUserName: "custom", + ExpectOK: true, + ExpectErr: false, + }, + + "future cert": { + Opts: x509.VerifyOptions{ + CurrentTime: time.Now().Add(time.Duration(-100 * time.Hour * 24 * 365)), + Roots: getRootCertPool(t), + }, + Certs: getCerts(t, clientCNCert), + User: CommonNameUserConversion, + + ExpectOK: false, + ExpectErr: true, + }, + "expired cert": { + Opts: x509.VerifyOptions{ + CurrentTime: time.Now().Add(time.Duration(100 * time.Hour * 24 * 365)), + Roots: getRootCertPool(t), + }, + Certs: getCerts(t, clientCNCert), + User: CommonNameUserConversion, + + ExpectOK: false, + ExpectErr: true, + }, + + "multi-level, valid": { + Opts: multilevelOpts, + Certs: getCertsFromFile(t, "client-valid", "intermediate"), + User: CommonNameUserConversion, + + ExpectUserName: "My Client", + ExpectOK: true, + ExpectErr: false, + }, + "multi-level, expired": { + Opts: multilevelOpts, + Certs: getCertsFromFile(t, "client-expired", "intermediate"), + User: CommonNameUserConversion, + + ExpectOK: false, + ExpectErr: true, + }, + } + + for k, testCase := range testCases { + req, _ := http.NewRequest("GET", "/", nil) + if !testCase.Insecure { + req.TLS = &tls.ConnectionState{PeerCertificates: testCase.Certs} + } + + a := New(testCase.Opts, testCase.User) + + user, ok, err := a.AuthenticateRequest(req) + + if testCase.ExpectErr && err == nil { + t.Errorf("%s: Expected error, got none", k) + continue + } + if !testCase.ExpectErr && err != nil { + t.Errorf("%s: Got unexpected error: %v", k, err) + continue + } + + if testCase.ExpectOK != ok { + t.Errorf("%s: Expected ok=%v, got %v", k, testCase.ExpectOK, ok) + continue + } + + if testCase.ExpectOK { + if testCase.ExpectUserName != user.GetName() { + t.Errorf("%s: Expected user.name=%v, got %v", k, testCase.ExpectUserName, user.GetName()) + } + + groups := user.GetGroups() + sort.Strings(testCase.ExpectGroups) + sort.Strings(groups) + if !reflect.DeepEqual(testCase.ExpectGroups, groups) { + t.Errorf("%s: Expected user.groups=%v, got %v", k, testCase.ExpectGroups, groups) + } + } + } +} + +func TestX509Verifier(t *testing.T) { + multilevelOpts := DefaultVerifyOptions() + multilevelOpts.Roots = x509.NewCertPool() + multilevelOpts.Roots.AddCert(getCertsFromFile(t, "root")[0]) + + testCases := map[string]struct { + Insecure bool + Certs []*x509.Certificate + + Opts x509.VerifyOptions + + AllowedCNs sets.String + + ExpectOK bool + ExpectErr bool + }{ + "non-tls": { + Insecure: true, + + ExpectOK: false, + ExpectErr: false, + }, + + "tls, no certs": { + ExpectOK: false, + ExpectErr: false, + }, + + "self signed": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, selfSignedCert), + + ExpectErr: true, + }, + + "server cert disallowed": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, serverCert), + + ExpectErr: true, + }, + "server cert allowing non-client cert usages": { + Opts: x509.VerifyOptions{Roots: getRootCertPool(t)}, + Certs: getCerts(t, serverCert), + + ExpectOK: true, + ExpectErr: false, + }, + + "valid client cert": { + Opts: getDefaultVerifyOptions(t), + Certs: getCerts(t, clientCNCert), + + ExpectOK: true, + ExpectErr: false, + }, + "valid client cert with wrong CN": { + Opts: getDefaultVerifyOptions(t), + AllowedCNs: sets.NewString("foo", "bar"), + Certs: getCerts(t, clientCNCert), + + ExpectOK: false, + ExpectErr: true, + }, + "valid client cert with right CN": { + Opts: getDefaultVerifyOptions(t), + AllowedCNs: sets.NewString("client_cn"), + Certs: getCerts(t, clientCNCert), + + ExpectOK: true, + ExpectErr: false, + }, + + "future cert": { + Opts: x509.VerifyOptions{ + CurrentTime: time.Now().Add(-100 * time.Hour * 24 * 365), + Roots: getRootCertPool(t), + }, + Certs: getCerts(t, clientCNCert), + + ExpectOK: false, + ExpectErr: true, + }, + "expired cert": { + Opts: x509.VerifyOptions{ + CurrentTime: time.Now().Add(100 * time.Hour * 24 * 365), + Roots: getRootCertPool(t), + }, + Certs: getCerts(t, clientCNCert), + + ExpectOK: false, + ExpectErr: true, + }, + + "multi-level, valid": { + Opts: multilevelOpts, + Certs: getCertsFromFile(t, "client-valid", "intermediate"), + + ExpectOK: true, + ExpectErr: false, + }, + "multi-level, expired": { + Opts: multilevelOpts, + Certs: getCertsFromFile(t, "client-expired", "intermediate"), + + ExpectOK: false, + ExpectErr: true, + }, + } + + for k, testCase := range testCases { + req, _ := http.NewRequest("GET", "/", nil) + if !testCase.Insecure { + req.TLS = &tls.ConnectionState{PeerCertificates: testCase.Certs} + } + + authCall := false + auth := authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) { + authCall = true + return &user.DefaultInfo{Name: "innerauth"}, true, nil + }) + + a := NewVerifier(testCase.Opts, auth, testCase.AllowedCNs) + + user, ok, err := a.AuthenticateRequest(req) + + if testCase.ExpectErr && err == nil { + t.Errorf("%s: Expected error, got none", k) + continue + } + if !testCase.ExpectErr && err != nil { + t.Errorf("%s: Got unexpected error: %v", k, err) + continue + } + + if testCase.ExpectOK != ok { + t.Errorf("%s: Expected ok=%v, got %v", k, testCase.ExpectOK, ok) + continue + } + + if testCase.ExpectOK { + if !authCall { + t.Errorf("%s: Expected inner auth called, wasn't", k) + continue + } + if "innerauth" != user.GetName() { + t.Errorf("%s: Expected user.name=%v, got %v", k, "innerauth", user.GetName()) + continue + } + } else { + if authCall { + t.Errorf("%s: Expected inner auth not to be called, was", k) + continue + } + } + } +} + +func getDefaultVerifyOptions(t *testing.T) x509.VerifyOptions { + options := DefaultVerifyOptions() + options.Roots = getRootCertPool(t) + return options +} + +func getRootCertPool(t *testing.T) *x509.CertPool { + return getRootCertPoolFor(t, rootCACert) +} + +func getRootCertPoolFor(t *testing.T, certs ...string) *x509.CertPool { + pool := x509.NewCertPool() + for _, cert := range certs { + pool.AddCert(getCert(t, cert)) + } + return pool +} + +func getCertsFromFile(t *testing.T, names ...string) []*x509.Certificate { + certs := []*x509.Certificate{} + for _, name := range names { + filename := "testdata/" + name + ".pem" + data, err := ioutil.ReadFile(filename) + if err != nil { + t.Fatalf("error reading %s: %v", filename, err) + } + certs = append(certs, getCert(t, string(data))) + } + return certs +} + +func getCert(t *testing.T, pemData string) *x509.Certificate { + pemBlock, _ := pem.Decode([]byte(pemData)) + cert, err := x509.ParseCertificate(pemBlock.Bytes) + if err != nil { + t.Fatalf("Error parsing cert: %v", err) + return nil + } + return cert +} + +func getCerts(t *testing.T, pemData ...string) []*x509.Certificate { + certs := []*x509.Certificate{} + for _, pemData := range pemData { + certs = append(certs, getCert(t, pemData)) + } + return certs +} diff --git a/pkg/authentication/token/tokenfile/tokenfile.go b/pkg/authentication/token/tokenfile/tokenfile.go new file mode 100644 index 000000000..43ff764de --- /dev/null +++ b/pkg/authentication/token/tokenfile/tokenfile.go @@ -0,0 +1,91 @@ +/* +Copyright 2014 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 tokenfile + +import ( + "encoding/csv" + "fmt" + "io" + "os" + "strings" + + "github.com/golang/glog" + "k8s.io/apiserver/pkg/authentication/user" +) + +type TokenAuthenticator struct { + tokens map[string]*user.DefaultInfo +} + +// New returns a TokenAuthenticator for a single token +func New(tokens map[string]*user.DefaultInfo) *TokenAuthenticator { + return &TokenAuthenticator{ + tokens: tokens, + } +} + +// NewCSV returns a TokenAuthenticator, populated from a CSV file. +// The CSV file must contain records in the format "token,username,useruid" +func NewCSV(path string) (*TokenAuthenticator, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + recordNum := 0 + tokens := make(map[string]*user.DefaultInfo) + reader := csv.NewReader(file) + reader.FieldsPerRecord = -1 + for { + record, err := reader.Read() + if err == io.EOF { + break + } + if err != nil { + return nil, err + } + if len(record) < 3 { + return nil, fmt.Errorf("token file '%s' must have at least 3 columns (token, user name, user uid), found %d", path, len(record)) + } + obj := &user.DefaultInfo{ + Name: record[1], + UID: record[2], + } + recordNum++ + if _, exist := tokens[record[0]]; exist { + glog.Warningf("duplicate token has been found in token file '%s', record number '%d'", path, recordNum) + } + tokens[record[0]] = obj + + if len(record) >= 4 { + obj.Groups = strings.Split(record[3], ",") + } + } + + return &TokenAuthenticator{ + tokens: tokens, + }, nil +} + +func (a *TokenAuthenticator) AuthenticateToken(value string) (user.Info, bool, error) { + user, ok := a.tokens[value] + if !ok { + return nil, false, nil + } + return user, true, nil +} diff --git a/pkg/authentication/token/tokenfile/tokenfile_test.go b/pkg/authentication/token/tokenfile/tokenfile_test.go new file mode 100644 index 000000000..6fd13ba2d --- /dev/null +++ b/pkg/authentication/token/tokenfile/tokenfile_test.go @@ -0,0 +1,141 @@ +/* +Copyright 2014 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 tokenfile + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "k8s.io/apiserver/pkg/authentication/user" +) + +func TestTokenFile(t *testing.T) { + auth, err := newWithContents(t, ` +token1,user1,uid1 +token2,user2,uid2 +token3,user3,uid3,"group1,group2" +token4,user4,uid4,"group2" +token5,user5,uid5,group5 +token6,user6,uid6,group5,otherdata +token7,user7,uid7,"group1,group2",otherdata +`) + if err != nil { + t.Fatalf("unable to read tokenfile: %v", err) + } + + testCases := []struct { + Token string + User *user.DefaultInfo + Ok bool + Err bool + }{ + { + Token: "token1", + User: &user.DefaultInfo{Name: "user1", UID: "uid1"}, + Ok: true, + }, + { + Token: "token2", + User: &user.DefaultInfo{Name: "user2", UID: "uid2"}, + Ok: true, + }, + { + Token: "token3", + User: &user.DefaultInfo{Name: "user3", UID: "uid3", Groups: []string{"group1", "group2"}}, + Ok: true, + }, + { + Token: "token4", + User: &user.DefaultInfo{Name: "user4", UID: "uid4", Groups: []string{"group2"}}, + Ok: true, + }, + { + Token: "token5", + User: &user.DefaultInfo{Name: "user5", UID: "uid5", Groups: []string{"group5"}}, + Ok: true, + }, + { + Token: "token6", + User: &user.DefaultInfo{Name: "user6", UID: "uid6", Groups: []string{"group5"}}, + Ok: true, + }, + { + Token: "token7", + User: &user.DefaultInfo{Name: "user7", UID: "uid7", Groups: []string{"group1", "group2"}}, + Ok: true, + }, + { + Token: "token8", + }, + } + for i, testCase := range testCases { + user, ok, err := auth.AuthenticateToken(testCase.Token) + if testCase.User == nil { + if user != nil { + t.Errorf("%d: unexpected non-nil user %#v", i, user) + } + } else if !reflect.DeepEqual(testCase.User, user) { + t.Errorf("%d: expected user %#v, got %#v", i, testCase.User, user) + } + + if testCase.Ok != ok { + t.Errorf("%d: expected auth %v, got %v", i, testCase.Ok, ok) + } + switch { + case err == nil && testCase.Err: + t.Errorf("%d: unexpected nil error", i) + case err != nil && !testCase.Err: + t.Errorf("%d: unexpected error: %v", i, err) + } + } +} + +func TestBadTokenFile(t *testing.T) { + _, err := newWithContents(t, ` +token1,user1,uid1 +token2,user2,uid2 +token3,user3 +token4 +`) + if err == nil { + t.Fatalf("unexpected non error") + } +} + +func TestInsufficientColumnsTokenFile(t *testing.T) { + _, err := newWithContents(t, "token4\n") + if err == nil { + t.Fatalf("unexpected non error") + } +} + +func newWithContents(t *testing.T, contents string) (auth *TokenAuthenticator, err error) { + f, err := ioutil.TempFile("", "tokenfile_test") + if err != nil { + t.Fatalf("unexpected error creating tokenfile: %v", err) + } + f.Close() + defer os.Remove(f.Name()) + + if err := ioutil.WriteFile(f.Name(), []byte(contents), 0700); err != nil { + t.Fatalf("unexpected error writing tokenfile: %v", err) + } + + return NewCSV(f.Name()) +} diff --git a/pkg/authorization/union/union.go b/pkg/authorization/union/union.go new file mode 100644 index 000000000..3b484d160 --- /dev/null +++ b/pkg/authorization/union/union.go @@ -0,0 +1,57 @@ +/* +Copyright 2014 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 union + +import ( + "strings" + + "k8s.io/apiserver/pkg/authorization/authorizer" + utilerrors "k8s.io/client-go/pkg/util/errors" +) + +// unionAuthzHandler authorizer against a chain of authorizer.Authorizer +type unionAuthzHandler []authorizer.Authorizer + +// New returns an authorizer that authorizes against a chain of authorizer.Authorizer objects +func New(authorizationHandlers ...authorizer.Authorizer) authorizer.Authorizer { + return unionAuthzHandler(authorizationHandlers) +} + +// Authorizes against a chain of authorizer.Authorizer objects and returns nil if successful and returns error if unsuccessful +func (authzHandler unionAuthzHandler) Authorize(a authorizer.Attributes) (bool, string, error) { + var ( + errlist []error + reasonlist []string + ) + + for _, currAuthzHandler := range authzHandler { + authorized, reason, err := currAuthzHandler.Authorize(a) + + if err != nil { + errlist = append(errlist, err) + } + if len(reason) != 0 { + reasonlist = append(reasonlist, reason) + } + if !authorized { + continue + } + return true, reason, nil + } + + return false, strings.Join(reasonlist, "\n"), utilerrors.NewAggregate(errlist) +} diff --git a/pkg/authorization/union/union_test.go b/pkg/authorization/union/union_test.go new file mode 100644 index 000000000..6107ace45 --- /dev/null +++ b/pkg/authorization/union/union_test.go @@ -0,0 +1,83 @@ +/* +Copyright 2014 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 union + +import ( + "fmt" + "testing" + + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +type mockAuthzHandler struct { + isAuthorized bool + err error +} + +func (mock *mockAuthzHandler) Authorize(a authorizer.Attributes) (bool, string, error) { + if mock.err != nil { + return false, "", mock.err + } + if !mock.isAuthorized { + return false, "", nil + } + return true, "", nil +} + +func TestAuthorizationSecondPasses(t *testing.T) { + handler1 := &mockAuthzHandler{isAuthorized: false} + handler2 := &mockAuthzHandler{isAuthorized: true} + authzHandler := New(handler1, handler2) + + authorized, _, _ := authzHandler.Authorize(nil) + if !authorized { + t.Errorf("Unexpected authorization failure") + } +} + +func TestAuthorizationFirstPasses(t *testing.T) { + handler1 := &mockAuthzHandler{isAuthorized: true} + handler2 := &mockAuthzHandler{isAuthorized: false} + authzHandler := New(handler1, handler2) + + authorized, _, _ := authzHandler.Authorize(nil) + if !authorized { + t.Errorf("Unexpected authorization failure") + } +} + +func TestAuthorizationNonePasses(t *testing.T) { + handler1 := &mockAuthzHandler{isAuthorized: false} + handler2 := &mockAuthzHandler{isAuthorized: false} + authzHandler := New(handler1, handler2) + + authorized, _, _ := authzHandler.Authorize(nil) + if authorized { + t.Errorf("Expected failed authorization") + } +} + +func TestAuthorizationError(t *testing.T) { + handler1 := &mockAuthzHandler{err: fmt.Errorf("foo")} + handler2 := &mockAuthzHandler{err: fmt.Errorf("foo")} + authzHandler := New(handler1, handler2) + + _, _, err := authzHandler.Authorize(nil) + if err == nil { + t.Errorf("Expected error: %v", err) + } +} diff --git a/pkg/httplog/doc.go b/pkg/httplog/doc.go new file mode 100644 index 000000000..139dea694 --- /dev/null +++ b/pkg/httplog/doc.go @@ -0,0 +1,19 @@ +/* +Copyright 2014 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 httplog contains a helper object and functions to maintain a log +// along with an http response. +package httplog // import "k8s.io/kubernetes/pkg/httplog" diff --git a/pkg/httplog/log.go b/pkg/httplog/log.go new file mode 100644 index 000000000..4a4894cee --- /dev/null +++ b/pkg/httplog/log.go @@ -0,0 +1,225 @@ +/* +Copyright 2014 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 httplog + +import ( + "bufio" + "fmt" + "net" + "net/http" + "runtime" + "time" + + "github.com/golang/glog" +) + +// Handler wraps all HTTP calls to delegate with nice logging. +// delegate may use LogOf(w).Addf(...) to write additional info to +// the per-request log message. +// +// Intended to wrap calls to your ServeMux. +func Handler(delegate http.Handler, pred StacktracePred) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + defer NewLogged(req, &w).StacktraceWhen(pred).Log() + delegate.ServeHTTP(w, req) + }) +} + +// StacktracePred returns true if a stacktrace should be logged for this status. +type StacktracePred func(httpStatus int) (logStacktrace bool) + +type logger interface { + Addf(format string, data ...interface{}) +} + +// Add a layer on top of ResponseWriter, so we can track latency and error +// message sources. +// +// TODO now that we're using go-restful, we shouldn't need to be wrapping +// the http.ResponseWriter. We can recover panics from go-restful, and +// the logging value is questionable. +type respLogger struct { + hijacked bool + statusRecorded bool + status int + statusStack string + addedInfo string + startTime time.Time + + captureErrorOutput bool + + req *http.Request + w http.ResponseWriter + + logStacktracePred StacktracePred +} + +// Simple logger that logs immediately when Addf is called +type passthroughLogger struct{} + +// Addf logs info immediately. +func (passthroughLogger) Addf(format string, data ...interface{}) { + glog.V(2).Info(fmt.Sprintf(format, data...)) +} + +// DefaultStacktracePred is the default implementation of StacktracePred. +func DefaultStacktracePred(status int) bool { + return (status < http.StatusOK || status >= http.StatusInternalServerError) && status != http.StatusSwitchingProtocols +} + +// NewLogged turns a normal response writer into a logged response writer. +// +// Usage: +// +// defer NewLogged(req, &w).StacktraceWhen(StatusIsNot(200, 202)).Log() +// +// (Only the call to Log() is deferred, so you can set everything up in one line!) +// +// Note that this *changes* your writer, to route response writing actions +// through the logger. +// +// Use LogOf(w).Addf(...) to log something along with the response result. +func NewLogged(req *http.Request, w *http.ResponseWriter) *respLogger { + if _, ok := (*w).(*respLogger); ok { + // Don't double-wrap! + panic("multiple NewLogged calls!") + } + rl := &respLogger{ + startTime: time.Now(), + req: req, + w: *w, + logStacktracePred: DefaultStacktracePred, + } + *w = rl // hijack caller's writer! + return rl +} + +// LogOf returns the logger hiding in w. If there is not an existing logger +// then a passthroughLogger will be created which will log to stdout immediately +// when Addf is called. +func LogOf(req *http.Request, w http.ResponseWriter) logger { + if _, exists := w.(*respLogger); !exists { + pl := &passthroughLogger{} + return pl + } + if rl, ok := w.(*respLogger); ok { + return rl + } + panic("Unable to find or create the logger!") +} + +// Unlogged returns the original ResponseWriter, or w if it is not our inserted logger. +func Unlogged(w http.ResponseWriter) http.ResponseWriter { + if rl, ok := w.(*respLogger); ok { + return rl.w + } + return w +} + +// StacktraceWhen sets the stacktrace logging predicate, which decides when to log a stacktrace. +// There's a default, so you don't need to call this unless you don't like the default. +func (rl *respLogger) StacktraceWhen(pred StacktracePred) *respLogger { + rl.logStacktracePred = pred + return rl +} + +// StatusIsNot returns a StacktracePred which will cause stacktraces to be logged +// for any status *not* in the given list. +func StatusIsNot(statuses ...int) StacktracePred { + return func(status int) bool { + for _, s := range statuses { + if status == s { + return false + } + } + return true + } +} + +// Addf adds additional data to be logged with this request. +func (rl *respLogger) Addf(format string, data ...interface{}) { + rl.addedInfo += "\n" + fmt.Sprintf(format, data...) +} + +// Log is intended to be called once at the end of your request handler, via defer +func (rl *respLogger) Log() { + latency := time.Since(rl.startTime) + if glog.V(2) { + if !rl.hijacked { + glog.InfoDepth(1, fmt.Sprintf("%s %s: (%v) %v%v%v [%s %s]", rl.req.Method, rl.req.RequestURI, latency, rl.status, rl.statusStack, rl.addedInfo, rl.req.Header["User-Agent"], rl.req.RemoteAddr)) + } else { + glog.InfoDepth(1, fmt.Sprintf("%s %s: (%v) hijacked [%s %s]", rl.req.Method, rl.req.RequestURI, latency, rl.req.Header["User-Agent"], rl.req.RemoteAddr)) + } + } +} + +// Header implements http.ResponseWriter. +func (rl *respLogger) Header() http.Header { + return rl.w.Header() +} + +// Write implements http.ResponseWriter. +func (rl *respLogger) Write(b []byte) (int, error) { + if !rl.statusRecorded { + rl.recordStatus(http.StatusOK) // Default if WriteHeader hasn't been called + } + if rl.captureErrorOutput { + rl.Addf("logging error output: %q\n", string(b)) + } + return rl.w.Write(b) +} + +// Flush implements http.Flusher even if the underlying http.Writer doesn't implement it. +// Flush is used for streaming purposes and allows to flush buffered data to the client. +func (rl *respLogger) Flush() { + if flusher, ok := rl.w.(http.Flusher); ok { + flusher.Flush() + } else if glog.V(2) { + glog.InfoDepth(1, fmt.Sprintf("Unable to convert %+v into http.Flusher", rl.w)) + } +} + +// WriteHeader implements http.ResponseWriter. +func (rl *respLogger) WriteHeader(status int) { + rl.recordStatus(status) + rl.w.WriteHeader(status) +} + +// Hijack implements http.Hijacker. +func (rl *respLogger) Hijack() (net.Conn, *bufio.ReadWriter, error) { + rl.hijacked = true + return rl.w.(http.Hijacker).Hijack() +} + +// CloseNotify implements http.CloseNotifier +func (rl *respLogger) CloseNotify() <-chan bool { + return rl.w.(http.CloseNotifier).CloseNotify() +} + +func (rl *respLogger) recordStatus(status int) { + rl.status = status + rl.statusRecorded = true + if rl.logStacktracePred(status) { + // Only log stacks for errors + stack := make([]byte, 50*1024) + stack = stack[:runtime.Stack(stack, false)] + rl.statusStack = "\n" + string(stack) + rl.captureErrorOutput = true + } else { + rl.statusStack = "" + } +} diff --git a/pkg/httplog/log_test.go b/pkg/httplog/log_test.go new file mode 100644 index 000000000..906ec3673 --- /dev/null +++ b/pkg/httplog/log_test.go @@ -0,0 +1,175 @@ +/* +Copyright 2014 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 httplog + +import ( + "bytes" + "net/http" + "net/http/httptest" + "reflect" + "testing" +) + +func TestDefaultStacktracePred(t *testing.T) { + for _, x := range []int{101, 200, 204, 302, 400, 404} { + if DefaultStacktracePred(x) { + t.Fatalf("should not log on %v by default", x) + } + } + + for _, x := range []int{500, 100} { + if !DefaultStacktracePred(x) { + t.Fatalf("should log on %v by default", x) + } + } +} + +func TestHandler(t *testing.T) { + want := &httptest.ResponseRecorder{ + HeaderMap: make(http.Header), + Body: new(bytes.Buffer), + } + want.WriteHeader(http.StatusOK) + mux := http.NewServeMux() + handler := Handler(mux, DefaultStacktracePred) + mux.HandleFunc("/kube", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + req, err := http.NewRequest("GET", "http://example.com/kube", nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + if !reflect.DeepEqual(want, w) { + t.Errorf("Expected %v, got %v", want, w) + } +} + +func TestStatusIsNot(t *testing.T) { + statusTestTable := []struct { + status int + statuses []int + want bool + }{ + {http.StatusOK, []int{}, true}, + {http.StatusOK, []int{http.StatusOK}, false}, + {http.StatusCreated, []int{http.StatusOK, http.StatusAccepted}, true}, + } + for _, tt := range statusTestTable { + sp := StatusIsNot(tt.statuses...) + got := sp(tt.status) + if got != tt.want { + t.Errorf("Expected %v, got %v", tt.want, got) + } + } +} + +func TestNewLogged(t *testing.T) { + req, err := http.NewRequest("GET", "http://example.com", nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + handler := func(w http.ResponseWriter, r *http.Request) { + NewLogged(req, &w) + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected NewLogged to panic") + } + }() + NewLogged(req, &w) + } + w := httptest.NewRecorder() + handler(w, req) +} + +func TestLogOf(t *testing.T) { + logOfTests := []bool{true, false} + for _, makeLogger := range logOfTests { + req, err := http.NewRequest("GET", "http://example.com", nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + handler := func(w http.ResponseWriter, r *http.Request) { + var want string + if makeLogger { + NewLogged(req, &w) + want = "*httplog.respLogger" + } else { + want = "*httplog.passthroughLogger" + } + got := reflect.TypeOf(LogOf(r, w)).String() + if want != got { + t.Errorf("Expected %v, got %v", want, got) + } + } + w := httptest.NewRecorder() + handler(w, req) + } +} + +func TestUnlogged(t *testing.T) { + unloggedTests := []bool{true, false} + for _, makeLogger := range unloggedTests { + req, err := http.NewRequest("GET", "http://example.com", nil) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + handler := func(w http.ResponseWriter, r *http.Request) { + want := w + if makeLogger { + NewLogged(req, &w) + } + got := Unlogged(w) + if want != got { + t.Errorf("Expected %v, got %v", want, got) + } + } + w := httptest.NewRecorder() + handler(w, req) + } +} + +type testResponseWriter struct{} + +func (*testResponseWriter) Header() http.Header { return nil } +func (*testResponseWriter) Write([]byte) (int, error) { return 0, nil } +func (*testResponseWriter) WriteHeader(int) {} + +func TestLoggedStatus(t *testing.T) { + req, err := http.NewRequest("GET", "http://example.com", nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + var tw http.ResponseWriter = new(testResponseWriter) + logger := NewLogged(req, &tw) + logger.Write(nil) + + if logger.status != http.StatusOK { + t.Errorf("expected status after write to be %v, got %v", http.StatusOK, logger.status) + } + + tw = new(testResponseWriter) + logger = NewLogged(req, &tw) + logger.WriteHeader(http.StatusForbidden) + logger.Write(nil) + + if logger.status != http.StatusForbidden { + t.Errorf("expected status after write to remain %v, got %v", http.StatusForbidden, logger.status) + } +} diff --git a/pkg/storage/etcd/metrics/metrics.go b/pkg/storage/etcd/metrics/metrics.go new file mode 100644 index 000000000..119e2e8fc --- /dev/null +++ b/pkg/storage/etcd/metrics/metrics.go @@ -0,0 +1,113 @@ +/* +Copyright 2015 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 metrics + +import ( + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +var ( + cacheHitCounter = prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "etcd_helper_cache_hit_count", + Help: "Counter of etcd helper cache hits.", + }, + ) + cacheMissCounter = prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "etcd_helper_cache_miss_count", + Help: "Counter of etcd helper cache miss.", + }, + ) + cacheEntryCounter = prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "etcd_helper_cache_entry_count", + Help: "Counter of etcd helper cache entries. This can be different from etcd_helper_cache_miss_count " + + "because two concurrent threads can miss the cache and generate the same entry twice.", + }, + ) + cacheGetLatency = prometheus.NewSummary( + prometheus.SummaryOpts{ + Name: "etcd_request_cache_get_latencies_summary", + Help: "Latency in microseconds of getting an object from etcd cache", + }, + ) + cacheAddLatency = prometheus.NewSummary( + prometheus.SummaryOpts{ + Name: "etcd_request_cache_add_latencies_summary", + Help: "Latency in microseconds of adding an object to etcd cache", + }, + ) + etcdRequestLatenciesSummary = prometheus.NewSummaryVec( + prometheus.SummaryOpts{ + Name: "etcd_request_latencies_summary", + Help: "Etcd request latency summary in microseconds for each operation and object type.", + }, + []string{"operation", "type"}, + ) +) + +var registerMetrics sync.Once + +// Register all metrics. +func Register() { + // Register the metrics. + registerMetrics.Do(func() { + prometheus.MustRegister(cacheHitCounter) + prometheus.MustRegister(cacheMissCounter) + prometheus.MustRegister(cacheEntryCounter) + prometheus.MustRegister(cacheAddLatency) + prometheus.MustRegister(cacheGetLatency) + prometheus.MustRegister(etcdRequestLatenciesSummary) + }) +} + +func RecordEtcdRequestLatency(verb, resource string, startTime time.Time) { + etcdRequestLatenciesSummary.WithLabelValues(verb, resource).Observe(float64(time.Since(startTime) / time.Microsecond)) +} + +func ObserveGetCache(startTime time.Time) { + cacheGetLatency.Observe(float64(time.Since(startTime) / time.Microsecond)) +} + +func ObserveAddCache(startTime time.Time) { + cacheAddLatency.Observe(float64(time.Since(startTime) / time.Microsecond)) +} + +func ObserveCacheHit() { + cacheHitCounter.Inc() +} + +func ObserveCacheMiss() { + cacheMissCounter.Inc() +} + +func ObserveNewEntry() { + cacheEntryCounter.Inc() +} + +func Reset() { + cacheHitCounter.Set(0) + cacheMissCounter.Set(0) + cacheEntryCounter.Set(0) + // TODO: Reset cacheAddLatency. + // TODO: Reset cacheGetLatency. + etcdRequestLatenciesSummary.Reset() +} diff --git a/pkg/util/cache/cache.go b/pkg/util/cache/cache.go new file mode 100644 index 000000000..9a09fe54d --- /dev/null +++ b/pkg/util/cache/cache.go @@ -0,0 +1,83 @@ +/* +Copyright 2014 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 cache + +import ( + "sync" +) + +const ( + shardsCount int = 32 +) + +type Cache []*cacheShard + +func NewCache(maxSize int) Cache { + if maxSize < shardsCount { + maxSize = shardsCount + } + cache := make(Cache, shardsCount) + for i := 0; i < shardsCount; i++ { + cache[i] = &cacheShard{ + items: make(map[uint64]interface{}), + maxSize: maxSize / shardsCount, + } + } + return cache +} + +func (c Cache) getShard(index uint64) *cacheShard { + return c[index%uint64(shardsCount)] +} + +// Returns true if object already existed, false otherwise. +func (c *Cache) Add(index uint64, obj interface{}) bool { + return c.getShard(index).add(index, obj) +} + +func (c *Cache) Get(index uint64) (obj interface{}, found bool) { + return c.getShard(index).get(index) +} + +type cacheShard struct { + items map[uint64]interface{} + sync.RWMutex + maxSize int +} + +// Returns true if object already existed, false otherwise. +func (s *cacheShard) add(index uint64, obj interface{}) bool { + s.Lock() + defer s.Unlock() + _, isOverwrite := s.items[index] + if !isOverwrite && len(s.items) >= s.maxSize { + var randomKey uint64 + for randomKey = range s.items { + break + } + delete(s.items, randomKey) + } + s.items[index] = obj + return isOverwrite +} + +func (s *cacheShard) get(index uint64) (obj interface{}, found bool) { + s.RLock() + defer s.RUnlock() + obj, found = s.items[index] + return +} diff --git a/pkg/util/cache/cache_test.go b/pkg/util/cache/cache_test.go new file mode 100644 index 000000000..42a58a93d --- /dev/null +++ b/pkg/util/cache/cache_test.go @@ -0,0 +1,90 @@ +/* +Copyright 2014 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 cache + +import ( + "testing" +) + +const ( + maxTestCacheSize int = shardsCount * 2 +) + +func ExpectEntry(t *testing.T, cache Cache, index uint64, expectedValue interface{}) bool { + elem, found := cache.Get(index) + if !found { + t.Errorf("Expected to find entry with key %d", index) + return false + } else if elem != expectedValue { + t.Errorf("Expected to find %v, got %v", expectedValue, elem) + return false + } + return true +} + +func TestBasic(t *testing.T) { + cache := NewCache(maxTestCacheSize) + cache.Add(1, "xxx") + ExpectEntry(t, cache, 1, "xxx") +} + +func TestOverflow(t *testing.T) { + cache := NewCache(maxTestCacheSize) + for i := 0; i < maxTestCacheSize+1; i++ { + cache.Add(uint64(i), "xxx") + } + foundIndexes := make([]uint64, 0) + for i := 0; i < maxTestCacheSize+1; i++ { + _, found := cache.Get(uint64(i)) + if found { + foundIndexes = append(foundIndexes, uint64(i)) + } + } + if len(foundIndexes) != maxTestCacheSize { + t.Errorf("Expect to find %d elements, got %d %v", maxTestCacheSize, len(foundIndexes), foundIndexes) + } +} + +func TestOverwrite(t *testing.T) { + cache := NewCache(maxTestCacheSize) + cache.Add(1, "xxx") + ExpectEntry(t, cache, 1, "xxx") + cache.Add(1, "yyy") + ExpectEntry(t, cache, 1, "yyy") +} + +// TestEvict this test will fail sporatically depending on what add() +// selects for the randomKey to be evicted. Ensure that randomKey +// is never the key we most recently added. Since the chance of failure +// on each evict is 50%, if we do it 7 times, it should catch the problem +// if it exists >99% of the time. +func TestEvict(t *testing.T) { + cache := NewCache(shardsCount) + var found bool + for retry := 0; retry < 7; retry++ { + cache.Add(uint64(shardsCount), "xxx") + found = ExpectEntry(t, cache, uint64(shardsCount), "xxx") + if !found { + break + } + cache.Add(0, "xxx") + found = ExpectEntry(t, cache, 0, "xxx") + if !found { + break + } + } +} diff --git a/pkg/util/cache/lruexpirecache.go b/pkg/util/cache/lruexpirecache.go new file mode 100644 index 000000000..9c87ba7d3 --- /dev/null +++ b/pkg/util/cache/lruexpirecache.go @@ -0,0 +1,85 @@ +/* +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 cache + +import ( + "sync" + "time" + + "github.com/golang/groupcache/lru" +) + +// Clock defines an interface for obtaining the current time +type Clock interface { + Now() time.Time +} + +// realClock implements the Clock interface by calling time.Now() +type realClock struct{} + +func (realClock) Now() time.Time { return time.Now() } + +type LRUExpireCache struct { + // clock is used to obtain the current time + clock Clock + + cache *lru.Cache + lock sync.Mutex +} + +// NewLRUExpireCache creates an expiring cache with the given size +func NewLRUExpireCache(maxSize int) *LRUExpireCache { + return &LRUExpireCache{clock: realClock{}, cache: lru.New(maxSize)} +} + +// NewLRUExpireCache creates an expiring cache with the given size, using the specified clock to obtain the current time +func NewLRUExpireCacheWithClock(maxSize int, clock Clock) *LRUExpireCache { + return &LRUExpireCache{clock: clock, cache: lru.New(maxSize)} +} + +type cacheEntry struct { + value interface{} + expireTime time.Time +} + +func (c *LRUExpireCache) Add(key lru.Key, value interface{}, ttl time.Duration) { + c.lock.Lock() + defer c.lock.Unlock() + c.cache.Add(key, &cacheEntry{value, c.clock.Now().Add(ttl)}) + // Remove entry from cache after ttl. + time.AfterFunc(ttl, func() { c.remove(key) }) +} + +func (c *LRUExpireCache) Get(key lru.Key) (interface{}, bool) { + c.lock.Lock() + defer c.lock.Unlock() + e, ok := c.cache.Get(key) + if !ok { + return nil, false + } + if c.clock.Now().After(e.(*cacheEntry).expireTime) { + go c.remove(key) + return nil, false + } + return e.(*cacheEntry).value, true +} + +func (c *LRUExpireCache) remove(key lru.Key) { + c.lock.Lock() + defer c.lock.Unlock() + c.cache.Remove(key) +} diff --git a/pkg/util/cache/lruexpirecache_test.go b/pkg/util/cache/lruexpirecache_test.go new file mode 100644 index 000000000..35c538d48 --- /dev/null +++ b/pkg/util/cache/lruexpirecache_test.go @@ -0,0 +1,68 @@ +/* +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 cache + +import ( + "testing" + "time" + + "k8s.io/kubernetes/pkg/util/clock" + + "github.com/golang/groupcache/lru" +) + +func expectEntry(t *testing.T, c *LRUExpireCache, key lru.Key, value interface{}) { + result, ok := c.Get(key) + if !ok || result != value { + t.Errorf("Expected cache[%v]: %v, got %v", key, value, result) + } +} + +func expectNotEntry(t *testing.T, c *LRUExpireCache, key lru.Key) { + if result, ok := c.Get(key); ok { + t.Errorf("Expected cache[%v] to be empty, got %v", key, result) + } +} + +func TestSimpleGet(t *testing.T) { + c := NewLRUExpireCache(10) + c.Add("long-lived", "12345", 10*time.Hour) + expectEntry(t, c, "long-lived", "12345") +} + +func TestExpiredGet(t *testing.T) { + fakeClock := clock.NewFakeClock(time.Now()) + c := NewLRUExpireCacheWithClock(10, fakeClock) + c.Add("short-lived", "12345", 1*time.Millisecond) + // ensure the entry expired + fakeClock.Step(2 * time.Millisecond) + expectNotEntry(t, c, "short-lived") +} + +func TestLRUOverflow(t *testing.T) { + c := NewLRUExpireCache(4) + c.Add("elem1", "1", 10*time.Hour) + c.Add("elem2", "2", 10*time.Hour) + c.Add("elem3", "3", 10*time.Hour) + c.Add("elem4", "4", 10*time.Hour) + c.Add("elem5", "5", 10*time.Hour) + expectNotEntry(t, c, "elem1") + expectEntry(t, c, "elem2", "2") + expectEntry(t, c, "elem3", "3") + expectEntry(t, c, "elem4", "4") + expectEntry(t, c, "elem5", "5") +}