mirror of https://github.com/k3s-io/k3s
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.pull/6/head
parent
f4cffdc7cf
commit
6e81e8c896
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.)
|
|
@ -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.
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue