mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-03 17:30:00 +00:00
commit
06633bf4cd
@ -57,21 +57,22 @@ var (
|
|||||||
"The port from which to serve read-only resources. If 0, don't serve on a "+
|
"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 "+
|
"read-only address. It is assumed that firewall rules are set up such that "+
|
||||||
"this port is not reachable from outside of the cluster.")
|
"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'.")
|
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")
|
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.")
|
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.")
|
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.")
|
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.")
|
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.")
|
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, ","))
|
authorizationMode = flag.String("authorization_mode", "AlwaysAllow", "Selects how to do authorization. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ","))
|
||||||
etcdServerList util.StringList
|
authorizationPolicyFile = flag.String("authorization_policy_file", "", "File with authorization policy in csv format, used with --authorization_mode=ABAC.")
|
||||||
etcdConfigFile = flag.String("etcd_config", "", "The config file for the etcd client. Mutually exclusive with -etcd_servers.")
|
etcdServerList util.StringList
|
||||||
corsAllowedOriginList util.StringList
|
etcdConfigFile = flag.String("etcd_config", "", "The config file for the etcd client. Mutually exclusive with -etcd_servers.")
|
||||||
allowPrivileged = flag.Bool("allow_privileged", false, "If true, allow privileged containers.")
|
corsAllowedOriginList util.StringList
|
||||||
portalNet util.IPNet // TODO: make this a list
|
allowPrivileged = flag.Bool("allow_privileged", false, "If true, allow privileged containers.")
|
||||||
enableLogsSupport = flag.Bool("enable_logs_support", true, "Enables server endpoint for log collection")
|
portalNet util.IPNet // TODO: make this a list
|
||||||
kubeletConfig = client.KubeletConfig{
|
enableLogsSupport = flag.Bool("enable_logs_support", true, "Enables server endpoint for log collection")
|
||||||
|
kubeletConfig = client.KubeletConfig{
|
||||||
Port: 10250,
|
Port: 10250,
|
||||||
EnableHttps: false,
|
EnableHttps: false,
|
||||||
}
|
}
|
||||||
@ -146,7 +147,7 @@ func main() {
|
|||||||
|
|
||||||
n := net.IPNet(portalNet)
|
n := net.IPNet(portalNet)
|
||||||
|
|
||||||
authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(*authorizationMode)
|
authorizer, err := apiserver.NewAuthorizerFromAuthorizationConfig(*authorizationMode, *authorizationPolicyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Fatalf("Invalid Authorization Config: %v", err)
|
glog.Fatalf("Invalid Authorization Config: %v", err)
|
||||||
}
|
}
|
||||||
|
19
docs/authentication.md
Normal file
19
docs/authentication.md
Normal file
@ -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.)
|
103
docs/authorization.md
Normal file
103
docs/authorization.md
Normal file
@ -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"
|
"errors"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer/abac"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Attributes implements authorizer.Attributes interface.
|
// Attributes implements authorizer.Attributes interface.
|
||||||
@ -56,20 +57,26 @@ func NewAlwaysDenyAuthorizer() authorizer.Authorizer {
|
|||||||
const (
|
const (
|
||||||
ModeAlwaysAllow string = "AlwaysAllow"
|
ModeAlwaysAllow string = "AlwaysAllow"
|
||||||
ModeAlwaysDeny string = "AlwaysDeny"
|
ModeAlwaysDeny string = "AlwaysDeny"
|
||||||
|
ModeABAC string = "ABAC"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Keep this list in sync with constant list above.
|
// 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
|
// NewAuthorizerFromAuthorizationConfig returns the right sort of authorizer.Authorizer
|
||||||
// based on the authorizationMode xor an error. authorizationMode should be one of AuthorizationModeChoices.
|
// 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.
|
// Keep cases in sync with constant list above.
|
||||||
switch authorizationMode {
|
switch authorizationMode {
|
||||||
case ModeAlwaysAllow:
|
case ModeAlwaysAllow:
|
||||||
return NewAlwaysAllowAuthorizer(), nil
|
return NewAlwaysAllowAuthorizer(), nil
|
||||||
case ModeAlwaysDeny:
|
case ModeAlwaysDeny:
|
||||||
return NewAlwaysDenyAuthorizer(), nil
|
return NewAlwaysDenyAuthorizer(), nil
|
||||||
|
case ModeABAC:
|
||||||
|
return abac.NewFromFile(authorizationPolicyFile)
|
||||||
default:
|
default:
|
||||||
return nil, errors.New("Unknown authorization mode")
|
return nil, errors.New("Unknown authorization mode")
|
||||||
}
|
}
|
||||||
|
124
pkg/auth/authorizer/abac/abac.go
Normal file
124
pkg/auth/authorizer/abac/abac.go
Normal file
@ -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.
|
||||||
|
}
|
148
pkg/auth/authorizer/abac/abac_test.go
Normal file
148
pkg/auth/authorizer/abac/abac_test.go
Normal file
@ -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
|
||||||
|
}
|
7
pkg/auth/authorizer/abac/example_policy_file.jsonl
Normal file
7
pkg/auth/authorizer/abac/example_policy_file.jsonl
Normal file
@ -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
|
Kind string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AttributesRecord) GetUserName() string {
|
func (a AttributesRecord) GetUserName() string {
|
||||||
return a.User.GetName()
|
return a.User.GetName()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AttributesRecord) IsReadOnly() bool {
|
func (a AttributesRecord) IsReadOnly() bool {
|
||||||
return a.ReadOnly
|
return a.ReadOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AttributesRecord) GetNamespace() string {
|
func (a AttributesRecord) GetNamespace() string {
|
||||||
return a.Namespace
|
return a.Namespace
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AttributesRecord) GetKind() string {
|
func (a AttributesRecord) GetKind() string {
|
||||||
return a.Kind
|
return a.Kind
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ import (
|
|||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/apiserver"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authorizer"
|
"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/client"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/master"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/master"
|
||||||
)
|
)
|
||||||
@ -621,15 +622,23 @@ func TestUnknownUserIsUnauthorized(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inject into master an authorizer that uses namespace information.
|
func newAuthorizerWithContents(t *testing.T, contents string) authorizer.Authorizer {
|
||||||
// TODO(etune): remove this test once a more comprehensive built-in authorizer is implemented.
|
f, err := ioutil.TempFile("", "auth_test")
|
||||||
type allowFooNamespaceAuthorizer struct{}
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error creating policyfile: %v", err)
|
||||||
func (allowFooNamespaceAuthorizer) Authorize(a authorizer.Attributes) error {
|
|
||||||
if a.GetNamespace() == "foo" {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
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
|
// TestNamespaceAuthorization tests that authorization can be controlled
|
||||||
@ -641,13 +650,13 @@ func TestNamespaceAuthorization(t *testing.T) {
|
|||||||
defer os.Remove(tokenFilename)
|
defer os.Remove(tokenFilename)
|
||||||
// This file has alice and bob in it.
|
// This file has alice and bob in it.
|
||||||
|
|
||||||
// Set up a master
|
|
||||||
|
|
||||||
helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1")
|
helper, err := master.NewEtcdHelper(newEtcdClient(), "v1beta1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a := newAuthorizerWithContents(t, `{"namespace": "foo"}
|
||||||
|
`)
|
||||||
m := master.New(&master.Config{
|
m := master.New(&master.Config{
|
||||||
EtcdHelper: helper,
|
EtcdHelper: helper,
|
||||||
KubeletClient: client.FakeKubeletClient{},
|
KubeletClient: client.FakeKubeletClient{},
|
||||||
@ -655,7 +664,7 @@ func TestNamespaceAuthorization(t *testing.T) {
|
|||||||
EnableUISupport: false,
|
EnableUISupport: false,
|
||||||
APIPrefix: "/api",
|
APIPrefix: "/api",
|
||||||
TokenAuthFile: tokenFilename,
|
TokenAuthFile: tokenFilename,
|
||||||
Authorizer: allowFooNamespaceAuthorizer{},
|
Authorizer: a,
|
||||||
})
|
})
|
||||||
|
|
||||||
s := httptest.NewServer(m.Handler)
|
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
|
// TestKindAuthorization tests that authorization can be controlled
|
||||||
// by namespace.
|
// by namespace.
|
||||||
func TestKindAuthorization(t *testing.T) {
|
func TestKindAuthorization(t *testing.T) {
|
||||||
@ -733,6 +731,8 @@ func TestKindAuthorization(t *testing.T) {
|
|||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a := newAuthorizerWithContents(t, `{"kind": "services"}
|
||||||
|
`)
|
||||||
m := master.New(&master.Config{
|
m := master.New(&master.Config{
|
||||||
EtcdHelper: helper,
|
EtcdHelper: helper,
|
||||||
KubeletClient: client.FakeKubeletClient{},
|
KubeletClient: client.FakeKubeletClient{},
|
||||||
@ -740,7 +740,7 @@ func TestKindAuthorization(t *testing.T) {
|
|||||||
EnableUISupport: false,
|
EnableUISupport: false,
|
||||||
APIPrefix: "/api",
|
APIPrefix: "/api",
|
||||||
TokenAuthFile: tokenFilename,
|
TokenAuthFile: tokenFilename,
|
||||||
Authorizer: allowServicesAuthorizer{},
|
Authorizer: a,
|
||||||
})
|
})
|
||||||
|
|
||||||
s := httptest.NewServer(m.Handler)
|
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
|
// TestReadOnlyAuthorization tests that authorization can be controlled
|
||||||
// by namespace.
|
// by namespace.
|
||||||
func TestReadOnlyAuthorization(t *testing.T) {
|
func TestReadOnlyAuthorization(t *testing.T) {
|
||||||
@ -813,6 +802,8 @@ func TestReadOnlyAuthorization(t *testing.T) {
|
|||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a := newAuthorizerWithContents(t, `{"readonly": true}
|
||||||
|
`)
|
||||||
m := master.New(&master.Config{
|
m := master.New(&master.Config{
|
||||||
EtcdHelper: helper,
|
EtcdHelper: helper,
|
||||||
KubeletClient: client.FakeKubeletClient{},
|
KubeletClient: client.FakeKubeletClient{},
|
||||||
@ -820,7 +811,7 @@ func TestReadOnlyAuthorization(t *testing.T) {
|
|||||||
EnableUISupport: false,
|
EnableUISupport: false,
|
||||||
APIPrefix: "/api",
|
APIPrefix: "/api",
|
||||||
TokenAuthFile: tokenFilename,
|
TokenAuthFile: tokenFilename,
|
||||||
Authorizer: allowReadAuthorizer{},
|
Authorizer: a,
|
||||||
})
|
})
|
||||||
|
|
||||||
s := httptest.NewServer(m.Handler)
|
s := httptest.NewServer(m.Handler)
|
||||||
|
Loading…
Reference in New Issue
Block a user