mirror of https://github.com/k3s-io/k3s.git
Merge pull request #16148 from liggitt/mkulke-fix_kubectl_for_namespaced_users
Auto commit by PR queue botpull/6/head
commit
611770778f
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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() {}
|
|
@ -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.
|
||||
|
||||
}
|
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {}
|
|
@ -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"`
|
||||
}
|
|
@ -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() {}
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 }
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue