From f7ebc7d0d5a2d28481d8b86a913d17edfe53ba19 Mon Sep 17 00:00:00 2001 From: Eric Tune Date: Mon, 27 Oct 2014 14:18:02 -0700 Subject: [PATCH] Added /_whoami and integration test for auth(z|n) Added new endpoint /_whoami for debugging authentication. Added integration test which checks that a user is authenticated using token authentication. Rearranged initialization of authenticator to support preceeding. --- pkg/master/handlers.go | 48 ++++++++++++++ pkg/master/master.go | 22 ++++--- test/integration/auth_test.go | 114 ++++++++++++++++++++++++++++++++++ 3 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 pkg/master/handlers.go create mode 100644 test/integration/auth_test.go diff --git a/pkg/master/handlers.go b/pkg/master/handlers.go new file mode 100644 index 0000000000..b23843b0b3 --- /dev/null +++ b/pkg/master/handlers.go @@ -0,0 +1,48 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package master + +import ( + "net/http" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator" +) + +// handleWhoAmI returns the user-string which this request is authenticated as (if any). +// Useful for debugging authentication. Always returns HTTP status okay and a human +// readable (not intended as API) description of authentication state of request. +func handleWhoAmI(auth authenticator.Request) func(w http.ResponseWriter, req *http.Request) { + return func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + if auth == nil { + w.Write([]byte("NO AUTHENTICATION SUPPORT")) + return + } + userInfo, ok, err := auth.AuthenticateRequest(req) + if err != nil { + w.Write([]byte("ERROR WHILE AUTHENTICATING")) + return + } + if !ok { + w.Write([]byte("NOT AUTHENTICATED")) + return + } + w.Write([]byte("AUTHENTICATED AS " + userInfo.GetName())) + return + } +} diff --git a/pkg/master/master.go b/pkg/master/master.go index b315cf4a17..627e5475ba 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -27,6 +27,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta1" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/v1beta2" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/tokenfile" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/handlers" @@ -160,6 +161,16 @@ func (m *Master) init(c *Config) { } } + var userContexts = handlers.NewUserRequestContext() + var authenticator authenticator.Request + if len(c.TokenAuthFile) != 0 { + tokenAuthenticator, err := tokenfile.New(c.TokenAuthFile) + if err != nil { + glog.Fatalf("Unable to load the token authentication file '%s': %v", c.TokenAuthFile, err) + } + authenticator = bearertoken.New(tokenAuthenticator) + } + m.storage = map[string]apiserver.RESTStorage{ "pods": pod.NewREST(&pod.RESTConfig{ CloudProvider: c.Cloud, @@ -197,14 +208,11 @@ func (m *Master) init(c *Config) { handler = apiserver.CORS(handler, allowedOriginRegexps, nil, nil, "true") } - if len(c.TokenAuthFile) != 0 { - auth, err := tokenfile.New(c.TokenAuthFile) - if err != nil { - glog.Fatalf("Unable to load the token authentication file '%s': %v", c.TokenAuthFile, err) - } - userContexts := handlers.NewUserRequestContext() - handler = handlers.NewRequestAuthenticator(userContexts, bearertoken.New(auth), handlers.Unauthorized, handler) + if authenticator != nil { + handler = handlers.NewRequestAuthenticator(userContexts, authenticator, handlers.Unauthorized, handler) } + m.mux.HandleFunc("/_whoami", handleWhoAmI(authenticator)) + m.Handler = handler } diff --git a/test/integration/auth_test.go b/test/integration/auth_test.go new file mode 100644 index 0000000000..a22c7581a0 --- /dev/null +++ b/test/integration/auth_test.go @@ -0,0 +1,114 @@ +// +build integration,!no-etcd + +/* +Copyright 2014 Google Inc. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integration + +// This file tests authentication and (soon) authorization of HTTP requests to a master object. +// It does not use the client in pkg/client/... because authentication and authorization needs +// to work for any client of the HTTP interface. + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/master" +) + +func init() { + requireEtcd() +} + +// TestWhoAmI passes a known Bearer Token to the master's /_whoami endpoint and checks that +// the master authenticates the user. +func TestWhoAmI(t *testing.T) { + deleteAllEtcdKeys() + + // Write a token file. + json := ` +abc123,alice,1 +xyz987,bob,2 +` + f, err := ioutil.TempFile("", "auth_integration_test") + f.Close() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer os.Remove(f.Name()) + if err := ioutil.WriteFile(f.Name(), []byte(json), 0700); err != nil { + t.Fatalf("unexpected error writing tokenfile: %v", err) + } + + // Set up a master + + helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + mux := http.NewServeMux() + + master.New(&master.Config{ + EtcdHelper: helper, + Mux: mux, + EnableLogsSupport: false, + EnableUISupport: false, + APIPrefix: "/api", + TokenAuthFile: f.Name(), + }) + + s := httptest.NewServer(mux) + defer s.Close() + + // TODO: also test TLS, using e.g NewUnsafeTLSTransport() and NewClientCertTLSTransport() (see pkg/client/helper.go) + transport := http.DefaultTransport + + testCases := []struct { + name string + token string + expected string + }{ + {"Valid token", "abc123", "AUTHENTICATED AS alice"}, + {"Unknown token", "456jkl", "NOT AUTHENTICATED"}, + {"Empty token", "", "NOT AUTHENTICATED"}, + } + for _, tc := range testCases { + req, err := http.NewRequest("GET", s.URL+"/_whoami", nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", tc.token)) + + resp, err := transport.RoundTrip(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + actual := string(body) + if tc.expected != actual { + t.Errorf("case: %s expected: %v got: %v", tc.name, tc.expected, actual) + } + } +}