Merge pull request #16148 from liggitt/mkulke-fix_kubectl_for_namespaced_users

Auto commit by PR queue bot
This commit is contained in:
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 ### 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). - user (the user-string which a user was authenticated as).
- group (the list of group names the authenticated user is a member of). - group (the list of group names the authenticated user is a member of).
- whether the request is readonly (GETs are readonly). - whether the request is for an API resource.
- what resource is being accessed. - the request path.
- applies only to the API endpoints, such as - allows authorizing access to miscellaneous endpoints like `/api` or `/healthz` (see [kubectl](#kubectl)).
`/api/v1/namespaces/default/pods`. For miscellaneous endpoints, like `/version`, the - the request verb.
resource is the empty string. - API verbs like `get`, `list`, `create`, `update`, and `watch` are used for API requests
- the namespace of the object being access, or the empty string if the - HTTP verbs like `get`, `post`, and `put` are used for non-API requests
endpoint does not support namespaced objects. - 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 We anticipate adding more attributes to allow finer grained access control and
to assist in policy management. 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. one map per line.
Each line is a "policy object". A policy object is a map with the following properties: 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. - Versioning properties:
- `group`, type string; if you specify `group`, it must match one of the groups of the authenticated user. - `apiVersion`, type string; valid values are "abac.authorization.kubernetes.io/v1beta1". Allows versioning and conversion of the policy format.
- `readonly`, type boolean, when true, means that the policy only applies to GET - `kind`, type string: valid values are "Policy". Allows versioning and conversion of the policy format.
operations.
- `resource`, type string; a resource from an URL, such as `pods`. - `spec` property set to a map with the following properties:
- `namespace`, type string; a namespace string. - 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). 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. However, unset should be preferred for readability.
In the future, policies may be expressed in a JSON format, and managed via a REST In the future, policies may be expressed in a JSON format, and managed via a REST interface.
interface.
### Authorization Algorithm ### 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 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). 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 A property set to "*" will match any value of the corresponding attribute.
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. 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). 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 any user to do something, write a policy with the user property set to "*".
To permit an action Policy with an unset namespace applies regardless of namespace. 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 ### Examples
1. Alice can do anything: `{"user":"alice"}` 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: `{"user":"kubelet", "resource": "pods", "readonly": true}` 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: `{"user":"kubelet", "resource": "events"}` 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": `{"user":"bob", "resource": "pods", "readonly": true, "namespace": "projectCaribou"}` 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) [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: 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 ```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. 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 // Start with common attributes that apply to resource and non-resource requests
attribs.Verb = apiRequestInfo.Verb 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 // If a path follows the conventions of the REST object store, then
// we can extract the resource. Otherwise, not. // 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. // If the request specifies a namespace, then the namespace is filled in.
// Assumes there is no empty string namespace. Unspecified results // Assumes there is no empty string namespace. Unspecified results
// in empty (does not understand defaulting rules.) // in empty (does not understand defaulting rules.)
attribs.Namespace = apiRequestInfo.Namespace attribs.Namespace = requestInfo.Namespace
return &attribs return &attribs
} }

View File

@ -30,6 +30,8 @@ import (
"k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/errors" "k8s.io/kubernetes/pkg/api/errors"
"k8s.io/kubernetes/pkg/api/testapi" "k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/util/sets"
) )
type fakeRL bool 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) { func TestGetAPIRequestInfo(t *testing.T) {
successCases := []struct { successCases := []struct {
method string method string

View File

@ -21,50 +21,35 @@ package abac
import ( import (
"bufio" "bufio"
"encoding/json"
"errors" "errors"
"fmt"
"os" "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" "k8s.io/kubernetes/pkg/auth/authorizer"
) )
// TODO: make this into a real API object. Note that when that happens, it type policyLoadError struct {
// will get MetaData. However, the Kind and Namespace in the struct below path string
// will be separate from the Kind and Namespace in the Metadata. Obviously, line int
// meta.Kind will be something like policy, and policy.Kind has to be allowed data []byte
// to be different. Less obviously, namespace needs to be different as well. err error
// 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 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. // TODO: Have policies be created via an API call and stored in REST storage.
func NewFromFile(path string) (policyList, error) { func NewFromFile(path string) (policyList, error) {
@ -79,29 +64,151 @@ func NewFromFile(path string) (policyList, error) {
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
pl := make(policyList, 0) pl := make(policyList, 0)
i := 0
unversionedLines := 0
for scanner.Scan() { for scanner.Scan() {
var p policy i++
p := &api.Policy{}
b := scanner.Bytes() b := scanner.Bytes()
// TODO: skip comment lines.
err = json.Unmarshal(b, &p) // skip comment lines and blank lines
if err != nil { trimmed := strings.TrimSpace(string(b))
// TODO: line number in errors. if len(trimmed) == 0 || strings.HasPrefix(trimmed, "#") {
return nil, err 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) 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 { if err := scanner.Err(); err != nil {
return nil, err return nil, policyLoadError{path, -1, nil, err}
} }
return pl, nil return pl, nil
} }
func (p policy) matches(a authorizer.Attributes) bool { func matches(p api.Policy, a authorizer.Attributes) bool {
if p.subjectMatches(a) { if subjectMatches(p, a) {
if p.Readonly == false || (p.Readonly == a.IsReadOnly()) { if verbMatches(p, a) {
if p.Resource == "" || (p.Resource == a.GetResource()) { // Resource and non-resource requests are mutually exclusive, at most one will match a policy
if p.Namespace == "" || (p.Namespace == a.GetNamespace()) { 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 return true
} }
} }
@ -110,31 +217,10 @@ func (p policy) matches(a authorizer.Attributes) bool {
return false 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 // Authorizer implements authorizer.Authorize
func (pl policyList) Authorize(a authorizer.Attributes) error { func (pl policyList) Authorize(a authorizer.Attributes) error {
for _, p := range pl { for _, p := range pl {
if p.matches(a) { if matches(*p, a) {
return nil return nil
} }
} }

View File

@ -21,8 +21,12 @@ import (
"os" "os"
"testing" "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/authorizer"
"k8s.io/kubernetes/pkg/auth/user" "k8s.io/kubernetes/pkg/auth/user"
"k8s.io/kubernetes/pkg/runtime"
) )
func TestEmptyFile(t *testing.T) { 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" } a, err := newWithContents(t, `{ "readonly": true, "resource": "events" }
{"user":"scheduler", "readonly": true, "resource": "pods" } {"user":"scheduler", "readonly": true, "resource": "pods" }
{"user":"scheduler", "resource": "bindings" } {"user":"scheduler", "resource": "bindings" }
@ -78,6 +82,102 @@ func TestNotAuthorized(t *testing.T) {
Verb string Verb string
Resource string Resource string
NS 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 ExpectAllow bool
}{ }{
// Scheduler can read pods // 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: "widgets", NS: "ns1", ExpectAllow: false},
{User: uAlice, Verb: "get", Resource: "", 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. // Chuck can read events, since anyone can.
{User: uChuck, Verb: "get", Resource: "events", NS: "ns1", ExpectAllow: true}, {User: uChuck, Verb: "get", Resource: "events", NS: "ns1", ExpectAllow: true},
{User: uChuck, Verb: "get", Resource: "events", NS: "", 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: "update", Resource: "events", NS: "ns1", ExpectAllow: false},
{User: uChuck, Verb: "get", Resource: "pods", NS: "ns1", ExpectAllow: false}, {User: uChuck, Verb: "get", Resource: "pods", NS: "ns1", ExpectAllow: false},
{User: uChuck, Verb: "get", Resource: "floop", NS: "ns1", ExpectAllow: false}, {User: uChuck, Verb: "get", Resource: "floop", NS: "ns1", ExpectAllow: false},
// Chunk can't access things with no kind or namespace // Chuck can't access things with no resource or namespace
// TODO: find a way to give someone access to miscellaneous endpoints, such as {User: uChuck, Verb: "get", Path: "/", Resource: "", NS: "", ExpectAllow: false},
// /healthz, /version, etc. // but can access /api
{User: uChuck, Verb: "get", Resource: "", NS: "", ExpectAllow: false}, {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 { for i, tc := range testCases {
attr := authorizer.AttributesRecord{ attr := authorizer.AttributesRecord{
User: &tc.User, User: &tc.User,
Verb: tc.Verb, Verb: tc.Verb,
Resource: tc.Resource, Resource: tc.Resource,
Namespace: tc.NS, 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) err := a.Authorize(attr)
actualAllow := bool(err == nil) actualAllow := bool(err == nil)
if tc.ExpectAllow != actualAllow { if tc.ExpectAllow != actualAllow {
t.Errorf("%d: Expected allowed=%v but actually allowed=%v\n\t%v", t.Errorf("%d: Expected allowed=%v but actually allowed=%v, for case %+v & %+v",
i, tc.ExpectAllow, actualAllow, tc) i, tc.ExpectAllow, actualAllow, tc, attr)
} }
} }
} }
@ -134,116 +262,316 @@ func TestNotAuthorized(t *testing.T) {
func TestSubjectMatches(t *testing.T) { func TestSubjectMatches(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
User user.DefaultInfo User user.DefaultInfo
PolicyUser string Policy runtime.Object
PolicyGroup string
ExpectMatch bool ExpectMatch bool
}{ }{
"empty policy matches unauthed user": { "v0 empty policy matches unauthed user": {
User: user.DefaultInfo{}, User: user.DefaultInfo{},
PolicyUser: "", Policy: &v0.Policy{
PolicyGroup: "", User: "",
Group: "",
},
ExpectMatch: true, ExpectMatch: true,
}, },
"empty policy matches authed user": { "v0 empty policy matches authed user": {
User: user.DefaultInfo{Name: "Foo"}, User: user.DefaultInfo{Name: "Foo"},
PolicyUser: "", Policy: &v0.Policy{
PolicyGroup: "", User: "",
Group: "",
},
ExpectMatch: true, ExpectMatch: true,
}, },
"empty policy matches authed user with groups": { "v0 empty policy matches authed user with groups": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"a", "b"}}, User: user.DefaultInfo{Name: "Foo", Groups: []string{"a", "b"}},
PolicyUser: "", Policy: &v0.Policy{
PolicyGroup: "", User: "",
Group: "",
},
ExpectMatch: true, ExpectMatch: true,
}, },
"user policy does not match unauthed user": { "v0 user policy does not match unauthed user": {
User: user.DefaultInfo{}, User: user.DefaultInfo{},
PolicyUser: "Foo", Policy: &v0.Policy{
PolicyGroup: "", User: "Foo",
Group: "",
},
ExpectMatch: false, ExpectMatch: false,
}, },
"user policy does not match different user": { "v0 user policy does not match different user": {
User: user.DefaultInfo{Name: "Bar"}, User: user.DefaultInfo{Name: "Bar"},
PolicyUser: "Foo", Policy: &v0.Policy{
PolicyGroup: "", User: "Foo",
Group: "",
},
ExpectMatch: false, ExpectMatch: false,
}, },
"user policy is case-sensitive": { "v0 user policy is case-sensitive": {
User: user.DefaultInfo{Name: "foo"}, User: user.DefaultInfo{Name: "foo"},
PolicyUser: "Foo", Policy: &v0.Policy{
PolicyGroup: "", User: "Foo",
Group: "",
},
ExpectMatch: false, ExpectMatch: false,
}, },
"user policy does not match substring": { "v0 user policy does not match substring": {
User: user.DefaultInfo{Name: "FooBar"}, User: user.DefaultInfo{Name: "FooBar"},
PolicyUser: "Foo", Policy: &v0.Policy{
PolicyGroup: "", User: "Foo",
Group: "",
},
ExpectMatch: false, ExpectMatch: false,
}, },
"user policy matches username": { "v0 user policy matches username": {
User: user.DefaultInfo{Name: "Foo"}, User: user.DefaultInfo{Name: "Foo"},
PolicyUser: "Foo", Policy: &v0.Policy{
PolicyGroup: "", User: "Foo",
Group: "",
},
ExpectMatch: true, ExpectMatch: true,
}, },
"group policy does not match unauthed user": { "v0 group policy does not match unauthed user": {
User: user.DefaultInfo{}, User: user.DefaultInfo{},
PolicyUser: "", Policy: &v0.Policy{
PolicyGroup: "Foo", User: "",
Group: "Foo",
},
ExpectMatch: false, ExpectMatch: false,
}, },
"group policy does not match user in different group": { "v0 group policy does not match user in different group": {
User: user.DefaultInfo{Name: "FooBar", Groups: []string{"B"}}, User: user.DefaultInfo{Name: "FooBar", Groups: []string{"B"}},
PolicyUser: "", Policy: &v0.Policy{
PolicyGroup: "A", User: "",
Group: "A",
},
ExpectMatch: false, ExpectMatch: false,
}, },
"group policy is case-sensitive": { "v0 group policy is case-sensitive": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}}, User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "", Policy: &v0.Policy{
PolicyGroup: "b", User: "",
Group: "b",
},
ExpectMatch: false, ExpectMatch: false,
}, },
"group policy does not match substring": { "v0 group policy does not match substring": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "BBB", "C"}}, User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "BBB", "C"}},
PolicyUser: "", Policy: &v0.Policy{
PolicyGroup: "B", User: "",
Group: "B",
},
ExpectMatch: false, ExpectMatch: false,
}, },
"group policy matches user in group": { "v0 group policy matches user in group": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}}, User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "", Policy: &v0.Policy{
PolicyGroup: "B", User: "",
Group: "B",
},
ExpectMatch: true, ExpectMatch: true,
}, },
"user and group policy requires user match": { "v0 user and group policy requires user match": {
User: user.DefaultInfo{Name: "Bar", Groups: []string{"A", "B", "C"}}, User: user.DefaultInfo{Name: "Bar", Groups: []string{"A", "B", "C"}},
PolicyUser: "Foo", Policy: &v0.Policy{
PolicyGroup: "B", User: "Foo",
Group: "B",
},
ExpectMatch: false, ExpectMatch: false,
}, },
"user and group policy requires group match": { "v0 user and group policy requires group match": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}}, User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "Foo", Policy: &v0.Policy{
PolicyGroup: "D", User: "Foo",
Group: "D",
},
ExpectMatch: false, ExpectMatch: false,
}, },
"user and group policy matches": { "v0 user and group policy matches": {
User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}}, User: user.DefaultInfo{Name: "Foo", Groups: []string{"A", "B", "C"}},
PolicyUser: "Foo", Policy: &v0.Policy{
PolicyGroup: "B", 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, ExpectMatch: true,
}, },
} }
for k, tc := range testCases { 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{ attr := authorizer.AttributesRecord{
User: &tc.User, User: &tc.User,
} }
actualMatch := policy{User: tc.PolicyUser, Group: tc.PolicyGroup}.subjectMatches(attr) actualMatch := subjectMatches(*policy, attr)
if tc.ExpectMatch != actualMatch { if tc.ExpectMatch != actualMatch {
t.Errorf("%v: Expected actorMatches=%v but actually got=%v", t.Errorf("%v: Expected actorMatches=%v but actually got=%v",
k, tc.ExpectMatch, actualMatch) k, tc.ExpectMatch, actualMatch)
@ -269,27 +597,30 @@ func newWithContents(t *testing.T, contents string) (authorizer.Authorizer, erro
func TestPolicy(t *testing.T) { func TestPolicy(t *testing.T) {
tests := []struct { tests := []struct {
policy policy policy runtime.Object
attr authorizer.Attributes attr authorizer.Attributes
matches bool matches bool
name string name string
}{ }{
// v0
{ {
policy: policy{}, policy: &v0.Policy{},
attr: authorizer.AttributesRecord{}, attr: authorizer.AttributesRecord{},
matches: true, matches: true,
name: "null", name: "v0 null",
}, },
// v0 mismatches
{ {
policy: policy{ policy: &v0.Policy{
Readonly: true, Readonly: true,
}, },
attr: authorizer.AttributesRecord{}, attr: authorizer.AttributesRecord{},
matches: false, matches: false,
name: "read-only mismatch", name: "v0 read-only mismatch",
}, },
{ {
policy: policy{ policy: &v0.Policy{
User: "foo", User: "foo",
}, },
attr: authorizer.AttributesRecord{ attr: authorizer.AttributesRecord{
@ -298,20 +629,21 @@ func TestPolicy(t *testing.T) {
}, },
}, },
matches: false, matches: false,
name: "user name mis-match", name: "v0 user name mis-match",
}, },
{ {
policy: policy{ policy: &v0.Policy{
Resource: "foo", Resource: "foo",
}, },
attr: authorizer.AttributesRecord{ attr: authorizer.AttributesRecord{
Resource: "bar", Resource: "bar",
ResourceRequest: true,
}, },
matches: false, matches: false,
name: "resource mis-match", name: "v0 resource mis-match",
}, },
{ {
policy: policy{ policy: &v0.Policy{
User: "foo", User: "foo",
Resource: "foo", Resource: "foo",
Namespace: "foo", Namespace: "foo",
@ -320,27 +652,314 @@ func TestPolicy(t *testing.T) {
User: &user.DefaultInfo{ User: &user.DefaultInfo{
Name: "foo", Name: "foo",
}, },
Resource: "foo", Resource: "foo",
Namespace: "foo", Namespace: "foo",
ResourceRequest: true,
}, },
matches: 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{ policy: &v0.Policy{
Namespace: "foo", Readonly: true,
}, },
attr: authorizer.AttributesRecord{ 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, 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 { 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 { 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"} {"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"*", "nonResourcePath": "*", "readonly": true}
{"user":"scheduler", "readonly": true, "resource": "pods"} {"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"admin", "namespace": "*", "resource": "*", "apiGroup": "*" }
{"user":"scheduler", "resource": "bindings"} {"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"scheduler", "namespace": "*", "resource": "pods", "readonly": true }
{"user":"kubelet", "readonly": true, "resource": "pods"} {"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"scheduler", "namespace": "*", "resource": "bindings" }
{"user":"kubelet", "readonly": true, "resource": "services"} {"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "pods", "readonly": true }
{"user":"kubelet", "readonly": true, "resource": "endpoints"} {"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "services", "readonly": true }
{"user":"kubelet", "resource": "events"} {"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "endpoints", "readonly": true }
{"user":"alice", "namespace": "projectCaribou"} {"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "user":"kubelet", "namespace": "*", "resource": "events" }
{"user":"bob", "readonly": true, "namespace": "projectCaribou"} {"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. // The group of the resource, if a request is for a REST object.
GetAPIGroup() string 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 // Authorizer makes an authorization decision based on information gained by making
@ -72,11 +79,13 @@ type RequestAttributesGetter interface {
// AttributesRecord implements Attributes interface. // AttributesRecord implements Attributes interface.
type AttributesRecord struct { type AttributesRecord struct {
User user.Info User user.Info
Verb string Verb string
Namespace string Namespace string
APIGroup string APIGroup string
Resource string Resource string
ResourceRequest bool
Path string
} }
func (a AttributesRecord) GetUserName() string { func (a AttributesRecord) GetUserName() string {
@ -106,3 +115,11 @@ func (a AttributesRecord) GetResource() string {
func (a AttributesRecord) GetAPIGroup() string { func (a AttributesRecord) GetAPIGroup() string {
return a.APIGroup return a.APIGroup
} }
func (a AttributesRecord) IsResourceRequest() bool {
return a.ResourceRequest
}
func (a AttributesRecord) GetPath() string {
return a.Path
}