Merge pull request #1529 from smarterclayton/add_auth_interfaces

Add simple Bearer authenticator filter for Kube
pull/6/head
erictune 2014-10-07 11:23:41 -07:00
commit 5503e95c1d
10 changed files with 643 additions and 0 deletions

View File

@ -29,6 +29,9 @@ import (
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/tokenfile"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/handlers"
"github.com/GoogleCloudPlatform/kubernetes/pkg/capabilities"
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
"github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider"
@ -51,6 +54,7 @@ var (
minionPort = flag.Uint("minion_port", 10250, "The port at which kubelet will be listening on the minions.")
healthCheckMinions = flag.Bool("health_check_minions", true, "If true, health check minions and filter unhealthy ones. Default true")
minionCacheTTL = flag.Duration("minion_cache_ttl", 30*time.Second, "Duration of time to cache minion information. Default 30 seconds")
tokenAuthFile = flag.String("token_auth_file", "", "If set, the file that will be used to secure the API server via token authentication")
etcdServerList util.StringList
machineList util.StringList
corsAllowedOriginList util.StringList
@ -172,6 +176,7 @@ func main() {
ui.InstallSupport(mux)
handler := http.Handler(mux)
if len(corsAllowedOriginList) > 0 {
allowedOriginRegexps, err := util.CompileRegexps(corsAllowedOriginList)
if err != nil {
@ -179,6 +184,16 @@ func main() {
}
handler = apiserver.CORS(handler, allowedOriginRegexps, nil, nil, "true")
}
if len(*tokenAuthFile) != 0 {
auth, err := tokenfile.New(*tokenAuthFile)
if err != nil {
glog.Fatalf("Unable to load the token authentication file '%s': %v", *tokenAuthFile, err)
}
userContexts := handlers.NewUserRequestContext()
handler = handlers.NewRequestAuthenticator(userContexts, bearertoken.New(auth), handlers.Unauthorized, handler)
}
handler = apiserver.RecoverPanics(handler)
s := &http.Server{

View File

@ -0,0 +1,47 @@
/*
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 bearertoken
import (
"net/http"
"strings"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
)
type Authenticator struct {
auth authenticator.Token
}
func New(auth authenticator.Token) *Authenticator {
return &Authenticator{auth}
}
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]
return a.auth.AuthenticateToken(token)
}

View File

@ -0,0 +1,86 @@
/*
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 bearertoken
import (
"errors"
"net/http"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/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 || err != nil {
t.Errorf("expected not authenticated user")
}
}
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)
}
}
}

View File

@ -0,0 +1,53 @@
/*
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 authenticator
import (
"net/http"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
)
// Token checks a string value against a backing authentication store and returns
// information about the current user and true if successful, false if not successful,
// or an error if the token could not be checked.
type Token interface {
AuthenticateToken(token string) (user.Info, bool, error)
}
// Request attempts to extract authentication information from a request and returns
// information about the current user and true if successful, false if not successful,
// or an error if the token could not be checked.
type Request interface {
AuthenticateRequest(req *http.Request) (user.Info, bool, error)
}
// TokenFunc is a function that implements the Token interface.
type TokenFunc func(token string) (user.Info, bool, error)
// AuthenticateToken implements authenticator.Token.
func (f TokenFunc) AuthenticateToken(token string) (user.Info, bool, error) {
return f(token)
}
// RequestFunc is a function that implements the Request interface.
type RequestFunc func(req *http.Request) (user.Info, bool, error)
// AuthenticateRequest implements authenticator.Request.
func (f RequestFunc) AuthenticateRequest(req *http.Request) (user.Info, bool, error) {
return f(req)
}

View File

@ -0,0 +1,70 @@
/*
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 tokenfile
import (
"encoding/csv"
"fmt"
"io"
"os"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
)
type TokenAuthenticator struct {
tokens map[string]*user.DefaultInfo
}
func New(path string) (*TokenAuthenticator, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
tokens := make(map[string]*user.DefaultInfo)
reader := csv.NewReader(file)
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],
}
tokens[record[0]] = obj
}
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
}

View File

@ -0,0 +1,113 @@
/*
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 tokenfile
import (
"io/ioutil"
"os"
"reflect"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
)
func TestTokenFile(t *testing.T) {
auth, err := newWithContents(t, `
token1,user1,uid1
token2,user2,uid2
`)
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",
},
{
Token: "token4",
},
}
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 %f, got %f", 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 New(f.Name())
}

View File

@ -0,0 +1,95 @@
/*
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 handlers
import (
"net/http"
"sync"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
"github.com/golang/glog"
)
// RequestContext is the interface used to associate a user with an http Request.
type RequestContext interface {
Set(*http.Request, user.Info)
Remove(*http.Request)
}
// NewRequestAuthenticator creates an http handler that tries to authenticate the given request as a user, and then
// stores any such user found onto the provided context for the request. If authentication fails or returns an error
// the failed handler is used. On success, handler is invoked to serve the request.
func NewRequestAuthenticator(context RequestContext, auth authenticator.Request, failed http.Handler, handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
user, ok, err := auth.AuthenticateRequest(req)
if err != nil || !ok {
if err != nil {
glog.Errorf("Unable to authenticate the request due to an error: %v", err)
}
failed.ServeHTTP(w, req)
return
}
context.Set(req, user)
defer context.Remove(req)
handler.ServeHTTP(w, req)
})
}
var Unauthorized http.HandlerFunc = unauthorized
// unauthorized serves an unauthorized message to clients.
func unauthorized(w http.ResponseWriter, req *http.Request) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
}
// UserRequestContext allows different levels of a call stack to store/retrieve info about the
// current user associated with an http.Request.
type UserRequestContext struct {
requests map[*http.Request]user.Info
lock sync.Mutex
}
// NewUserRequestContext provides a map for storing and retrieving users associated with requests.
// Be sure to pair each `context.Set(req, user)` call with a `defer context.Remove(req)` call or
// you will leak requests. It implements the RequestContext interface.
func NewUserRequestContext() *UserRequestContext {
return &UserRequestContext{
requests: make(map[*http.Request]user.Info),
}
}
func (c *UserRequestContext) Get(req *http.Request) (user.Info, bool) {
c.lock.Lock()
defer c.lock.Unlock()
user, ok := c.requests[req]
return user, ok
}
func (c *UserRequestContext) Set(req *http.Request, user user.Info) {
c.lock.Lock()
defer c.lock.Unlock()
c.requests[req] = user
}
func (c *UserRequestContext) Remove(req *http.Request) {
c.lock.Lock()
defer c.lock.Unlock()
delete(c.requests, req)
}

View File

@ -0,0 +1,102 @@
/*
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 handlers
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
)
func TestAuthenticateRequest(t *testing.T) {
success := make(chan struct{})
context := NewUserRequestContext()
auth := NewRequestAuthenticator(
context,
authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) {
return &user.DefaultInfo{Name: "user"}, true, nil
}),
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
t.Errorf("unexpected call to failed")
}),
http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
if user, ok := context.Get(req); user == nil || !ok {
t.Errorf("no user stored on context: %#v", context)
}
close(success)
}),
)
auth.ServeHTTP(httptest.NewRecorder(), &http.Request{})
<-success
if len(context.requests) > 0 {
t.Errorf("context should have no stored requests", context)
}
}
func TestAuthenticateRequestFailed(t *testing.T) {
failed := make(chan struct{})
context := NewUserRequestContext()
auth := NewRequestAuthenticator(
context,
authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) {
return nil, false, nil
}),
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
close(failed)
}),
http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
t.Errorf("unexpected call to handler")
}),
)
auth.ServeHTTP(httptest.NewRecorder(), &http.Request{})
<-failed
if len(context.requests) > 0 {
t.Errorf("context should have no stored requests", context)
}
}
func TestAuthenticateRequestError(t *testing.T) {
failed := make(chan struct{})
context := NewUserRequestContext()
auth := NewRequestAuthenticator(
context,
authenticator.RequestFunc(func(req *http.Request) (user.Info, bool, error) {
return nil, false, errors.New("failure")
}),
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
close(failed)
}),
http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
t.Errorf("unexpected call to handler")
}),
)
auth.ServeHTTP(httptest.NewRecorder(), &http.Request{})
<-failed
if len(context.requests) > 0 {
t.Errorf("context should have no stored requests", context)
}
}

19
pkg/auth/user/doc.go Normal file
View File

@ -0,0 +1,19 @@
/*
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 user contains utilities for dealing with simple user exchange in the auth
// packages. The user.Info interface defines an interface for exchanging that info.
package user

43
pkg/auth/user/user.go Normal file
View File

@ -0,0 +1,43 @@
/*
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 user
// UserInfo describes a user that has been authenticated to the system.
type Info interface {
// GetName returns the name that uniquely identifies this user among all
// other active users.
GetName() string
// GetUID returns a unique value for a particular user that will change
// if the user is removed from the system and another user is added with
// the same name.
GetUID() string
}
// DefaultInfo provides a simple user information exchange object
// for components that implement the UserInfo interface.
type DefaultInfo struct {
Name string
UID string
}
func (i *DefaultInfo) GetName() string {
return i.Name
}
func (i *DefaultInfo) GetUID() string {
return i.UID
}