diff --git a/pkg/apiserver/handlers.go b/pkg/apiserver/handlers.go index 304b3335d39..da9a2daeb26 100644 --- a/pkg/apiserver/handlers.go +++ b/pkg/apiserver/handlers.go @@ -24,6 +24,7 @@ import ( "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer" + authhandlers "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/handlers" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/golang/glog" @@ -121,18 +122,34 @@ func CORS(handler http.Handler, allowedOriginPatterns []*regexp.Regexp, allowedM } // RequestAttributeGetter is a function that extracts authorizer.Attributes from an http.Request -type RequestAttributeGetter func(req *http.Request) (attribs authorizer.Attributes) +type RequestAttributeGetter interface { + GetAttribs(req *http.Request) (attribs authorizer.Attributes) +} -// BasicAttributeGetter gets authorizer.Attributes from an http.Request. -func BasicAttributeGetter(req *http.Request) (attribs authorizer.Attributes) { - // TODO: fill in attributes once attributes are defined. - return +type requestAttributeGetter struct { + userContexts authhandlers.RequestContext +} + +// NewAttributeGetter returns an object which implements the RequestAttributeGetter interface. +func NewRequestAttributeGetter(userContexts authhandlers.RequestContext) RequestAttributeGetter { + return &requestAttributeGetter{userContexts} +} + +func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attributes { + attribs := authorizer.AttributesRecord{} + + user, ok := r.userContexts.Get(req) + if ok { + attribs.User = user + } + + return &attribs } // WithAuthorizationCheck passes all authorized requests on to handler, and returns a forbidden error otherwise. func WithAuthorizationCheck(handler http.Handler, getAttribs RequestAttributeGetter, a authorizer.Authorizer) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - err := a.Authorize(getAttribs(req)) + err := a.Authorize(getAttribs.GetAttribs(req)) if err == nil { handler.ServeHTTP(w, req) return diff --git a/pkg/auth/authorizer/interfaces.go b/pkg/auth/authorizer/interfaces.go index 72a23062903..61de4c4259c 100644 --- a/pkg/auth/authorizer/interfaces.go +++ b/pkg/auth/authorizer/interfaces.go @@ -16,10 +16,14 @@ limitations under the License. package authorizer +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" +) + // Attributes is an interface used by an Authorizer to get information about a request // that is used to make an authorization decision. type Attributes interface { - // TODO: add attribute getter functions, e.g. GetUserName(), per #1430. + GetUserName() string } // Authorizer makes an authorization decision based on information gained by making @@ -28,3 +32,12 @@ type Attributes interface { type Authorizer interface { Authorize(a Attributes) (err error) } + +// AttributesRecord implements Attributes interface. +type AttributesRecord struct { + User user.Info +} + +func (a *AttributesRecord) GetUserName() string { + return a.User.GetName() +} diff --git a/pkg/auth/handlers/handlers.go b/pkg/auth/handlers/handlers.go index 3e4e6030970..68334381de6 100644 --- a/pkg/auth/handlers/handlers.go +++ b/pkg/auth/handlers/handlers.go @@ -28,6 +28,7 @@ import ( // RequestContext is the interface used to associate a user with an http Request. type RequestContext interface { Set(*http.Request, user.Info) + Get(req *http.Request) (user.Info, bool) Remove(*http.Request) } diff --git a/pkg/master/master.go b/pkg/master/master.go index c856c596718..5d3ee34759d 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -30,6 +30,7 @@ import ( "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/authorizer" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/handlers" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/cloudprovider" @@ -67,6 +68,7 @@ type Config struct { CorsAllowedOriginList util.StringList TokenAuthFile string AuthorizationMode string + AuthorizerForTesting authorizer.Authorizer // Number of masters running; all masters must be started with the // same value for this field. (Numbers > 1 currently untested.) @@ -318,11 +320,18 @@ func (m *Master) init(c *Config) { } // Install Authorizer - authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(m.authorizationzMode) - if err != nil { - glog.Fatal(err) + var authorizer authorizer.Authorizer + if c.AuthorizerForTesting != nil { + authorizer = c.AuthorizerForTesting + } else { + var err error + authorizer, err = apiserver.NewAuthorizerFromAuthorizationConfig(m.authorizationzMode) + if err != nil { + glog.Fatal(err) + } } - handler = apiserver.WithAuthorizationCheck(handler, apiserver.BasicAttributeGetter, authorizer) + attributeGetter := apiserver.NewRequestAttributeGetter(userContexts) + handler = apiserver.WithAuthorizationCheck(handler, attributeGetter, authorizer) // Install Authenticator if authenticator != nil { diff --git a/test/integration/auth_test.go b/test/integration/auth_test.go index cfdd4da6778..df60f1c5468 100644 --- a/test/integration/auth_test.go +++ b/test/integration/auth_test.go @@ -24,6 +24,7 @@ package integration import ( "bytes" + "errors" "fmt" "io/ioutil" "net/http" @@ -31,6 +32,7 @@ import ( "os" "testing" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/master" @@ -441,3 +443,190 @@ func TestAuthModeAlwaysDeny(t *testing.T) { } } } + +// Inject into master an authorizer that uses user info. +// TODO(etune): remove this test once a more comprehensive built-in authorizer is implemented. +type allowAliceAuthorizer struct{} + +func (allowAliceAuthorizer) Authorize(a authorizer.Attributes) error { + if a.GetUserName() == "alice" { + return nil + } + return errors.New("I can't allow that. Go ask alice.") +} + +// TestAliceNotForbiddenOrUnauthorized tests a user who is known to +// the authentication system and authorized to do any actions. +func TestAliceNotForbiddenOrUnauthorized(t *testing.T) { + + deleteAllEtcdKeys() + + tokenFilename := writeTestTokenFile() + defer os.Remove(tokenFilename) + // This file has alice and bob in it. + + aaa := allowAliceAuthorizer{} + + // Set up a master + + helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + m := master.New(&master.Config{ + EtcdHelper: helper, + KubeletClient: client.FakeKubeletClient{}, + EnableLogsSupport: false, + EnableUISupport: false, + APIPrefix: "/api", + TokenAuthFile: tokenFilename, + AuthorizerForTesting: aaa, + }) + + s := httptest.NewServer(m.Handler) + defer s.Close() + transport := http.DefaultTransport + + // Alice is authorized. + + // + for _, r := range getTestRequests() { + token := AliceToken + t.Logf("case %v", r) + bodyBytes := bytes.NewReader([]byte(r.body)) + req, err := http.NewRequest(r.verb, s.URL+r.URL, bodyBytes) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + { + resp, err := transport.RoundTrip(req) + defer resp.Body.Close() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if _, ok := r.statusCodes[resp.StatusCode]; !ok { + t.Errorf("Expected status one of %v, but got %v", r.statusCodes, resp.StatusCode) + } + } + } +} + +// TestBobIsForbidden tests that a user who is known to +// the authentication system but not authorized to do any actions +// should receive "Forbidden". +func TestBobIsForbidden(t *testing.T) { + deleteAllEtcdKeys() + + tokenFilename := writeTestTokenFile() + defer os.Remove(tokenFilename) + // This file has alice and bob in it. + + aaa := allowAliceAuthorizer{} + + // Set up a master + + helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + m := master.New(&master.Config{ + EtcdHelper: helper, + KubeletClient: client.FakeKubeletClient{}, + EnableLogsSupport: false, + EnableUISupport: false, + APIPrefix: "/api", + TokenAuthFile: tokenFilename, + AuthorizerForTesting: aaa, + }) + + s := httptest.NewServer(m.Handler) + defer s.Close() + transport := http.DefaultTransport + + // Alice is authorized. + + // + for _, r := range getTestRequests() { + token := BobToken + t.Logf("case %v", r) + bodyBytes := bytes.NewReader([]byte(r.body)) + req, err := http.NewRequest(r.verb, s.URL+r.URL, bodyBytes) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + + { + resp, err := transport.RoundTrip(req) + defer resp.Body.Close() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Expect all of bob's actions to return Forbidden + if resp.StatusCode != http.StatusForbidden { + t.Errorf("Expected not status Forbidden, but got %s", resp.Status) + } + } + } +} + +// TestUnknownUserIsUnauthorized tests that a user who is unknown +// to the authentication system get status code "Unauthorized". +// An authorization module is installed in this scenario for integration +// test purposes, but requests aren't expected to reach it. +func TestUnknownUserIsUnauthorized(t *testing.T) { + deleteAllEtcdKeys() + + tokenFilename := writeTestTokenFile() + defer os.Remove(tokenFilename) + // This file has alice and bob in it. + + aaa := allowAliceAuthorizer{} + + // Set up a master + + helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + m := master.New(&master.Config{ + EtcdHelper: helper, + KubeletClient: client.FakeKubeletClient{}, + EnableLogsSupport: false, + EnableUISupport: false, + APIPrefix: "/api", + TokenAuthFile: tokenFilename, + AuthorizerForTesting: aaa, + }) + + s := httptest.NewServer(m.Handler) + defer s.Close() + transport := http.DefaultTransport + + for _, r := range getTestRequests() { + token := UnknownToken + t.Logf("case %v", r) + bodyBytes := bytes.NewReader([]byte(r.body)) + req, err := http.NewRequest(r.verb, s.URL+r.URL, bodyBytes) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + { + resp, err := transport.RoundTrip(req) + defer resp.Body.Close() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Expect all of unauthenticated user's request to be "Unauthorized" + if resp.StatusCode != http.StatusUnauthorized { + t.Errorf("Expected status Unauthorized, but got %s", resp.Status) + } + } + } +}