mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-02 00:07:50 +00:00
Merge pull request #16148 from liggitt/mkulke-fix_kubectl_for_namespaced_users
Auto commit by PR queue bot
This commit is contained in:
commit
611770778f
@ -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.
|
||||||
|
24
pkg/apis/abac/latest/latest.go
Normal file
24
pkg/apis/abac/latest/latest.go
Normal 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
37
pkg/apis/abac/register.go
Normal 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
70
pkg/apis/abac/types.go
Normal 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.
|
||||||
|
|
||||||
|
}
|
58
pkg/apis/abac/v0/conversion.go
Normal file
58
pkg/apis/abac/v0/conversion.go
Normal 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
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
77
pkg/apis/abac/v0/conversion_test.go
Normal file
77
pkg/apis/abac/v0/conversion_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
37
pkg/apis/abac/v0/register.go
Normal file
37
pkg/apis/abac/v0/register.go
Normal 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
45
pkg/apis/abac/v0/types.go
Normal 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"`
|
||||||
|
}
|
37
pkg/apis/abac/v1beta1/register.go
Normal file
37
pkg/apis/abac/v1beta1/register.go
Normal 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() {}
|
60
pkg/apis/abac/v1beta1/types.go
Normal file
60
pkg/apis/abac/v1beta1/types.go
Normal 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"`
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
@ -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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user