From 6e81e8c896c533a3617b249c4157f49551294923 Mon Sep 17 00:00:00 2001 From: Eric Tune Date: Mon, 6 Oct 2014 16:11:04 -0700 Subject: [PATCH] Basic ACL file. Added function to read basic ACL from a CSV file. Added implementation of Authorize based on that file's policies. Added docs on authentication and authorization. Added example file and tested it. --- cmd/apiserver/apiserver.go | 33 ++-- docs/authentication.md | 19 +++ docs/authorization.md | 103 ++++++++++++ pkg/apiserver/authz.go | 11 +- pkg/auth/authorizer/abac/abac.go | 124 +++++++++++++++ pkg/auth/authorizer/abac/abac_test.go | 148 ++++++++++++++++++ .../authorizer/abac/example_policy_file.jsonl | 7 + pkg/auth/authorizer/interfaces.go | 8 +- test/integration/auth_test.go | 61 +++----- 9 files changed, 457 insertions(+), 57 deletions(-) create mode 100644 docs/authentication.md create mode 100644 docs/authorization.md create mode 100644 pkg/auth/authorizer/abac/abac.go create mode 100644 pkg/auth/authorizer/abac/abac_test.go create mode 100644 pkg/auth/authorizer/abac/example_policy_file.jsonl diff --git a/cmd/apiserver/apiserver.go b/cmd/apiserver/apiserver.go index 5e65ae8bc1f..29305088a6a 100644 --- a/cmd/apiserver/apiserver.go +++ b/cmd/apiserver/apiserver.go @@ -57,21 +57,22 @@ var ( "The port from which to serve read-only resources. If 0, don't serve on a "+ "read-only address. It is assumed that firewall rules are set up such that "+ "this port is not reachable from outside of the cluster.") - apiPrefix = flag.String("api_prefix", "/api", "The prefix for API requests on the server. Default '/api'.") - storageVersion = flag.String("storage_version", "", "The version to store resources with. Defaults to server preferred") - cloudProvider = flag.String("cloud_provider", "", "The provider for cloud services. Empty string for no provider.") - cloudConfigFile = flag.String("cloud_config", "", "The path to the cloud provider configuration file. Empty string for no configuration file.") - 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 - allowPrivileged = flag.Bool("allow_privileged", false, "If true, allow privileged containers.") - portalNet util.IPNet // TODO: make this a list - enableLogsSupport = flag.Bool("enable_logs_support", true, "Enables server endpoint for log collection") - kubeletConfig = client.KubeletConfig{ + apiPrefix = flag.String("api_prefix", "/api", "The prefix for API requests on the server. Default '/api'.") + storageVersion = flag.String("storage_version", "", "The version to store resources with. Defaults to server preferred") + cloudProvider = flag.String("cloud_provider", "", "The provider for cloud services. Empty string for no provider.") + cloudConfigFile = flag.String("cloud_config", "", "The path to the cloud provider configuration file. Empty string for no configuration file.") + 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, ",")) + authorizationPolicyFile = flag.String("authorization_policy_file", "", "File with authorization policy in csv format, used with --authorization_mode=ABAC.") + etcdServerList util.StringList + etcdConfigFile = flag.String("etcd_config", "", "The config file for the etcd client. Mutually exclusive with -etcd_servers.") + corsAllowedOriginList util.StringList + allowPrivileged = flag.Bool("allow_privileged", false, "If true, allow privileged containers.") + portalNet util.IPNet // TODO: make this a list + enableLogsSupport = flag.Bool("enable_logs_support", true, "Enables server endpoint for log collection") + kubeletConfig = client.KubeletConfig{ Port: 10250, EnableHttps: false, } @@ -146,7 +147,7 @@ func main() { n := net.IPNet(portalNet) - authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(*authorizationMode) + authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(*authorizationMode, *authorizationPolicyFile) if err != nil { glog.Fatalf("Invalid Authorization Config: %v", err) } diff --git a/docs/authentication.md b/docs/authentication.md new file mode 100644 index 00000000000..dc4273151fd --- /dev/null +++ b/docs/authentication.md @@ -0,0 +1,19 @@ +# Authentication Plugins + +Kubernetes uses tokens to authenticate users for API calls. + +Authentication is enabled by passing the `--token_auth_file=SOMEFILE` option +to apiserver. Currently, tokens last indefinitely, and the token list cannot +be changed without restarting apiserver. We plan in the future for tokens to +be short-lived, and to be generated as needed rather than stored in a file. + +The token file format is implemented in `pkg/auth/authenticator/tokenfile/...` +and is a csv file with 3 columns: token, user name, user uid. + +## Plugin Development + +We plan for the Kubernetes API server to issue tokens +after the user has been (re)authenticated by a *bedrock* authentication +provider external to Kubernetes. We plan to make it easy to develop modules +that interface between kubernetes and a bedrock authentication provider (e.g. +github.com, google.com, enterprise directory, kerberos, etc.) diff --git a/docs/authorization.md b/docs/authorization.md new file mode 100644 index 00000000000..da959a3406b --- /dev/null +++ b/docs/authorization.md @@ -0,0 +1,103 @@ +# Authorization Plugins + + +In Kubernetes, authorization happens as a separate step from authentication. +See the [authentication documentation](../authn_plugins/README.md) for an +overview of authentication. + +Authorization applies to all HTTP accesses on the main apiserver port. (The +readonly port is not currently subject to authorization, but is planned to be +removed soon.) + +The authorization check for any request compares attributes of the context of +the request, (such as user, resource kind, and namespace) with access +policies. An API call must be allowed by some policy in order to proceed. + +The following implementations are available, and are selected by flag: + - `--authoriation_mode=AlwaysDeny` + - `--authoriation_mode=AlwaysAllow` + - `--authoriation_mode=ABAC` + +`AlwaysDeny` blocks all requests (used in tests). +`AlwaysAllow` allows all requests; use if you don't need authorization. +`ABAC` allows for user-configured authorization policy. ABAC stands for Attribute-Based Access Control. + +## ABAC Mode +### Request Attributes + +A request has 4 attributes that can be considered for authorization: + - user (the user-string which a user was authenticated as). + - whether the request is readonly (GETs are readonly) + - what kind of object is being accessed + - applies only to the API endpoints, such as + `/api/v1beta1/pods`. For miscelaneous endpoints, like `/version`, the + kind is the empty string. + - the namespace of the object being access, or the empty string if the + endpoint does not support namespaced objects. + +We anticipate adding more attributes to allow finer grained access control and +to assist in policy management. + +### Policy File Format + +For mode `ABAC`, also specify `--authorization_policy_file=SOME_FILENAME`. + +The file format is [one JSON object per line](http://jsonlines.org/). There should be no enclosing list or map, just +one map per line. + +Each line is a "policy object". A policy object is a map with the following properties: + - `user`, type string; the user-string from `--token_auth_file` + - `readonly`, type boolean, when true, means that the policy only applies to GET + operations. + - `kind`, type string; a kind of object, from an URL, such as `pods`. + - `namespace`, type string; a namespace string. + +An unset property is the same as a property set to the zero value for its type (e.g. empty string, 0, false). +However, unset should be preferred for readability. + +In the future, policies may be expressed in a JSON format, and managed via a REST +interface. + +### Authorization Algorithm + +A request has attributes which correspond to the properties of a policy object. + +When a request is received, the attributes are determined. Unknown attributes +are set to the zero value of its type (e.g. empty string, 0, false). + +An unset property will match any value of the corresponding +attribute. An unset attribute will match any value of the corresponding property. + +The tuple of attributes is checked for a match against every policy in the policy file. +If at least one line matches the request attributes, then the request is authorized (but may fail later validation). + +To permit any user to do something, write a policy with the user property unset. +To permit an action Policy with an unset namespace applies regardless of namespace. + +### Examples + 1. Alice can do anything: `{"user":"alice"}` + 2. Kubelet can read any pods: `{"user":"kubelet", "kind": "pods", "readonly": true}` + 3. Kubelet can read and write events: `{"user":"kubelet", "kind": "events"}` + 4. Bob can just read pods in namespace "projectCaribou": `{"user":"bob", "kind": "pods", "readonly": true, "ns": "projectCaribou"}` + +[Complete file example](../pkg/auth/authorizer/abac/example_policy_file.jsonl) + +## Plugin Developement + +Other implementations can be developed fairly easily. +The APIserver calls the Authorizer interface: +```go +type Authorizer interface { + Authorize(a Attributes) error +} +``` +to determine whether or not to allow each API action. + +An authorization plugin is a module that implements this interface. +Authorization plugin code goes in `pkg/auth/authorization/$MODULENAME`. + +An authorization module can be completely implemented in go, or can call out +to a remote authorization service. Authorization modules can implement +their own caching to reduce the cost of repeated authorization calls with the +same or similar arguments. Developers should then consider the interaction between +caching and revokation of permissions. diff --git a/pkg/apiserver/authz.go b/pkg/apiserver/authz.go index 882af6739e9..90037fc0f53 100644 --- a/pkg/apiserver/authz.go +++ b/pkg/apiserver/authz.go @@ -20,6 +20,7 @@ import ( "errors" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer/abac" ) // Attributes implements authorizer.Attributes interface. @@ -56,20 +57,26 @@ func NewAlwaysDenyAuthorizer() authorizer.Authorizer { const ( ModeAlwaysAllow string = "AlwaysAllow" ModeAlwaysDeny string = "AlwaysDeny" + ModeABAC string = "ABAC" ) // Keep this list in sync with constant list above. -var AuthorizationModeChoices = []string{ModeAlwaysAllow, ModeAlwaysDeny} +var AuthorizationModeChoices = []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC} // 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) { +func NewAuthorizerFromAuthorizationConfig(authorizationMode string, authorizationPolicyFile string) (authorizer.Authorizer, error) { + if authorizationPolicyFile != "" && authorizationMode != "ABAC" { + return nil, errors.New("Cannot specify --authorization_policy_file without mode ABAC") + } // Keep cases in sync with constant list above. switch authorizationMode { case ModeAlwaysAllow: return NewAlwaysAllowAuthorizer(), nil case ModeAlwaysDeny: return NewAlwaysDenyAuthorizer(), nil + case ModeABAC: + return abac.NewFromFile(authorizationPolicyFile) default: return nil, errors.New("Unknown authorization mode") } diff --git a/pkg/auth/authorizer/abac/abac.go b/pkg/auth/authorizer/abac/abac.go new file mode 100644 index 00000000000..f7fbd7ec106 --- /dev/null +++ b/pkg/auth/authorizer/abac/abac.go @@ -0,0 +1,124 @@ +/* +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 abac + +// Policy authorizes Kubernetes API actions using an Attribute-based access +// control scheme. + +import ( + "bufio" + "encoding/json" + "errors" + "os" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer" +) + +// TODO: make this into a real API object. Note that when that happens, it +// will get MetaData. However, the Kind and Namespace in the struct below +// will be separate from the Kind and Namespace in the Metadata. Obviously, +// meta.Kind will be something like policy, and policy.Kind has to be allowed +// to be different. Less obviously, namespace needs to be different as well. +// This will allow wildcard matching strings to be used in the future for the +// body.Namespace, if we want to add that feature, without affecting the +// meta.Namespace. +type policy struct { + User string `json:"user,omitempty" yaml:"user,omitempty"` + // TODO: add support for groups as well as users. + // TODO: add support for robot accounts as well as human user accounts. + // TODO: decide how to namespace user names when multiple authentication + // providers are in use. Either add "Realm", or assume "user@example.com" + // format. + + // TODO: Make the "cluster" Kinds be one API group (minions, bindings, + // events, endpoints). The "user" Kinds are another (pods, services, + // replicationControllers, operations) Make a "plugin", e.g. build + // controller, be another group. That way when we add a new object to a + // the API, we don't have to add lots of policy? + + // TODO: make this a proper REST object with its own registry. + Readonly bool `json:"readonly,omitempty" yaml:"readonly,omitempty"` + Kind string `json:"kind,omitempty" yaml:"kind,omitempty"` + Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + + // TODO: "expires" string in RFC3339 format. + + // TODO: want a way to allow some users to restart containers of a pod but + // not delete or modify it. + + // TODO: want a way to allow a controller to create a pod based only on a + // certain podTemplates. +} + +type policyList []policy + +// TODO: Have policies be created via an API call and stored in REST storage. +func NewFromFile(path string) (policyList, error) { + // File format is one map per line. This allows easy concatentation of files, + // comments in files, and identification of errors by line number. + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + pl := make(policyList, 0) + var p policy + + for scanner.Scan() { + b := scanner.Bytes() + // TODO: skip comment lines. + err = json.Unmarshal(b, &p) + if err != nil { + // TODO: line number in errors. + return nil, err + } + pl = append(pl, p) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + return pl, nil +} + +func (p policy) matches(a authorizer.Attributes) bool { + if p.User == "" || p.User == a.GetUserName() { + if p.Readonly == false || (p.Readonly == a.IsReadOnly()) { + if p.Kind == "" || (p.Kind == a.GetKind()) { + if p.Namespace == "" || (p.Namespace == a.GetNamespace()) { + return true + } + } + } + } + return false +} + +// Authorizer implements authorizer.Authorize +func (pl policyList) Authorize(a authorizer.Attributes) error { + for _, p := range pl { + if p.matches(a) { + return nil + } + } + return errors.New("No policy matched.") + // TODO: Benchmark how much time policy matching takes with a medium size + // policy file, compared to other steps such as encoding/decoding. + // Then, add Caching only if needed. +} diff --git a/pkg/auth/authorizer/abac/abac_test.go b/pkg/auth/authorizer/abac/abac_test.go new file mode 100644 index 00000000000..ded50a87daf --- /dev/null +++ b/pkg/auth/authorizer/abac/abac_test.go @@ -0,0 +1,148 @@ +/* +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 abac + +import ( + "io/ioutil" + "os" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user" +) + +func TestEmptyFile(t *testing.T) { + _, err := newWithContents(t, "") + if err != nil { + t.Errorf("unable to read policy file: %v", err) + } +} + +func TestOneLineFileNoNewLine(t *testing.T) { + _, err := newWithContents(t, `{"user":"scheduler", "readonly": true, "kind": "pods", "namespace":"ns1"}`) + if err != nil { + t.Errorf("unable to read policy file: %v", err) + } +} + +func TestTwoLineFile(t *testing.T) { + _, err := newWithContents(t, `{"user":"scheduler", "readonly": true, "kind": "pods"} +{"user":"scheduler", "readonly": true, "kind": "services"} +`) + if err != nil { + t.Errorf("unable to read policy file: %v", err) + } +} + +// Test the file that we will point users at as an example. +func TestExampleFile(t *testing.T) { + _, err := NewFromFile("./example_policy_file.jsonl") + if err != nil { + t.Errorf("unable to read policy file: %v", err) + } +} + +func NotTestAuthorize(t *testing.T) { + a, err := newWithContents(t, `{ "readonly": true, "kind": "events"} +{"user":"scheduler", "readonly": true, "kind": "pods"} +{"user":"scheduler", "kind": "bindings"} +{"user":"kubelet", "readonly": true, "kind": "bindings"} +{"user":"kubelet", "kind": "events"} +{"user":"alice", "ns": "projectCaribou"} +{"user":"bob", "readonly": true, "ns": "projectCaribou"} +`) + if err != nil { + t.Fatalf("unable to read policy file: %v", err) + } + + uScheduler := user.DefaultInfo{Name: "scheduler", UID: "uid1"} + uAlice := user.DefaultInfo{Name: "alice", UID: "uid3"} + uChuck := user.DefaultInfo{Name: "chuck", UID: "uid5"} + + testCases := []struct { + User user.DefaultInfo + RO bool + Kind string + NS string + ExpectAllow bool + }{ + // Scheduler can read pods + {User: uScheduler, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: true}, + {User: uScheduler, RO: true, Kind: "pods", NS: "", ExpectAllow: true}, + // Scheduler cannot write pods + {User: uScheduler, RO: false, Kind: "pods", NS: "ns1", ExpectAllow: false}, + {User: uScheduler, RO: false, Kind: "pods", NS: "", ExpectAllow: false}, + // Scheduler can write bindings + {User: uScheduler, RO: true, Kind: "bindings", NS: "ns1", ExpectAllow: true}, + {User: uScheduler, RO: true, Kind: "bindings", NS: "", ExpectAllow: true}, + + // Alice can read and write anything in the right namespace. + {User: uAlice, RO: true, Kind: "pods", NS: "projectCaribou", ExpectAllow: true}, + {User: uAlice, RO: true, Kind: "widgets", NS: "projectCaribou", ExpectAllow: true}, + {User: uAlice, RO: true, Kind: "", NS: "projectCaribou", ExpectAllow: true}, + {User: uAlice, RO: false, Kind: "pods", NS: "projectCaribou", ExpectAllow: true}, + {User: uAlice, RO: false, Kind: "widgets", NS: "projectCaribou", ExpectAllow: true}, + {User: uAlice, RO: false, Kind: "", NS: "projectCaribou", ExpectAllow: true}, + // .. but not the wrong namespace. + {User: uAlice, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: false}, + {User: uAlice, RO: true, Kind: "widgets", NS: "ns1", ExpectAllow: false}, + {User: uAlice, RO: true, Kind: "", NS: "ns1", ExpectAllow: false}, + + // Chuck can read events, since anyone can. + {User: uChuck, RO: true, Kind: "events", NS: "ns1", ExpectAllow: true}, + {User: uChuck, RO: true, Kind: "events", NS: "", ExpectAllow: true}, + // Chuck can't do other things. + {User: uChuck, RO: false, Kind: "events", NS: "ns1", ExpectAllow: false}, + {User: uChuck, RO: true, Kind: "pods", NS: "ns1", ExpectAllow: false}, + {User: uChuck, RO: true, Kind: "floop", NS: "ns1", ExpectAllow: false}, + // Chunk can't access things with no kind or namespace + // TODO: find a way to give someone access to miscelaneous endpoints, such as + // /healthz, /version, etc. + {User: uChuck, RO: true, Kind: "", NS: "", ExpectAllow: false}, + } + for _, tc := range testCases { + attr := authorizer.AttributesRecord{ + User: &tc.User, + ReadOnly: tc.RO, + Kind: tc.Kind, + Namespace: tc.NS, + } + t.Logf("tc: %v -> attr %v", tc, attr) + err := a.Authorize(attr) + actualAllow := bool(err == nil) + if tc.ExpectAllow != actualAllow { + t.Errorf("Expected allowed=%v but actually allowed=%v, for case %v", + tc.ExpectAllow, actualAllow, tc) + } + } +} + +func newWithContents(t *testing.T, contents string) (authorizer.Authorizer, error) { + f, err := ioutil.TempFile("", "abac_test") + if err != nil { + t.Fatalf("unexpected error creating policyfile: %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 policyfile: %v", err) + } + + pl, err := NewFromFile(f.Name()) + return pl, err +} diff --git a/pkg/auth/authorizer/abac/example_policy_file.jsonl b/pkg/auth/authorizer/abac/example_policy_file.jsonl new file mode 100644 index 00000000000..7842fa89749 --- /dev/null +++ b/pkg/auth/authorizer/abac/example_policy_file.jsonl @@ -0,0 +1,7 @@ +{"user":"admin"} +{"user":"scheduler", "readonly": true, "kind": "pods"} +{"user":"scheduler", "kind": "bindings"} +{"user":"kubelet", "readonly": true, "kind": "bindings"} +{"user":"kubelet", "kind": "events"} +{"user":"alice", "ns": "projectCaribou"} +{"user":"bob", "readonly": true, "ns": "projectCaribou"} diff --git a/pkg/auth/authorizer/interfaces.go b/pkg/auth/authorizer/interfaces.go index dd5ad05fcc9..5913244141e 100644 --- a/pkg/auth/authorizer/interfaces.go +++ b/pkg/auth/authorizer/interfaces.go @@ -54,18 +54,18 @@ type AttributesRecord struct { Kind string } -func (a *AttributesRecord) GetUserName() string { +func (a AttributesRecord) GetUserName() string { return a.User.GetName() } -func (a *AttributesRecord) IsReadOnly() bool { +func (a AttributesRecord) IsReadOnly() bool { return a.ReadOnly } -func (a *AttributesRecord) GetNamespace() string { +func (a AttributesRecord) GetNamespace() string { return a.Namespace } -func (a *AttributesRecord) GetKind() string { +func (a AttributesRecord) GetKind() string { return a.Kind } diff --git a/test/integration/auth_test.go b/test/integration/auth_test.go index b5e952f7bc3..b01877cbbeb 100644 --- a/test/integration/auth_test.go +++ b/test/integration/auth_test.go @@ -34,6 +34,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver" "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer" + "github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer/abac" "github.com/GoogleCloudPlatform/kubernetes/pkg/client" "github.com/GoogleCloudPlatform/kubernetes/pkg/master" ) @@ -621,15 +622,23 @@ func TestUnknownUserIsUnauthorized(t *testing.T) { } } -// Inject into master an authorizer that uses namespace information. -// TODO(etune): remove this test once a more comprehensive built-in authorizer is implemented. -type allowFooNamespaceAuthorizer struct{} - -func (allowFooNamespaceAuthorizer) Authorize(a authorizer.Attributes) error { - if a.GetNamespace() == "foo" { - return nil +func newAuthorizerWithContents(t *testing.T, contents string) authorizer.Authorizer { + f, err := ioutil.TempFile("", "auth_test") + if err != nil { + t.Fatalf("unexpected error creating policyfile: %v", err) } - return errors.New("I can't allow that. Try another namespace, buddy.") + f.Close() + defer os.Remove(f.Name()) + + if err := ioutil.WriteFile(f.Name(), []byte(contents), 0700); err != nil { + t.Fatalf("unexpected error writing policyfile: %v", err) + } + + pl, err := abac.NewFromFile(f.Name()) + if err != nil { + t.Fatalf("unexpected error creating authorizer from policyfile: %v", err) + } + return pl } // TestNamespaceAuthorization tests that authorization can be controlled @@ -641,13 +650,13 @@ func TestNamespaceAuthorization(t *testing.T) { defer os.Remove(tokenFilename) // This file has alice and bob in it. - // Set up a master - helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1") if err != nil { t.Fatalf("unexpected error: %v", err) } + a := newAuthorizerWithContents(t, `{"namespace": "foo"} +`) m := master.New(&master.Config{ EtcdHelper: helper, KubeletClient: client.FakeKubeletClient{}, @@ -655,7 +664,7 @@ func TestNamespaceAuthorization(t *testing.T) { EnableUISupport: false, APIPrefix: "/api", TokenAuthFile: tokenFilename, - Authorizer: allowFooNamespaceAuthorizer{}, + Authorizer: a, }) s := httptest.NewServer(m.Handler) @@ -706,17 +715,6 @@ func TestNamespaceAuthorization(t *testing.T) { } } -// Inject into master an authorizer that uses kind information. -// TODO(etune): remove this test once a more comprehensive built-in authorizer is implemented. -type allowServicesAuthorizer struct{} - -func (allowServicesAuthorizer) Authorize(a authorizer.Attributes) error { - if a.GetKind() == "services" { - return nil - } - return errors.New("I can't allow that. Hint: try services.") -} - // TestKindAuthorization tests that authorization can be controlled // by namespace. func TestKindAuthorization(t *testing.T) { @@ -733,6 +731,8 @@ func TestKindAuthorization(t *testing.T) { t.Fatalf("unexpected error: %v", err) } + a := newAuthorizerWithContents(t, `{"kind": "services"} +`) m := master.New(&master.Config{ EtcdHelper: helper, KubeletClient: client.FakeKubeletClient{}, @@ -740,7 +740,7 @@ func TestKindAuthorization(t *testing.T) { EnableUISupport: false, APIPrefix: "/api", TokenAuthFile: tokenFilename, - Authorizer: allowServicesAuthorizer{}, + Authorizer: a, }) s := httptest.NewServer(m.Handler) @@ -786,17 +786,6 @@ func TestKindAuthorization(t *testing.T) { } } -// Inject into master an authorizer that uses ReadOnly information. -// TODO(etune): remove this test once a more comprehensive built-in authorizer is implemented. -type allowReadAuthorizer struct{} - -func (allowReadAuthorizer) Authorize(a authorizer.Attributes) error { - if a.IsReadOnly() { - return nil - } - return errors.New("I'm afraid I can't let you do that.") -} - // TestReadOnlyAuthorization tests that authorization can be controlled // by namespace. func TestReadOnlyAuthorization(t *testing.T) { @@ -813,6 +802,8 @@ func TestReadOnlyAuthorization(t *testing.T) { t.Fatalf("unexpected error: %v", err) } + a := newAuthorizerWithContents(t, `{"readonly": true} +`) m := master.New(&master.Config{ EtcdHelper: helper, KubeletClient: client.FakeKubeletClient{}, @@ -820,7 +811,7 @@ func TestReadOnlyAuthorization(t *testing.T) { EnableUISupport: false, APIPrefix: "/api", TokenAuthFile: tokenFilename, - Authorizer: allowReadAuthorizer{}, + Authorizer: a, }) s := httptest.NewServer(m.Handler)