diff --git a/cmd/apiserver/apiserver.go b/cmd/apiserver/apiserver.go index 373889f5e22..75b45d0232c 100644 --- a/cmd/apiserver/apiserver.go +++ b/cmd/apiserver/apiserver.go @@ -23,6 +23,7 @@ import ( "net" "net/http" "strconv" + "strings" "time" "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" @@ -63,6 +64,7 @@ var ( healthCheckMinions = flag.Bool("health_check_minions", true, "If true, health check minions and filter unhealthy ones. Default true.") eventTTL = flag.Duration("event_ttl", 48*time.Hour, "Amount of time to retain events. Default 2 days.") tokenAuthFile = flag.String("token_auth_file", "", "If set, the file that will be used to secure the API server via token authentication.") + authorizationMode = flag.String("authorization_mode", "AlwaysAllow", "Selects how to do authorization. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ",")) etcdServerList util.StringList etcdConfigFile = flag.String("etcd_config", "", "The config file for the etcd client. Mutually exclusive with -etcd_servers.") corsAllowedOriginList util.StringList @@ -159,6 +161,7 @@ func main() { ReadOnlyPort: *readOnlyPort, ReadWritePort: *port, PublicAddress: *publicAddressOverride, + AuthorizationMode: *authorizationMode, } m := master.New(config) diff --git a/cmd/integration/integration.go b/cmd/integration/integration.go index bbb9be4e481..3a12bd45483 100644 --- a/cmd/integration/integration.go +++ b/cmd/integration/integration.go @@ -146,6 +146,7 @@ func startComponents(manifestURL string) (apiServerURL string) { KubeletClient: fakeKubeletClient{}, EnableLogsSupport: false, APIPrefix: "/api", + AuthorizationMode: "AlwaysAllow", ReadWritePort: portNumber, ReadOnlyPort: portNumber, diff --git a/hack/test-integration.sh b/hack/test-integration.sh index eb9200d269b..e99829fb6e9 100755 --- a/hack/test-integration.sh +++ b/hack/test-integration.sh @@ -41,7 +41,7 @@ start_etcd echo "" echo "Integration test cases..." echo "" -GOFLAGS="-tags 'integration no-docker'" \ +GOFLAGS="-tags 'integration no-docker' -test.v" \ "${KUBE_ROOT}/hack/test-go.sh" test/integration echo "" diff --git a/pkg/apiserver/authz.go b/pkg/apiserver/authz.go new file mode 100644 index 00000000000..09470f51543 --- /dev/null +++ b/pkg/apiserver/authz.go @@ -0,0 +1,68 @@ +/* +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 apiserver + +import ( + "errors" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer" +) + +// Attributes implements authorizer.Attributes interface. +type Attributes struct { + // TODO: add fields and methods when authorizer.Attributes is completed. +} + +// alwaysAllowAuthorizer is an implementation of authorizer.Attributes +// which always says yes to an authorization request. +// It is useful in tests and when using kubernetes in an open manner. +type alwaysAllowAuthorizer struct{} + +func (alwaysAllowAuthorizer) Authorize(a authorizer.Attributes) (err error) { + return nil +} + +// alwaysDenyAuthorizer is an implementation of authorizer.Attributes +// which always says no to an authorization request. +// It is useful in unit tests to force an operation to be forbidden. +type alwaysDenyAuthorizer struct{} + +func (alwaysDenyAuthorizer) Authorize(a authorizer.Attributes) (err error) { + return errors.New("Everything is forbidden.") +} + +const ( + ModeAlwaysAllow string = "AlwaysAllow" + ModeAlwaysDeny string = "AlwaysDeny" +) + +// Keep this list in sync with constant list above. +var AuthorizationModeChoices = []string{ModeAlwaysAllow, ModeAlwaysDeny} + +// NewAuthorizerFromAuthorizationConfig returns the right sort of authorizer.Authorizer +// based on the authorizationMode xor an error. authorizationMode should be one of AuthorizationModeChoices. +func NewAuthorizerFromAuthorizationConfig(authorizationMode string) (authorizer.Authorizer, error) { + // Keep cases in sync with constant list above. + switch authorizationMode { + case ModeAlwaysAllow: + return new(alwaysAllowAuthorizer), nil + case ModeAlwaysDeny: + return new(alwaysDenyAuthorizer), nil + default: + return nil, errors.New("Unknown authorization mode") + } +} diff --git a/pkg/apiserver/handlers.go b/pkg/apiserver/handlers.go index 43336de7a24..304b3335d39 100644 --- a/pkg/apiserver/handlers.go +++ b/pkg/apiserver/handlers.go @@ -23,6 +23,7 @@ import ( "runtime/debug" "strings" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer" "github.com/GoogleCloudPlatform/kubernetes/pkg/httplog" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/golang/glog" @@ -118,3 +119,24 @@ func CORS(handler http.Handler, allowedOriginPatterns []*regexp.Regexp, allowedM handler.ServeHTTP(w, req) }) } + +// RequestAttributeGetter is a function that extracts authorizer.Attributes from an http.Request +type RequestAttributeGetter func(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 +} + +// 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)) + if err == nil { + handler.ServeHTTP(w, req) + return + } + forbidden(w, req) + }) +} diff --git a/pkg/auth/authorizer/interfaces.go b/pkg/auth/authorizer/interfaces.go new file mode 100644 index 00000000000..72a23062903 --- /dev/null +++ b/pkg/auth/authorizer/interfaces.go @@ -0,0 +1,30 @@ +/* +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 authorizer + +// 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. +} + +// Authorizer makes an authorization decision based on information gained by making +// zero or more calls to methods of the Attributes interface. It returns nil when an action is +// authorized, otherwise it returns an error. +type Authorizer interface { + Authorize(a Attributes) (err error) +} diff --git a/pkg/master/master.go b/pkg/master/master.go index f085fc2a7fd..8401c00c1d8 100644 --- a/pkg/master/master.go +++ b/pkg/master/master.go @@ -66,6 +66,7 @@ type Config struct { APIPrefix string CorsAllowedOriginList util.StringList TokenAuthFile string + AuthorizationMode string // Number of masters running; all masters must be started with the // same value for this field. (Numbers > 1 currently untested.) @@ -101,6 +102,7 @@ type Master struct { apiPrefix string corsAllowedOriginList util.StringList tokenAuthFile string + authorizationzMode string masterCount int // "Outputs" @@ -220,9 +222,11 @@ func New(c *Config) *Master { apiPrefix: c.APIPrefix, corsAllowedOriginList: c.CorsAllowedOriginList, tokenAuthFile: c.TokenAuthFile, - masterCount: c.MasterCount, - readOnlyServer: net.JoinHostPort(c.PublicAddress, strconv.Itoa(int(c.ReadOnlyPort))), - readWriteServer: net.JoinHostPort(c.PublicAddress, strconv.Itoa(int(c.ReadWritePort))), + authorizationzMode: c.AuthorizationMode, + + masterCount: c.MasterCount, + readOnlyServer: net.JoinHostPort(c.PublicAddress, strconv.Itoa(int(c.ReadOnlyPort))), + readWriteServer: net.JoinHostPort(c.PublicAddress, strconv.Itoa(int(c.ReadWritePort))), } m.masterServices = util.NewRunner(m.serviceWriterLoop, m.roServiceWriterLoop) m.init(c) @@ -310,6 +314,14 @@ func (m *Master) init(c *Config) { handler = apiserver.CORS(handler, allowedOriginRegexps, nil, nil, "true") } + // Install Authorizer + authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(m.authorizationzMode) + if err != nil { + glog.Fatal(err) + } + handler = apiserver.WithAuthorizationCheck(handler, apiserver.BasicAttributeGetter, authorizer) + + // Install Authenticator if authenticator != nil { handler = handlers.NewRequestAuthenticator(userContexts, authenticator, handlers.Unauthorized, handler) } diff --git a/test/integration/auth_test.go b/test/integration/auth_test.go index 515a9204c18..a031794017a 100644 --- a/test/integration/auth_test.go +++ b/test/integration/auth_test.go @@ -23,6 +23,7 @@ package integration // to work for any client of the HTTP interface. import ( + "bytes" "fmt" "io/ioutil" "net/http" @@ -70,6 +71,7 @@ xyz987,bob,2 EnableUISupport: false, APIPrefix: "/api", TokenAuthFile: f.Name(), + AuthorizationMode: "AlwaysAllow", }) s := httptest.NewServer(m.Handler) @@ -118,3 +120,293 @@ xyz987,bob,2 } } } + +// Bodies for requests used in subsequent tests. +var aPod string = ` +{ + "kind": "Pod", + "apiVersion": "v1beta1", + "id": "a", + "desiredState": { + "manifest": { + "version": "v1beta1", + "id": "a", + "containers": [{ "name": "foo", "image": "bar/foo", }] + } + }, +} +` +var aRC string = ` +{ + "kind": "ReplicationController", + "apiVersion": "v1beta1", + "id": "a", + "desiredState": { + "replicas": 2, + "replicaSelector": {"name": "a"}, + "podTemplate": { + "desiredState": { + "manifest": { + "version": "v1beta1", + "id": "a", + "containers": [{ + "name": "foo", + "image": "bar/foo", + }] + } + }, + "labels": {"name": "a"} + }}, + "labels": {"name": "a"} +} +` +var aService string = ` +{ + "kind": "Service", + "apiVersion": "v1beta1", + "id": "a", + "port": 8000, + "labels": { "name": "a" }, + "selector": { "name": "a" } +} +` +var aMinion string = ` +{ + "kind": "Minion", + "apiVersion": "v1beta1", + "id": "a", + "hostIP": "10.10.10.10", +} +` + +var aEvent string = ` +{ + "kind": "Binding", + "apiVersion": "v1beta1", + "id": "a", + "involvedObject": { + { + "kind": "Minion", + "name": "a" + "apiVersion": "v1beta1", + } +} +` + +var aBinding string = ` +{ + "kind": "Binding", + "apiVersion": "v1beta1", + "id": "a", + "host": "10.10.10.10", + "podID": "a" +} +` + +var aEndpoints string = ` +{ + "kind": "Endpoints", + "apiVersion": "v1beta1", + "id": "a", + "endpoints": ["10.10.1.1:1909"], +} +` + +// Requests to try. Each one should be forbidden or not forbidden +// depending on the authentication and authorization setup of the master. + +func getTestRequests() []struct { + verb string + URL string + body string +} { + requests := []struct { + verb string + URL string + body string + }{ + // Normal methods on pods + {"GET", "/api/v1beta1/pods", ""}, + {"GET", "/api/v1beta1/pods/a", ""}, + {"POST", "/api/v1beta1/pods", aPod}, + {"PUT", "/api/v1beta1/pods", aPod}, + {"GET", "/api/v1beta1/pods", ""}, + {"GET", "/api/v1beta1/pods/a", ""}, + {"DELETE", "/api/v1beta1/pods", ""}, + + // Non-standard methods (not expected to work, + // but expected to pass/fail authorization prior to + // failing validation. + {"PATCH", "/api/v1beta1/pods/a", ""}, + {"OPTIONS", "/api/v1beta1/pods", ""}, + {"OPTIONS", "/api/v1beta1/pods/a", ""}, + {"HEAD", "/api/v1beta1/pods", ""}, + {"HEAD", "/api/v1beta1/pods/a", ""}, + {"TRACE", "/api/v1beta1/pods", ""}, + {"TRACE", "/api/v1beta1/pods/a", ""}, + {"NOSUCHVERB", "/api/v1beta1/pods", ""}, + + // Normal methods on services + {"GET", "/api/v1beta1/services", ""}, + {"GET", "/api/v1beta1/services/a", ""}, + {"POST", "/api/v1beta1/services", aService}, + {"PUT", "/api/v1beta1/services", aService}, + {"GET", "/api/v1beta1/services", ""}, + {"GET", "/api/v1beta1/services/a", ""}, + {"DELETE", "/api/v1beta1/services", ""}, + + // Normal methods on replicationControllers + {"GET", "/api/v1beta1/replicationControllers", ""}, + {"GET", "/api/v1beta1/replicationControllers/a", ""}, + {"POST", "/api/v1beta1/replicationControllers", aRC}, + {"PUT", "/api/v1beta1/replicationControllers", aRC}, + {"GET", "/api/v1beta1/replicationControllers", ""}, + {"GET", "/api/v1beta1/replicationControllers/a", ""}, + {"DELETE", "/api/v1beta1/replicationControllers", ""}, + + // Normal methods on endpoints + {"GET", "/api/v1beta1/endpoints", ""}, + {"GET", "/api/v1beta1/endpoints/a", ""}, + {"POST", "/api/v1beta1/endpoints", aEndpoints}, + {"PUT", "/api/v1beta1/endpoints", aEndpoints}, + {"GET", "/api/v1beta1/endpoints", ""}, + {"GET", "/api/v1beta1/endpoints/a", ""}, + {"DELETE", "/api/v1beta1/endpoints", ""}, + + // Normal methods on minions + {"GET", "/api/v1beta1/minions", ""}, + {"GET", "/api/v1beta1/minions/a", ""}, + {"POST", "/api/v1beta1/minions", aMinion}, + {"PUT", "/api/v1beta1/minions", aMinion}, + {"GET", "/api/v1beta1/minions", ""}, + {"GET", "/api/v1beta1/minions/a", ""}, + {"DELETE", "/api/v1beta1/minions", ""}, + + // Normal methods on events + {"GET", "/api/v1beta1/events", ""}, + {"GET", "/api/v1beta1/events/a", ""}, + {"POST", "/api/v1beta1/events", aEvent}, + {"PUT", "/api/v1beta1/events", aEvent}, + {"GET", "/api/v1beta1/events", ""}, + {"GET", "/api/v1beta1/events/a", ""}, + {"DELETE", "/api/v1beta1/events", ""}, + + // Normal methods on bindings + {"GET", "/api/v1beta1/events", ""}, + {"GET", "/api/v1beta1/events/a", ""}, + {"POST", "/api/v1beta1/events", aBinding}, + {"PUT", "/api/v1beta1/events", aBinding}, + {"GET", "/api/v1beta1/events", ""}, + {"GET", "/api/v1beta1/events/a", ""}, + {"DELETE", "/api/v1beta1/events", ""}, + + // Non-existent object type. + {"GET", "/api/v1beta1/foo", ""}, + {"GET", "/api/v1beta1/foo/a", ""}, + {"POST", "/api/v1beta1/foo", `{"foo": "foo"}`}, + {"PUT", "/api/v1beta1/foo", `{"foo": "foo"}`}, + {"GET", "/api/v1beta1/foo", ""}, + {"GET", "/api/v1beta1/foo/a", ""}, + {"DELETE", "/api/v1beta1/foo", ""}, + + // Operations + {"GET", "/api/v1beta1/operations", ""}, + {"GET", "/api/v1beta1/operations/1234567890", ""}, + + // Special verbs on pods + {"GET", "/api/v1beta1/proxy/pods/a", ""}, + {"GET", "/api/v1beta1/redirect/pods/a", ""}, + // TODO: test .../watch/..., which doesn't end before the test timeout. + + // Non-object endpoints + {"GET", "/", ""}, + {"GET", "/healthz", ""}, + {"GET", "/versions", ""}, + } + return requests +} + +// The TestAuthMode* tests tests a large number of URLs and checks that they +// are FORBIDDEN or not, depending on the mode. They do not attempt to do +// detailed verification of behaviour beyond authorization. They are not +// fuzz tests. +// +// TODO(etune): write a fuzz test of the REST API. +func TestAuthModeAlwaysAllow(t *testing.T) { + deleteAllEtcdKeys() + + // 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, + EnableLogsSupport: false, + EnableUISupport: false, + APIPrefix: "/api", + AuthorizationMode: "AlwaysAllow", + }) + + s := httptest.NewServer(m.Handler) + defer s.Close() + transport := http.DefaultTransport + + for _, r := range getTestRequests() { + 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) + } + resp, err := transport.RoundTrip(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.StatusCode == http.StatusForbidden { + t.Errorf("Expected status other than Forbidden") + } + } +} + +func TestAuthModeAlwaysDeny(t *testing.T) { + deleteAllEtcdKeys() + + // 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, + EnableLogsSupport: false, + EnableUISupport: false, + APIPrefix: "/api", + AuthorizationMode: "AlwaysDeny", + }) + + s := httptest.NewServer(m.Handler) + defer s.Close() + transport := http.DefaultTransport + + for _, r := range getTestRequests() { + 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) + } + + resp, err := transport.RoundTrip(req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.StatusCode != http.StatusForbidden { + t.Errorf("Expected status Forbidden but got status %v", resp.Status) + } + } +} diff --git a/test/integration/client_test.go b/test/integration/client_test.go index 77eb6ca2fa1..3521d282d5c 100644 --- a/test/integration/client_test.go +++ b/test/integration/client_test.go @@ -44,6 +44,7 @@ func TestClient(t *testing.T) { EnableLogsSupport: false, EnableUISupport: false, APIPrefix: "/api", + AuthorizationMode: "AlwaysAllow", }) s := httptest.NewServer(m.Handler)