diff --git a/staging/BUILD b/staging/BUILD index b997ee21102..181807d6438 100644 --- a/staging/BUILD +++ b/staging/BUILD @@ -119,6 +119,7 @@ filegroup( "//staging/src/k8s.io/apiserver/pkg/authentication/serviceaccount:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authentication/token/cache:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authentication/token/tokenfile:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/authentication/token/union:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authentication/user:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory:all-srcs", diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/token/union/BUILD b/staging/src/k8s.io/apiserver/pkg/authentication/token/union/BUILD new file mode 100644 index 00000000000..7a6b23db96b --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authentication/token/union/BUILD @@ -0,0 +1,41 @@ +package(default_visibility = ["//visibility:public"]) + +licenses(["notice"]) + +load( + "@io_bazel_rules_go//go:def.bzl", + "go_library", + "go_test", +) + +go_test( + name = "go_default_test", + srcs = ["unionauth_test.go"], + library = ":go_default_library", + tags = ["automanaged"], + deps = ["//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library"], +) + +go_library( + name = "go_default_library", + srcs = ["union.go"], + tags = ["automanaged"], + deps = [ + "//vendor/k8s.io/apimachinery/pkg/util/errors:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/authenticator:go_default_library", + "//vendor/k8s.io/apiserver/pkg/authentication/user:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], +) diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/token/union/union.go b/staging/src/k8s.io/apiserver/pkg/authentication/token/union/union.go new file mode 100644 index 00000000000..7cc391bc488 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authentication/token/union/union.go @@ -0,0 +1,70 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package union + +import ( + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +// unionAuthTokenHandler authenticates tokens using a chain of authenticator.Token objects +type unionAuthTokenHandler struct { + // Handlers is a chain of request authenticators to delegate to + Handlers []authenticator.Token + // FailOnError determines whether an error returns short-circuits the chain + FailOnError bool +} + +// New returns a token authenticator that validates credentials using a chain of authenticator.Token objects. +// The entire chain is tried until one succeeds. If all fail, an aggregate error is returned. +func New(authTokenHandlers ...authenticator.Token) authenticator.Token { + if len(authTokenHandlers) == 1 { + return authTokenHandlers[0] + } + return &unionAuthTokenHandler{Handlers: authTokenHandlers, FailOnError: false} +} + +// NewFailOnError returns a token authenticator that validates credentials using a chain of authenticator.Token objects. +// The first error short-circuits the chain. +func NewFailOnError(authTokenHandlers ...authenticator.Token) authenticator.Token { + if len(authTokenHandlers) == 1 { + return authTokenHandlers[0] + } + return &unionAuthTokenHandler{Handlers: authTokenHandlers, FailOnError: true} +} + +// AuthenticateToken authenticates the token using a chain of authenticator.Token objects. +func (authHandler *unionAuthTokenHandler) AuthenticateToken(token string) (user.Info, bool, error) { + var errlist []error + for _, currAuthRequestHandler := range authHandler.Handlers { + info, ok, err := currAuthRequestHandler.AuthenticateToken(token) + 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/staging/src/k8s.io/apiserver/pkg/authentication/token/union/unionauth_test.go b/staging/src/k8s.io/apiserver/pkg/authentication/token/union/unionauth_test.go new file mode 100644 index 00000000000..1107c575467 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authentication/token/union/unionauth_test.go @@ -0,0 +1,158 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package union + +import ( + "errors" + "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) AuthenticateToken(token string) (user.Info, bool, error) { + return mock.returnUser, mock.isAuthenticated, mock.err +} + +func TestAuthenticateTokenSecondPasses(t *testing.T) { + handler1 := &mockAuthRequestHandler{returnUser: user1} + handler2 := &mockAuthRequestHandler{returnUser: user2, isAuthenticated: true} + authRequestHandler := New(handler1, handler2) + + authenticatedUser, isAuthenticated, err := authRequestHandler.AuthenticateToken("foo") + 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 TestAuthenticateTokenFirstPasses(t *testing.T) { + handler1 := &mockAuthRequestHandler{returnUser: user1, isAuthenticated: true} + handler2 := &mockAuthRequestHandler{returnUser: user2} + authRequestHandler := New(handler1, handler2) + + authenticatedUser, isAuthenticated, err := authRequestHandler.AuthenticateToken("foo") + 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 TestAuthenticateTokenSuppressUnnecessaryErrors(t *testing.T) { + handler1 := &mockAuthRequestHandler{err: errors.New("first")} + handler2 := &mockAuthRequestHandler{isAuthenticated: true} + authRequestHandler := New(handler1, handler2) + + _, isAuthenticated, err := authRequestHandler.AuthenticateToken("foo") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if !isAuthenticated { + t.Errorf("Unexpectedly unauthenticated: %v", isAuthenticated) + } +} + +func TestAuthenticateTokenNoAuthenticators(t *testing.T) { + authRequestHandler := New() + + authenticatedUser, isAuthenticated, err := authRequestHandler.AuthenticateToken("foo") + 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 TestAuthenticateTokenNonePass(t *testing.T) { + handler1 := &mockAuthRequestHandler{} + handler2 := &mockAuthRequestHandler{} + authRequestHandler := New(handler1, handler2) + + _, isAuthenticated, err := authRequestHandler.AuthenticateToken("foo") + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + if isAuthenticated { + t.Errorf("Unexpectedly authenticated: %v", isAuthenticated) + } +} + +func TestAuthenticateTokenAdditiveErrors(t *testing.T) { + handler1 := &mockAuthRequestHandler{err: errors.New("first")} + handler2 := &mockAuthRequestHandler{err: errors.New("second")} + authRequestHandler := New(handler1, handler2) + + _, isAuthenticated, err := authRequestHandler.AuthenticateToken("foo") + 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 TestAuthenticateTokenFailEarly(t *testing.T) { + handler1 := &mockAuthRequestHandler{err: errors.New("first")} + handler2 := &mockAuthRequestHandler{err: errors.New("second")} + authRequestHandler := NewFailOnError(handler1, handler2) + + _, isAuthenticated, err := authRequestHandler.AuthenticateToken("foo") + 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) + } +}