Merge pull request #16148 from liggitt/mkulke-fix_kubectl_for_namespaced_users

Auto commit by PR queue bot
pull/6/head
k8s-merge-robot 2015-12-03 11:32:08 -08:00
commit 611770778f
16 changed files with 1492 additions and 213 deletions

View File

@ -57,16 +57,18 @@ The following implementations are available, and are selected by flag:
### Request Attributes
A request has 5 attributes that can be considered for authorization:
A request has the following attributes that can be considered for authorization:
- user (the user-string which a user was authenticated as).
- group (the list of group names the authenticated user is a member of).
- whether the request is readonly (GETs are readonly).
- what resource is being accessed.
- applies only to the API endpoints, such as
`/api/v1/namespaces/default/pods`. For miscellaneous endpoints, like `/version`, the
resource is the empty string.
- the namespace of the object being access, or the empty string if the
endpoint does not support namespaced objects.
- whether the request is for an API resource.
- the request path.
- allows authorizing access to miscellaneous endpoints like `/api` or `/healthz` (see [kubectl](#kubectl)).
- the request verb.
- API verbs like `get`, `list`, `create`, `update`, and `watch` are used for API requests
- HTTP verbs like `get`, `post`, and `put` are used for non-API requests
- what resource is being accessed (for API requests only)
- the namespace of the object being accessed (for namespaced API requests only)
- the API group being accessed (for API requests only)
We anticipate adding more attributes to allow finer grained access control and
to assist in policy management.
@ -79,18 +81,29 @@ The file format is [one JSON object per line](http://jsonlines.org/). There sho
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`. If you specify `user`, it must match the username of the authenticated user.
- `group`, type string; if you specify `group`, it must match one of the groups of the authenticated user.
- `readonly`, type boolean, when true, means that the policy only applies to GET
operations.
- `resource`, type string; a resource from an URL, such as `pods`.
- `namespace`, type string; a namespace string.
- Versioning properties:
- `apiVersion`, type string; valid values are "abac.authorization.kubernetes.io/v1beta1". Allows versioning and conversion of the policy format.
- `kind`, type string: valid values are "Policy". Allows versioning and conversion of the policy format.
- `spec` property set to a map with the following properties:
- Subject-matching properties:
- `user`, type string; the user-string from `--token-auth-file`. If you specify `user`, it must match the username of the authenticated user. `*` matches all requests.
- `group`, type string; if you specify `group`, it must match one of the groups of the authenticated user. `*` matches all requests.
- `readonly`, type boolean, when true, means that the policy only applies to get, list, and watch operations.
- Resource-matching properties:
- `apiGroup`, type string; an API group, such as `extensions`. `*` matches all API groups.
- `namespace`, type string; a namespace string. `*` matches all resource requests.
- `resource`, type string; a resource, such as `pods`. `*` matches all resource requests.
- Non-resource-matching properties:
- `nonResourcePath`, type string; matches the non-resource request paths (like `/version` and `/apis`). `*` matches all non-resource requests. `/foo/*` matches `/foo/` and all of its subpaths.
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.
In the future, policies may be expressed in a JSON format, and managed via a REST interface.
### Authorization Algorithm
@ -99,21 +112,35 @@ 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.
A property set to "*" will match any value of the corresponding attribute.
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.
To permit any user to do something, write a policy with the user property set to "*".
To permit a user to do anything, write a policy with the apiGroup, namespace, resource, and nonResourcePath properties set to "*".
### Kubectl
Kubectl uses the `/api` and `/apis` endpoints of api-server to negotiate client/server versions. To validate objects sent to the API by create/update operations, kubectl queries certain swagger resources. For API version `v1` those would be `/swaggerapi/api/v1` & `/swaggerapi/experimental/v1`.
When using ABAC authorization, those special resources have to be explicitly exposed via the `nonResourcePath` property in a policy (see [examples](#examples) below):
* `/api`, `/api/*`, `/apis`, and `/apis/*` for API version negotiation.
* `/version` for retrieving the server version via `kubectl version`.
* `/swaggerapi/*` for create/update operations.
To inspect the HTTP calls involved in a specific kubectl operation you can turn up the verbosity:
kubectl --v=8 version
### Examples
1. Alice can do anything: `{"user":"alice"}`
2. Kubelet can read any pods: `{"user":"kubelet", "resource": "pods", "readonly": true}`
3. Kubelet can read and write events: `{"user":"kubelet", "resource": "events"}`
4. Bob can just read pods in namespace "projectCaribou": `{"user":"bob", "resource": "pods", "readonly": true, "namespace": "projectCaribou"}`
1. Alice can do anything to all resources: `{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "alice", "namespace": "*", "resource": "*", "apiGroup": "*"}}`
2. Kubelet can read any pods: `{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "kubelet", "namespace": "*", "resource": "pods", "readonly": true}}`
3. Kubelet can read and write events: `{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "kubelet", "namespace": "*", "resource": "events"}}`
4. Bob can just read pods in namespace "projectCaribou": `{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "bob", "namespace": "projectCaribou", "resource": "pods", "readonly": true}}`
5. Anyone can make read-only requests to all non-API paths: `{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "*", "readonly": true, "nonResourcePath": "*"}}`
[Complete file example](http://releases.k8s.io/HEAD/pkg/auth/authorizer/abac/example_policy_file.jsonl)
@ -134,7 +161,7 @@ system:serviceaccount:<namespace>:default
For example, if you wanted to grant the default service account in the kube-system full privilege to the API, you would add this line to your policy file:
```json
{"user":"system:serviceaccount:kube-system:default"}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","user":"system:serviceaccount:kube-system:default","namespace":"*","resource":"*","apiGroup":"*"}
```
The apiserver will need to be restarted to pickup the new policy lines.

View File

@ -0,0 +1,24 @@
/*
Copyright 2014 The Kubernetes Authors 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 latest
import (
"k8s.io/kubernetes/pkg/apis/abac/v1beta1"
)
// Codec is the default codec for serializing input that should use the latest supported version.
var Codec = v1beta1.Codec

37
pkg/apis/abac/register.go Normal file
View File

@ -0,0 +1,37 @@
/*
Copyright 2015 The Kubernetes Authors 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 api
import (
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/runtime"
)
// Group is the API group for abac
const Group = "abac.authorization.kubernetes.io"
// Scheme is the default instance of runtime.Scheme to which types in the abac API group are registered.
var Scheme = runtime.NewScheme()
func init() {
Scheme.AddInternalGroupVersion(unversioned.GroupVersion{Group: Group, Version: ""})
Scheme.AddKnownTypes(unversioned.GroupVersion{Group: Group, Version: ""},
&Policy{},
)
}
func (*Policy) IsAnAPIObject() {}

70
pkg/apis/abac/types.go Normal file
View File

@ -0,0 +1,70 @@
/*
Copyright 2015 The Kubernetes Authors 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 api
import "k8s.io/kubernetes/pkg/api/unversioned"
// Policy contains a single ABAC policy rule
type Policy struct {
unversioned.TypeMeta
// Spec describes the policy rule
Spec PolicySpec
}
// PolicySpec contains the attributes for a policy rule
type PolicySpec struct {
// User is the username this rule applies to.
// Either user or group is required to match the request.
// "*" matches all users.
User string
// Group is the group this rule applies to.
// Either user or group is required to match the request.
// "*" matches all groups.
Group string
// Readonly matches readonly requests when true, and all requests when false
Readonly bool
// APIGroup is the name of an API group. APIGroup, Resource, and Namespace are required to match resource requests.
// "*" matches all API groups
APIGroup string
// Resource is the name of a resource. APIGroup, Resource, and Namespace are required to match resource requests.
// "*" matches all resources
Resource string
// Namespace is the name of a namespace. APIGroup, Resource, and Namespace are required to match resource requests.
// "*" matches all namespaces (including unnamespaced requests)
Namespace string
// NonResourcePath matches non-resource request paths.
// "*" matches all paths
// "/foo/*" matches all subpaths of foo
NonResourcePath string
// 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.
}

View File

@ -0,0 +1,58 @@
/*
Copyright 2015 The Kubernetes Authors 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 v0
import (
"k8s.io/kubernetes/pkg/apis/abac"
"k8s.io/kubernetes/pkg/conversion"
)
func init() {
api.Scheme.AddConversionFuncs(
func(in *Policy, out *api.Policy, s conversion.Scope) error {
// Begin by copying all fields
out.Spec.User = in.User
out.Spec.Group = in.Group
out.Spec.Namespace = in.Namespace
out.Spec.Resource = in.Resource
out.Spec.Readonly = in.Readonly
// In v0, unspecified user and group matches all subjects
if len(in.User) == 0 && len(in.Group) == 0 {
out.Spec.User = "*"
}
// In v0, leaving namespace empty matches all namespaces
if len(in.Namespace) == 0 {
out.Spec.Namespace = "*"
}
// In v0, leaving resource empty matches all resources
if len(in.Resource) == 0 {
out.Spec.Resource = "*"
}
// Any rule in v0 should match all API groups
out.Spec.APIGroup = "*"
// In v0, leaving namespace and resource blank allows non-resource paths
if len(in.Namespace) == 0 && len(in.Resource) == 0 {
out.Spec.NonResourcePath = "*"
}
return nil
},
)
}

View File

@ -0,0 +1,77 @@
/*
Copyright 2015 The Kubernetes Authors 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 v0_test
import (
"reflect"
"testing"
"k8s.io/kubernetes/pkg/apis/abac"
"k8s.io/kubernetes/pkg/apis/abac/v0"
)
func TestConversion(t *testing.T) {
testcases := map[string]struct {
old *v0.Policy
expected *api.Policy
}{
// a completely empty policy rule allows everything to all users
"empty": {
old: &v0.Policy{},
expected: &api.Policy{Spec: api.PolicySpec{User: "*", Readonly: false, NonResourcePath: "*", Namespace: "*", Resource: "*", APIGroup: "*"}},
},
// specifying a user is preserved
"user": {
old: &v0.Policy{User: "bob"},
expected: &api.Policy{Spec: api.PolicySpec{User: "bob", Readonly: false, NonResourcePath: "*", Namespace: "*", Resource: "*", APIGroup: "*"}},
},
// specifying a group is preserved (and no longer matches all users)
"group": {
old: &v0.Policy{Group: "mygroup"},
expected: &api.Policy{Spec: api.PolicySpec{Group: "mygroup", Readonly: false, NonResourcePath: "*", Namespace: "*", Resource: "*", APIGroup: "*"}},
},
// specifying a namespace removes the * match on non-resource path
"namespace": {
old: &v0.Policy{Namespace: "myns"},
expected: &api.Policy{Spec: api.PolicySpec{User: "*", Readonly: false, NonResourcePath: "", Namespace: "myns", Resource: "*", APIGroup: "*"}},
},
// specifying a resource removes the * match on non-resource path
"resource": {
old: &v0.Policy{Resource: "myresource"},
expected: &api.Policy{Spec: api.PolicySpec{User: "*", Readonly: false, NonResourcePath: "", Namespace: "*", Resource: "myresource", APIGroup: "*"}},
},
// specifying a namespace+resource removes the * match on non-resource path
"namespace+resource": {
old: &v0.Policy{Namespace: "myns", Resource: "myresource"},
expected: &api.Policy{Spec: api.PolicySpec{User: "*", Readonly: false, NonResourcePath: "", Namespace: "myns", Resource: "myresource", APIGroup: "*"}},
},
}
for k, tc := range testcases {
internal := &api.Policy{}
if err := api.Scheme.Convert(tc.old, internal); err != nil {
t.Errorf("%s: unexpected error: %v", k, err)
}
if !reflect.DeepEqual(internal, tc.expected) {
t.Errorf("%s: expected\n\t%#v, got \n\t%#v", k, tc.expected, internal)
}
}
}

View File

@ -0,0 +1,37 @@
/*
Copyright 2015 The Kubernetes Authors 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 v0
import (
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apis/abac"
"k8s.io/kubernetes/pkg/runtime"
)
// GroupVersion is the API group and version for abac v0
var GroupVersion = unversioned.GroupVersion{Group: api.Group, Version: "v0"}
// Codec encodes internal objects to the v0 version for the abac group
var Codec = runtime.CodecFor(api.Scheme, GroupVersion.String())
func init() {
api.Scheme.AddKnownTypes(GroupVersion,
&Policy{},
)
}
func (*Policy) IsAnAPIObject() {}

45
pkg/apis/abac/v0/types.go Normal file
View File

@ -0,0 +1,45 @@
/*
Copyright 2015 The Kubernetes Authors 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 v0
import "k8s.io/kubernetes/pkg/api/unversioned"
// Policy contains a single ABAC policy rule
type Policy struct {
unversioned.TypeMeta `json:",inline"`
// User is the username this rule applies to.
// Either user or group is required to match the request.
// "*" matches all users.
User string `json:"user,omitempty"`
// Group is the group this rule applies to.
// Either user or group is required to match the request.
// "*" matches all groups.
Group string `json:"group,omitempty"`
// Readonly matches readonly requests when true, and all requests when false
Readonly bool `json:"readonly,omitempty"`
// Resource is the name of a resource
// "*" matches all resources
Resource string `json:"resource,omitempty"`
// Namespace is the name of a namespace
// "*" matches all namespaces (including unnamespaced requests)
Namespace string `json:"namespace,omitempty"`
}

View File

@ -0,0 +1,37 @@
/*
Copyright 2015 The Kubernetes Authors 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 v1beta1
import (
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apis/abac"
"k8s.io/kubernetes/pkg/runtime"
)
// GroupVersion is the API group and version for abac v1beta1
var GroupVersion = unversioned.GroupVersion{Group: api.Group, Version: "v1beta1"}
// Codec encodes internal objects to the v1beta1 version for the abac group
var Codec = runtime.CodecFor(api.Scheme, GroupVersion.String())
func init() {
api.Scheme.AddKnownTypes(GroupVersion,
&Policy{},
)
}
func (*Policy) IsAnAPIObject() {}

View File

@ -0,0 +1,60 @@
/*
Copyright 2015 The Kubernetes Authors 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 v1beta1
import "k8s.io/kubernetes/pkg/api/unversioned"
// Policy contains a single ABAC policy rule
type Policy struct {
unversioned.TypeMeta `json:",inline"`
// Spec describes the policy rule
Spec PolicySpec `json:"spec"`
}
// PolicySpec contains the attributes for a policy rule
type PolicySpec struct {
// User is the username this rule applies to.
// Either user or group is required to match the request.
// "*" matches all users.
User string `json:"user,omitempty"`
// Group is the group this rule applies to.
// Either user or group is required to match the request.
// "*" matches all groups.
Group string `json:"group,omitempty"`
// Readonly matches readonly requests when true, and all requests when false
Readonly bool `json:"readonly,omitempty"`
// APIGroup is the name of an API group. APIGroup, Resource, and Namespace are required to match resource requests.
// "*" matches all API groups
APIGroup string `json:"apiGroup,omitempty"`
// Resource is the name of a resource. APIGroup, Resource, and Namespace are required to match resource requests.
// "*" matches all resources
Resource string `json:"resource,omitempty"`
// Namespace is the name of a namespace. APIGroup, Resource, and Namespace are required to match resource requests.
// "*" matches all namespaces (including unnamespaced requests)
Namespace string `json:"namespace,omitempty"`
// NonResourcePath matches non-resource request paths.
// "*" matches all paths
// "/foo/*" matches all subpaths of foo
NonResourcePath string `json:"nonResourcePath,omitempty"`
}

View File

@ -362,19 +362,24 @@ func (r *requestAttributeGetter) GetAttribs(req *http.Request) authorizer.Attrib
}
}
apiRequestInfo, _ := r.requestInfoResolver.GetRequestInfo(req)
requestInfo, _ := r.requestInfoResolver.GetRequestInfo(req)
attribs.APIGroup = apiRequestInfo.APIGroup
attribs.Verb = apiRequestInfo.Verb
// Start with common attributes that apply to resource and non-resource requests
attribs.ResourceRequest = requestInfo.IsResourceRequest
attribs.Path = requestInfo.Path
attribs.Verb = requestInfo.Verb
// If the request was for a resource in an API group, include that info
attribs.APIGroup = requestInfo.APIGroup
// If a path follows the conventions of the REST object store, then
// we can extract the resource. Otherwise, not.
attribs.Resource = apiRequestInfo.Resource
attribs.Resource = requestInfo.Resource
// If the request specifies a namespace, then the namespace is filled in.
// Assumes there is no empty string namespace. Unspecified results
// in empty (does not understand defaulting rules.)
attribs.Namespace = apiRequestInfo.Namespace
attribs.Namespace = requestInfo.Namespace
return &attribs
}

View File

@ -30,6 +30,8 @@ import (
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/util/sets"
)
type fakeRL bool
@ -218,6 +220,83 @@ func TestTimeout(t *testing.T) {
}
}
func TestGetAttribs(t *testing.T) {
r := &requestAttributeGetter{api.NewRequestContextMapper(), &RequestInfoResolver{sets.NewString("api", "apis"), sets.NewString("api")}}
testcases := map[string]struct {
Verb string
Path string
ExpectedAttributes *authorizer.AttributesRecord
}{
"non-resource root": {
Verb: "POST",
Path: "/",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "post",
Path: "/",
},
},
"non-resource api prefix": {
Verb: "GET",
Path: "/api/",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "get",
Path: "/api/",
},
},
"non-resource group api prefix": {
Verb: "GET",
Path: "/apis/extensions/",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "get",
Path: "/apis/extensions/",
},
},
"resource": {
Verb: "POST",
Path: "/api/v1/nodes/mynode",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "create",
Path: "/api/v1/nodes/mynode",
ResourceRequest: true,
Resource: "nodes",
},
},
"namespaced resource": {
Verb: "PUT",
Path: "/api/v1/namespaces/myns/pods/mypod",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "update",
Path: "/api/v1/namespaces/myns/pods/mypod",
ResourceRequest: true,
Namespace: "myns",
Resource: "pods",
},
},
"API group resource": {
Verb: "GET",
Path: "/apis/extensions/v1beta1/namespaces/myns/jobs",
ExpectedAttributes: &authorizer.AttributesRecord{
Verb: "list",
Path: "/apis/extensions/v1beta1/namespaces/myns/jobs",
ResourceRequest: true,
APIGroup: "extensions",
Namespace: "myns",
Resource: "jobs",
},
},
}
for k, tc := range testcases {
req, _ := http.NewRequest(tc.Verb, tc.Path, nil)
attribs := r.GetAttribs(req)
if !reflect.DeepEqual(attribs, tc.ExpectedAttributes) {
t.Errorf("%s: expected\n\t%#v\ngot\n\t%#v", k, tc.ExpectedAttributes, attribs)
}
}
}
func TestGetAPIRequestInfo(t *testing.T) {
successCases := []struct {
method string

View File

@ -21,50 +21,35 @@ package abac
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
"github.com/golang/glog"
"k8s.io/kubernetes/pkg/apis/abac"
"k8s.io/kubernetes/pkg/apis/abac/latest"
"k8s.io/kubernetes/pkg/apis/abac/v0"
_ "k8s.io/kubernetes/pkg/apis/abac/v1beta1"
"k8s.io/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"`
Group string `json:"group,omitempty"`
// 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 (nodes, 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"`
Resource string `json:"resource,omitempty"`
Namespace string `json:"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 policyLoadError struct {
path string
line int
data []byte
err error
}
type policyList []policy
func (p policyLoadError) Error() string {
if p.line >= 0 {
return fmt.Sprintf("error reading policy file %s, line %d: %s: %v", p.path, p.line, string(p.data), p.err)
}
return fmt.Sprintf("error reading policy file %s: %v", p.path, p.err)
}
type policyList []*api.Policy
// TODO: Have policies be created via an API call and stored in REST storage.
func NewFromFile(path string) (policyList, error) {
@ -79,29 +64,151 @@ func NewFromFile(path string) (policyList, error) {
scanner := bufio.NewScanner(file)
pl := make(policyList, 0)
i := 0
unversionedLines := 0
for scanner.Scan() {
var p policy
i++
p := &api.Policy{}
b := scanner.Bytes()
// TODO: skip comment lines.
err = json.Unmarshal(b, &p)
if err != nil {
// TODO: line number in errors.
return nil, err
// skip comment lines and blank lines
trimmed := strings.TrimSpace(string(b))
if len(trimmed) == 0 || strings.HasPrefix(trimmed, "#") {
continue
}
version, kind, err := api.Scheme.DataVersionAndKind(b)
if err != nil {
return nil, policyLoadError{path, i, b, err}
}
if version == "" && kind == "" {
unversionedLines++
// Migrate unversioned policy object
oldPolicy := &v0.Policy{}
if err := latest.Codec.DecodeInto(b, oldPolicy); err != nil {
return nil, policyLoadError{path, i, b, err}
}
if err := api.Scheme.Convert(oldPolicy, p); err != nil {
return nil, policyLoadError{path, i, b, err}
}
} else {
decodedObj, err := latest.Codec.Decode(b)
if err != nil {
return nil, policyLoadError{path, i, b, err}
}
decodedPolicy, ok := decodedObj.(*api.Policy)
if !ok {
return nil, policyLoadError{path, i, b, fmt.Errorf("unrecognized object: %#v", decodedObj)}
}
p = decodedPolicy
}
pl = append(pl, p)
}
if unversionedLines > 0 {
glog.Warningf(`Policy file %s contained unversioned rules. See docs/admin/authorization.md#abac-mode for ABAC file format details.`, path)
}
if err := scanner.Err(); err != nil {
return nil, err
return nil, policyLoadError{path, -1, nil, err}
}
return pl, nil
}
func (p policy) matches(a authorizer.Attributes) bool {
if p.subjectMatches(a) {
if p.Readonly == false || (p.Readonly == a.IsReadOnly()) {
if p.Resource == "" || (p.Resource == a.GetResource()) {
if p.Namespace == "" || (p.Namespace == a.GetNamespace()) {
func matches(p api.Policy, a authorizer.Attributes) bool {
if subjectMatches(p, a) {
if verbMatches(p, a) {
// Resource and non-resource requests are mutually exclusive, at most one will match a policy
if resourceMatches(p, a) {
return true
}
if nonResourceMatches(p, a) {
return true
}
}
}
return false
}
// subjectMatches returns true if specified user and group properties in the policy match the attributes
func subjectMatches(p api.Policy, a authorizer.Attributes) bool {
matched := false
// If the policy specified a user, ensure it matches
if len(p.Spec.User) > 0 {
if p.Spec.User == "*" {
matched = true
} else {
matched = p.Spec.User == a.GetUserName()
if !matched {
return false
}
}
}
// If the policy specified a group, ensure it matches
if len(p.Spec.Group) > 0 {
if p.Spec.Group == "*" {
matched = true
} else {
matched = false
for _, group := range a.GetGroups() {
if p.Spec.Group == group {
matched = true
}
}
if !matched {
return false
}
}
}
return matched
}
func verbMatches(p api.Policy, a authorizer.Attributes) bool {
// TODO: match on verb
// All policies allow read only requests
if a.IsReadOnly() {
return true
}
// Allow if policy is not readonly
if !p.Spec.Readonly {
return true
}
return false
}
func nonResourceMatches(p api.Policy, a authorizer.Attributes) bool {
// A non-resource policy cannot match a resource request
if !a.IsResourceRequest() {
// Allow wildcard match
if p.Spec.NonResourcePath == "*" {
return true
}
// Allow exact match
if p.Spec.NonResourcePath == a.GetPath() {
return true
}
// Allow a trailing * subpath match
if strings.HasSuffix(p.Spec.NonResourcePath, "*") && strings.HasPrefix(a.GetPath(), strings.TrimRight(p.Spec.NonResourcePath, "*")) {
return true
}
}
return false
}
func resourceMatches(p api.Policy, a authorizer.Attributes) bool {
// A resource policy cannot match a non-resource request
if a.IsResourceRequest() {
if p.Spec.Namespace == "*" || p.Spec.Namespace == a.GetNamespace() {
if p.Spec.Resource == "*" || p.Spec.Resource == a.GetResource() {
if p.Spec.APIGroup == "*" || p.Spec.APIGroup == a.GetAPIGroup() {
return true
}
}
@ -110,31 +217,10 @@ func (p policy) matches(a authorizer.Attributes) bool {
return false
}
func (p policy) subjectMatches(a authorizer.Attributes) bool {
if p.User != "" {
// Require user match
if p.User != a.GetUserName() {
return false
}
}
if p.Group != "" {
// Require group match
for _, group := range a.GetGroups() {
if p.Group == group {
return true
}
}
return false
}
return true
}
// Authorizer implements authorizer.Authorize
func (pl policyList) Authorize(a authorizer.Attributes) error {
for _, p := range pl {
if p.matches(a) {
if matches(*p, a) {
return nil
}
}

View File

@ -21,8 +21,12 @@ import (
"os"
"testing"
"k8s.io/kubernetes/pkg/apis/abac"
"k8s.io/kubernetes/pkg/apis/abac/v0"
"k8s.io/kubernetes/pkg/apis/abac/v1beta1"
"k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/auth/user"
"k8s.io/kubernetes/pkg/runtime"
)
func TestEmptyFile(t *testing.T) {
@ -56,7 +60,7 @@ func TestExampleFile(t *testing.T) {
}
}
func TestNotAuthorized(t *testing.T) {
func TestAuthorizeV0(t *testing.T) {
a, err := newWithContents(t, `{ "readonly": true, "resource": "events" }
{"user":"scheduler", "readonly": true, "resource": "pods" }
{"user":"scheduler", "resource": "bindings" }
@ -78,6 +82,102 @@ func TestNotAuthorized(t *testing.T) {
Verb string
Resource string
NS string
APIGroup string
Path string
ExpectAllow bool
}{
// Scheduler can read pods
{User: uScheduler, Verb: "list", Resource: "pods", NS: "ns1", ExpectAllow: true},
{User: uScheduler, Verb: "list", Resource: "pods", NS: "", ExpectAllow: true},
// Scheduler cannot write pods
{User: uScheduler, Verb: "create", Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uScheduler, Verb: "create", Resource: "pods", NS: "", ExpectAllow: false},
// Scheduler can write bindings
{User: uScheduler, Verb: "get", Resource: "bindings", NS: "ns1", ExpectAllow: true},
{User: uScheduler, Verb: "get", Resource: "bindings", NS: "", ExpectAllow: true},
// Alice can read and write anything in the right namespace.
{User: uAlice, Verb: "get", Resource: "pods", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "get", Resource: "widgets", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "get", Resource: "", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "update", Resource: "pods", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "update", Resource: "widgets", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "update", Resource: "", NS: "projectCaribou", ExpectAllow: true},
{User: uAlice, Verb: "update", Resource: "foo", NS: "projectCaribou", APIGroup: "bar", ExpectAllow: true},
// .. but not the wrong namespace.
{User: uAlice, Verb: "get", Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uAlice, Verb: "get", Resource: "widgets", NS: "ns1", ExpectAllow: false},
{User: uAlice, Verb: "get", Resource: "", NS: "ns1", ExpectAllow: false},
// Chuck can read events, since anyone can.
{User: uChuck, Verb: "get", Resource: "events", NS: "ns1", ExpectAllow: true},
{User: uChuck, Verb: "get", Resource: "events", NS: "", ExpectAllow: true},
// Chuck can't do other things.
{User: uChuck, Verb: "update", Resource: "events", NS: "ns1", ExpectAllow: false},
{User: uChuck, Verb: "get", Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uChuck, Verb: "get", Resource: "floop", NS: "ns1", ExpectAllow: false},
// Chunk can't access things with no kind or namespace
{User: uChuck, Verb: "get", Path: "/", Resource: "", NS: "", ExpectAllow: false},
}
for i, tc := range testCases {
attr := authorizer.AttributesRecord{
User: &tc.User,
Verb: tc.Verb,
Resource: tc.Resource,
Namespace: tc.NS,
APIGroup: tc.APIGroup,
Path: tc.Path,
ResourceRequest: len(tc.NS) > 0 || len(tc.Resource) > 0,
}
err := a.Authorize(attr)
actualAllow := bool(err == nil)
if tc.ExpectAllow != actualAllow {
t.Logf("tc: %v -> attr %v", tc, attr)
t.Errorf("%d: Expected allowed=%v but actually allowed=%v\n\t%v",
i, tc.ExpectAllow, actualAllow, tc)
}
}
}
func TestAuthorizeV1beta1(t *testing.T) {
a, err := newWithContents(t,
`
# Comment line, after a blank line
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"*", "readonly": true, "nonResourcePath": "/api"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"*", "nonResourcePath": "/custom"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"*", "nonResourcePath": "/root/*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"noresource", "nonResourcePath": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"*", "readonly": true, "resource": "events", "namespace": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"scheduler", "readonly": true, "resource": "pods", "namespace": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"scheduler", "resource": "bindings", "namespace": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"kubelet", "readonly": true, "resource": "bindings", "namespace": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"kubelet", "resource": "events", "namespace": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"alice", "resource": "*", "namespace": "projectCaribou"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"bob", "readonly": true, "resource": "*", "namespace": "projectCaribou"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"debbie", "resource": "pods", "namespace": "projectCaribou"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"apigroupuser", "resource": "*", "namespace": "projectAnyGroup", "apiGroup": "*"}}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"apigroupuser", "resource": "*", "namespace": "projectEmptyGroup", "apiGroup": "" }}
{"apiVersion":"abac.authorization.kubernetes.io/v1beta1","kind":"Policy","spec":{"user":"apigroupuser", "resource": "*", "namespace": "projectXGroup", "apiGroup": "x"}}`)
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"}
uDebbie := user.DefaultInfo{Name: "debbie", UID: "uid6"}
uNoResource := user.DefaultInfo{Name: "noresource", UID: "uid7"}
uAPIGroup := user.DefaultInfo{Name: "apigroupuser", UID: "uid8"}
testCases := []struct {
User user.DefaultInfo
Verb string
Resource string
APIGroup string
NS string
Path string
ExpectAllow bool
}{
// Scheduler can read pods
@ -102,6 +202,9 @@ func TestNotAuthorized(t *testing.T) {
{User: uAlice, Verb: "get", Resource: "widgets", NS: "ns1", ExpectAllow: false},
{User: uAlice, Verb: "get", Resource: "", NS: "ns1", ExpectAllow: false},
// Debbie can write to pods in the right namespace
{User: uDebbie, Verb: "update", Resource: "pods", NS: "projectCaribou", ExpectAllow: true},
// Chuck can read events, since anyone can.
{User: uChuck, Verb: "get", Resource: "events", NS: "ns1", ExpectAllow: true},
{User: uChuck, Verb: "get", Resource: "events", NS: "", ExpectAllow: true},
@ -109,24 +212,49 @@ func TestNotAuthorized(t *testing.T) {
{User: uChuck, Verb: "update", Resource: "events", NS: "ns1", ExpectAllow: false},
{User: uChuck, Verb: "get", Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uChuck, Verb: "get", Resource: "floop", NS: "ns1", ExpectAllow: false},
// Chunk can't access things with no kind or namespace
// TODO: find a way to give someone access to miscellaneous endpoints, such as
// /healthz, /version, etc.
{User: uChuck, Verb: "get", Resource: "", NS: "", ExpectAllow: false},
// Chuck can't access things with no resource or namespace
{User: uChuck, Verb: "get", Path: "/", Resource: "", NS: "", ExpectAllow: false},
// but can access /api
{User: uChuck, Verb: "get", Path: "/api", Resource: "", NS: "", ExpectAllow: true},
// though he cannot write to it
{User: uChuck, Verb: "create", Path: "/api", Resource: "", NS: "", ExpectAllow: false},
// while he can write to /custom
{User: uChuck, Verb: "update", Path: "/custom", Resource: "", NS: "", ExpectAllow: true},
// he cannot get "/root"
{User: uChuck, Verb: "get", Path: "/root", Resource: "", NS: "", ExpectAllow: false},
// but can get any subpath
{User: uChuck, Verb: "get", Path: "/root/", Resource: "", NS: "", ExpectAllow: true},
{User: uChuck, Verb: "get", Path: "/root/test/1/2/3", Resource: "", NS: "", ExpectAllow: true},
// the user "noresource" can get any non-resource request
{User: uNoResource, Verb: "get", Path: "", Resource: "", NS: "", ExpectAllow: true},
{User: uNoResource, Verb: "get", Path: "/", Resource: "", NS: "", ExpectAllow: true},
{User: uNoResource, Verb: "get", Path: "/foo/bar/baz", Resource: "", NS: "", ExpectAllow: true},
// but cannot get any request where IsResourceRequest() == true
{User: uNoResource, Verb: "get", Path: "/", Resource: "", NS: "bar", ExpectAllow: false},
{User: uNoResource, Verb: "get", Path: "/foo/bar/baz", Resource: "foo", NS: "bar", ExpectAllow: false},
// Test APIGroup matching
{User: uAPIGroup, Verb: "get", APIGroup: "x", Resource: "foo", NS: "projectAnyGroup", ExpectAllow: true},
{User: uAPIGroup, Verb: "get", APIGroup: "x", Resource: "foo", NS: "projectEmptyGroup", ExpectAllow: false},
{User: uAPIGroup, Verb: "get", APIGroup: "x", Resource: "foo", NS: "projectXGroup", ExpectAllow: true},
}
for i, tc := range testCases {
attr := authorizer.AttributesRecord{
User: &tc.User,
Verb: tc.Verb,
Resource: tc.Resource,
Namespace: tc.NS,
User: &tc.User,
Verb: tc.Verb,
Resource: tc.Resource,
APIGroup: tc.APIGroup,
Namespace: tc.NS,
ResourceRequest: len(tc.NS) > 0 || len(tc.Resource) > 0,
Path: tc.Path,
}
t.Logf("tc: %v -> attr %v", tc, attr)
// t.Logf("tc %2v: %v -> attr %v", i, tc, attr)
err := a.Authorize(attr)
actualAllow := bool(err == nil)
if tc.ExpectAllow != actualAllow {
t.Errorf("%d: Expected allowed=%v but actually allowed=%v\n\t%v",
i, tc.ExpectAllow, actualAllow, tc)
t.Errorf("%d: Expected allowed=%v but actually allowed=%v, for case %+v & %+v",
i, tc.ExpectAllow, actualAllow, tc, attr)
}
}
}
@ -134,116 +262,316 @@ func TestNotAuthorized(t *testing.T) {
func TestSubjectMatches(t *testing.T) {
testCases := map[string]struct {
User user.DefaultInfo
PolicyUser string
PolicyGroup string
Policy runtime.Object
ExpectMatch bool
}{
"empty policy matches unauthed user": {
User: user.DefaultInfo{},
PolicyUser: "",
PolicyGroup: "",
"v0 empty policy matches unauthed user": {
User: user.DefaultInfo{},
Policy: &v0.Policy{
User: "",
Group: "",
},
ExpectMatch: true,
},
"empty policy matches authed user": {
User: user.DefaultInfo{Name: "Foo"},
PolicyUser: "",
PolicyGroup: "",
"v0 empty policy matches authed user": {
User: user.DefaultInfo{Name: "Foo"},
Policy: &v0.Policy{
User: "",
Group: "",
},
ExpectMatch: true,
},
"empty policy matches authed user with groups": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"a", "b"}},
PolicyUser: "",
PolicyGroup: "",
"v0 empty policy matches authed user with groups": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"a", "b"}},
Policy: &v0.Policy{
User: "",
Group: "",
},
ExpectMatch: true,
},
"user policy does not match unauthed user": {
User: user.DefaultInfo{},
PolicyUser: "Foo",
PolicyGroup: "",
"v0 user policy does not match unauthed user": {
User: user.DefaultInfo{},
Policy: &v0.Policy{
User: "Foo",
Group: "",
},
ExpectMatch: false,
},
"user policy does not match different user": {
User: user.DefaultInfo{Name: "Bar"},
PolicyUser: "Foo",
PolicyGroup: "",
"v0 user policy does not match different user": {
User: user.DefaultInfo{Name: "Bar"},
Policy: &v0.Policy{
User: "Foo",
Group: "",
},
ExpectMatch: false,
},
"user policy is case-sensitive": {
User: user.DefaultInfo{Name: "foo"},
PolicyUser: "Foo",
PolicyGroup: "",
"v0 user policy is case-sensitive": {
User: user.DefaultInfo{Name: "foo"},
Policy: &v0.Policy{
User: "Foo",
Group: "",
},
ExpectMatch: false,
},
"user policy does not match substring": {
User: user.DefaultInfo{Name: "FooBar"},
PolicyUser: "Foo",
PolicyGroup: "",
"v0 user policy does not match substring": {
User: user.DefaultInfo{Name: "FooBar"},
Policy: &v0.Policy{
User: "Foo",
Group: "",
},
ExpectMatch: false,
},
"user policy matches username": {
User: user.DefaultInfo{Name: "Foo"},
PolicyUser: "Foo",
PolicyGroup: "",
"v0 user policy matches username": {
User: user.DefaultInfo{Name: "Foo"},
Policy: &v0.Policy{
User: "Foo",
Group: "",
},
ExpectMatch: true,
},
"group policy does not match unauthed user": {
User: user.DefaultInfo{},
PolicyUser: "",
PolicyGroup: "Foo",
"v0 group policy does not match unauthed user": {
User: user.DefaultInfo{},
Policy: &v0.Policy{
User: "",
Group: "Foo",
},
ExpectMatch: false,
},
"group policy does not match user in different group": {
User: user.DefaultInfo{Name: "FooBar", Groups: []string{"B"}},
PolicyUser: "",
PolicyGroup: "A",
"v0 group policy does not match user in different group": {
User: user.DefaultInfo{Name: "FooBar", Groups: []string{"B"}},
Policy: &v0.Policy{
User: "",
Group: "A",
},
ExpectMatch: false,
},
"group policy is case-sensitive": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "",
PolicyGroup: "b",
"v0 group policy is case-sensitive": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v0.Policy{
User: "",
Group: "b",
},
ExpectMatch: false,
},
"group policy does not match substring": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "BBB", "C"}},
PolicyUser: "",
PolicyGroup: "B",
"v0 group policy does not match substring": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "BBB", "C"}},
Policy: &v0.Policy{
User: "",
Group: "B",
},
ExpectMatch: false,
},
"group policy matches user in group": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "",
PolicyGroup: "B",
"v0 group policy matches user in group": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v0.Policy{
User: "",
Group: "B",
},
ExpectMatch: true,
},
"user and group policy requires user match": {
User: user.DefaultInfo{Name: "Bar", Groups: []string{"A", "B", "C"}},
PolicyUser: "Foo",
PolicyGroup: "B",
"v0 user and group policy requires user match": {
User: user.DefaultInfo{Name: "Bar", Groups: []string{"A", "B", "C"}},
Policy: &v0.Policy{
User: "Foo",
Group: "B",
},
ExpectMatch: false,
},
"user and group policy requires group match": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "Foo",
PolicyGroup: "D",
"v0 user and group policy requires group match": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v0.Policy{
User: "Foo",
Group: "D",
},
ExpectMatch: false,
},
"user and group policy matches": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "Foo",
PolicyGroup: "B",
"v0 user and group policy matches": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v0.Policy{
User: "Foo",
Group: "B",
},
ExpectMatch: true,
},
"v1 empty policy does not match unauthed user": {
User: user.DefaultInfo{},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "",
},
},
ExpectMatch: false,
},
"v1 empty policy does not match authed user": {
User: user.DefaultInfo{Name: "Foo"},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "",
},
},
ExpectMatch: false,
},
"v1 empty policy does not match authed user with groups": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"a", "b"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "",
},
},
ExpectMatch: false,
},
"v1 user policy does not match unauthed user": {
User: user.DefaultInfo{},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "",
},
},
ExpectMatch: false,
},
"v1 user policy does not match different user": {
User: user.DefaultInfo{Name: "Bar"},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "",
},
},
ExpectMatch: false,
},
"v1 user policy is case-sensitive": {
User: user.DefaultInfo{Name: "foo"},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "",
},
},
ExpectMatch: false,
},
"v1 user policy does not match substring": {
User: user.DefaultInfo{Name: "FooBar"},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "",
},
},
ExpectMatch: false,
},
"v1 user policy matches username": {
User: user.DefaultInfo{Name: "Foo"},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "",
},
},
ExpectMatch: true,
},
"v1 group policy does not match unauthed user": {
User: user.DefaultInfo{},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "Foo",
},
},
ExpectMatch: false,
},
"v1 group policy does not match user in different group": {
User: user.DefaultInfo{Name: "FooBar", Groups: []string{"B"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "A",
},
},
ExpectMatch: false,
},
"v1 group policy is case-sensitive": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "b",
},
},
ExpectMatch: false,
},
"v1 group policy does not match substring": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "BBB", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "B",
},
},
ExpectMatch: false,
},
"v1 group policy matches user in group": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "",
Group: "B",
},
},
ExpectMatch: true,
},
"v1 user and group policy requires user match": {
User: user.DefaultInfo{Name: "Bar", Groups: []string{"A", "B", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "B",
},
},
ExpectMatch: false,
},
"v1 user and group policy requires group match": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "D",
},
},
ExpectMatch: false,
},
"v1 user and group policy matches": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
Policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "Foo",
Group: "B",
},
},
ExpectMatch: true,
},
}
for k, tc := range testCases {
policy := &api.Policy{}
if err := api.Scheme.Convert(tc.Policy, policy); err != nil {
t.Errorf("%s: error converting: %v", k, err)
continue
}
attr := authorizer.AttributesRecord{
User: &tc.User,
}
actualMatch := policy{User: tc.PolicyUser, Group: tc.PolicyGroup}.subjectMatches(attr)
actualMatch := subjectMatches(*policy, attr)
if tc.ExpectMatch != actualMatch {
t.Errorf("%v: Expected actorMatches=%v but actually got=%v",
k, tc.ExpectMatch, actualMatch)
@ -269,27 +597,30 @@ func newWithContents(t *testing.T, contents string) (authorizer.Authorizer, erro
func TestPolicy(t *testing.T) {
tests := []struct {
policy policy
policy runtime.Object
attr authorizer.Attributes
matches bool
name string
}{
// v0
{
policy: policy{},
policy: &v0.Policy{},
attr: authorizer.AttributesRecord{},
matches: true,
name: "null",
name: "v0 null",
},
// v0 mismatches
{
policy: policy{
policy: &v0.Policy{
Readonly: true,
},
attr: authorizer.AttributesRecord{},
matches: false,
name: "read-only mismatch",
name: "v0 read-only mismatch",
},
{
policy: policy{
policy: &v0.Policy{
User: "foo",
},
attr: authorizer.AttributesRecord{
@ -298,20 +629,21 @@ func TestPolicy(t *testing.T) {
},
},
matches: false,
name: "user name mis-match",
name: "v0 user name mis-match",
},
{
policy: policy{
policy: &v0.Policy{
Resource: "foo",
},
attr: authorizer.AttributesRecord{
Resource: "bar",
Resource: "bar",
ResourceRequest: true,
},
matches: false,
name: "resource mis-match",
name: "v0 resource mis-match",
},
{
policy: policy{
policy: &v0.Policy{
User: "foo",
Resource: "foo",
Namespace: "foo",
@ -320,27 +652,314 @@ func TestPolicy(t *testing.T) {
User: &user.DefaultInfo{
Name: "foo",
},
Resource: "foo",
Namespace: "foo",
Resource: "foo",
Namespace: "foo",
ResourceRequest: true,
},
matches: true,
name: "namespace mis-match",
name: "v0 namespace mis-match",
},
// v0 matches
{
policy: &v0.Policy{},
attr: authorizer.AttributesRecord{ResourceRequest: true},
matches: true,
name: "v0 null resource",
},
{
policy: policy{
Namespace: "foo",
policy: &v0.Policy{
Readonly: true,
},
attr: authorizer.AttributesRecord{
Namespace: "bar",
Verb: "get",
},
matches: true,
name: "v0 read-only match",
},
{
policy: &v0.Policy{
User: "foo",
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
},
},
matches: true,
name: "v0 user name match",
},
{
policy: &v0.Policy{
Resource: "foo",
},
attr: authorizer.AttributesRecord{
Resource: "foo",
ResourceRequest: true,
},
matches: true,
name: "v0 resource match",
},
// v1 mismatches
{
policy: &v1beta1.Policy{},
attr: authorizer.AttributesRecord{
ResourceRequest: true,
},
matches: false,
name: "resource mis-match",
name: "v1 null",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "foo",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "bar",
},
ResourceRequest: true,
},
matches: false,
name: "v1 user name mis-match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
Readonly: true,
},
},
attr: authorizer.AttributesRecord{
ResourceRequest: true,
},
matches: false,
name: "v1 read-only mismatch",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
Resource: "foo",
},
},
attr: authorizer.AttributesRecord{
Resource: "bar",
ResourceRequest: true,
},
matches: false,
name: "v1 resource mis-match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "foo",
Namespace: "barr",
Resource: "baz",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
},
Namespace: "bar",
Resource: "baz",
ResourceRequest: true,
},
matches: false,
name: "v1 namespace mis-match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
NonResourcePath: "/api",
},
},
attr: authorizer.AttributesRecord{
Path: "/api2",
ResourceRequest: false,
},
matches: false,
name: "v1 non-resource mis-match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
NonResourcePath: "/api/*",
},
},
attr: authorizer.AttributesRecord{
Path: "/api2/foo",
ResourceRequest: false,
},
matches: false,
name: "v1 non-resource wildcard subpath mis-match",
},
// v1 matches
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "foo",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
},
ResourceRequest: true,
},
matches: true,
name: "v1 user match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
},
},
attr: authorizer.AttributesRecord{
ResourceRequest: true,
},
matches: true,
name: "v1 user wildcard match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
Group: "bar",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
Groups: []string{"bar"},
},
ResourceRequest: true,
},
matches: true,
name: "v1 group match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
Group: "*",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
Groups: []string{"bar"},
},
ResourceRequest: true,
},
matches: true,
name: "v1 group wildcard match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
Readonly: true,
},
},
attr: authorizer.AttributesRecord{
Verb: "get",
ResourceRequest: true,
},
matches: true,
name: "v1 read-only match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
Resource: "foo",
},
},
attr: authorizer.AttributesRecord{
Resource: "foo",
ResourceRequest: true,
},
matches: true,
name: "v1 resource match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "foo",
Namespace: "bar",
Resource: "baz",
},
},
attr: authorizer.AttributesRecord{
User: &user.DefaultInfo{
Name: "foo",
},
Namespace: "bar",
Resource: "baz",
ResourceRequest: true,
},
matches: true,
name: "v1 namespace match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
NonResourcePath: "/api",
},
},
attr: authorizer.AttributesRecord{
Path: "/api",
ResourceRequest: false,
},
matches: true,
name: "v1 non-resource match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
NonResourcePath: "*",
},
},
attr: authorizer.AttributesRecord{
Path: "/api",
ResourceRequest: false,
},
matches: true,
name: "v1 non-resource wildcard match",
},
{
policy: &v1beta1.Policy{
Spec: v1beta1.PolicySpec{
User: "*",
NonResourcePath: "/api/*",
},
},
attr: authorizer.AttributesRecord{
Path: "/api/foo",
ResourceRequest: false,
},
matches: true,
name: "v1 non-resource wildcard subpath match",
},
}
for _, test := range tests {
matches := test.policy.matches(test.attr)
policy := &api.Policy{}
if err := api.Scheme.Convert(test.policy, policy); err != nil {
t.Errorf("%s: error converting: %v", test.name, err)
continue
}
matches := matches(*policy, test.attr)
if test.matches != matches {
t.Errorf("unexpected value for %s, expected: %t, saw: %t", test.name, test.matches, matches)
t.Errorf("%s: expected: %t, saw: %t", test.name, test.matches, matches)
continue
}
}
}

View File

@ -1,9 +1,10 @@
{"user":"admin"}
{"user":"scheduler", "readonly": true, "resource": "pods"}
{"user":"scheduler", "resource": "bindings"}
{"user":"kubelet", "readonly": true, "resource": "pods"}
{"user":"kubelet", "readonly": true, "resource": "services"}
{"user":"kubelet", "readonly": true, "resource": "endpoints"}
{"user":"kubelet", "resource": "events"}
{"user":"alice", "namespace": "projectCaribou"}
{"user":"bob", "readonly": true, "namespace": "projectCaribou"}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"*", "nonResourcePath": "*", "readonly": true}
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"admin", "namespace": "*", "resource": "*", "apiGroup": "*" }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"scheduler", "namespace": "*", "resource": "pods", "readonly": true }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"scheduler", "namespace": "*", "resource": "bindings" }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "pods", "readonly": true }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "services", "readonly": true }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "endpoints", "readonly": true }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "events" }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"alice", "namespace": "projectCaribou", "resource": "*", "apiGroup": "*" }
{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"bob", "namespace": "projectCaribou", "resource": "*", "apiGroup": "*", "readonly": true }

View File

@ -50,6 +50,13 @@ type Attributes interface {
// The group of the resource, if a request is for a REST object.
GetAPIGroup() string
// IsResourceRequest returns true for requests to API resources, like /api/v1/nodes,
// and false for non-resource endpoints like /api, /healthz, and /swaggerapi
IsResourceRequest() bool
// GetPath returns the path of the request
GetPath() string
}
// Authorizer makes an authorization decision based on information gained by making
@ -72,11 +79,13 @@ type RequestAttributesGetter interface {
// AttributesRecord implements Attributes interface.
type AttributesRecord struct {
User user.Info
Verb string
Namespace string
APIGroup string
Resource string
User user.Info
Verb string
Namespace string
APIGroup string
Resource string
ResourceRequest bool
Path string
}
func (a AttributesRecord) GetUserName() string {
@ -106,3 +115,11 @@ func (a AttributesRecord) GetResource() string {
func (a AttributesRecord) GetAPIGroup() string {
return a.APIGroup
}
func (a AttributesRecord) IsResourceRequest() bool {
return a.ResourceRequest
}
func (a AttributesRecord) GetPath() string {
return a.Path
}