Commit b256b070 authored by Kubernetes Submit Queue's avatar Kubernetes Submit Queue Committed by GitHub

Merge pull request #32407 from deads2k/authz-01-lsar

Automatic merge from submit-queue add local subject access review API Adds a local subject access review endpoint to allow a project-admin (someone with full rights within a namespace) the power to inspect whether a person can perform an action in his namespace. This is a separate resource be factoring like this ensures that it is impossible for him to look outside his namespace and makes it possible to create authorization rules that can restrict this power to a project-admin in his own namespace. Other factorings require introspection of objects. @kubernetes/sig-auth
parents e357b9a6 e5dbfdac
...@@ -9,6 +9,59 @@ ...@@ -9,6 +9,59 @@
}, },
"apis": [ "apis": [
{ {
"path": "/apis/authorization.k8s.io/v1beta1/namespaces/{namespace}/localsubjectaccessreviews",
"description": "API at /apis/authorization.k8s.io/v1beta1",
"operations": [
{
"type": "v1beta1.LocalSubjectAccessReview",
"method": "POST",
"summary": "create a LocalSubjectAccessReview",
"nickname": "createNamespacedLocalSubjectAccessReview",
"parameters": [
{
"type": "string",
"paramType": "query",
"name": "pretty",
"description": "If 'true', then the output is pretty printed.",
"required": false,
"allowMultiple": false
},
{
"type": "v1beta1.LocalSubjectAccessReview",
"paramType": "body",
"name": "body",
"description": "",
"required": true,
"allowMultiple": false
},
{
"type": "string",
"paramType": "path",
"name": "namespace",
"description": "object name and auth scope, such as for teams and projects",
"required": true,
"allowMultiple": false
}
],
"responseMessages": [
{
"code": 200,
"message": "OK",
"responseModel": "v1beta1.LocalSubjectAccessReview"
}
],
"produces": [
"application/json",
"application/yaml",
"application/vnd.kubernetes.protobuf"
],
"consumes": [
"*/*"
]
}
]
},
{
"path": "/apis/authorization.k8s.io/v1beta1/selfsubjectaccessreviews", "path": "/apis/authorization.k8s.io/v1beta1/selfsubjectaccessreviews",
"description": "API at /apis/authorization.k8s.io/v1beta1", "description": "API at /apis/authorization.k8s.io/v1beta1",
"operations": [ "operations": [
...@@ -123,9 +176,9 @@ ...@@ -123,9 +176,9 @@
} }
], ],
"models": { "models": {
"v1beta1.SelfSubjectAccessReview": { "v1beta1.LocalSubjectAccessReview": {
"id": "v1beta1.SelfSubjectAccessReview", "id": "v1beta1.LocalSubjectAccessReview",
"description": "SelfSubjectAccessReview checks whether or the current user can perform an action. Not filling in a spec.namespace means \"in all namespaces\". Self is a special case, because users should always be able to check whether they can perform an action", "description": "LocalSubjectAccessReview checks whether or not a user or group can perform an action in a given namespace. Having a namespace scoped resource makes it much easier to grant namespace scoped policy that includes permissions checking.",
"required": [ "required": [
"spec" "spec"
], ],
...@@ -142,8 +195,8 @@ ...@@ -142,8 +195,8 @@
"$ref": "v1.ObjectMeta" "$ref": "v1.ObjectMeta"
}, },
"spec": { "spec": {
"$ref": "v1beta1.SelfSubjectAccessReviewSpec", "$ref": "v1beta1.SubjectAccessReviewSpec",
"description": "Spec holds information about the request being evaluated. user and groups must be empty" "description": "Spec holds information about the request being evaluated. spec.namespace must be equal to the namespace you made the request against. If empty, it is defaulted."
}, },
"status": { "status": {
"$ref": "v1beta1.SubjectAccessReviewStatus", "$ref": "v1beta1.SubjectAccessReviewStatus",
...@@ -259,9 +312,9 @@ ...@@ -259,9 +312,9 @@
} }
} }
}, },
"v1beta1.SelfSubjectAccessReviewSpec": { "v1beta1.SubjectAccessReviewSpec": {
"id": "v1beta1.SelfSubjectAccessReviewSpec", "id": "v1beta1.SubjectAccessReviewSpec",
"description": "SelfSubjectAccessReviewSpec is a description of the access request. Exactly one of ResourceAuthorizationAttributes and NonResourceAuthorizationAttributes must be set", "description": "SubjectAccessReviewSpec is a description of the access request. Exactly one of ResourceAuthorizationAttributes and NonResourceAuthorizationAttributes must be set",
"properties": { "properties": {
"resourceAttributes": { "resourceAttributes": {
"$ref": "v1beta1.ResourceAttributes", "$ref": "v1beta1.ResourceAttributes",
...@@ -270,6 +323,21 @@ ...@@ -270,6 +323,21 @@
"nonResourceAttributes": { "nonResourceAttributes": {
"$ref": "v1beta1.NonResourceAttributes", "$ref": "v1beta1.NonResourceAttributes",
"description": "NonResourceAttributes describes information for a non-resource access request" "description": "NonResourceAttributes describes information for a non-resource access request"
},
"user": {
"type": "string",
"description": "User is the user you're testing for. If you specify \"User\" but not \"Group\", then is it interpreted as \"What if User were not a member of any groups"
},
"group": {
"type": "array",
"items": {
"type": "string"
},
"description": "Groups is the groups you're testing for."
},
"extra": {
"type": "object",
"description": "Extra corresponds to the user.Info.GetExtra() method from the authenticator. Since that is input to the authorizer it needs a reflection here."
} }
} }
}, },
...@@ -342,9 +410,9 @@ ...@@ -342,9 +410,9 @@
} }
} }
}, },
"v1beta1.SubjectAccessReview": { "v1beta1.SelfSubjectAccessReview": {
"id": "v1beta1.SubjectAccessReview", "id": "v1beta1.SelfSubjectAccessReview",
"description": "SubjectAccessReview checks whether or not a user or group can perform an action.", "description": "SelfSubjectAccessReview checks whether or the current user can perform an action. Not filling in a spec.namespace means \"in all namespaces\". Self is a special case, because users should always be able to check whether they can perform an action",
"required": [ "required": [
"spec" "spec"
], ],
...@@ -361,8 +429,8 @@ ...@@ -361,8 +429,8 @@
"$ref": "v1.ObjectMeta" "$ref": "v1.ObjectMeta"
}, },
"spec": { "spec": {
"$ref": "v1beta1.SubjectAccessReviewSpec", "$ref": "v1beta1.SelfSubjectAccessReviewSpec",
"description": "Spec holds information about the request being evaluated" "description": "Spec holds information about the request being evaluated. user and groups must be empty"
}, },
"status": { "status": {
"$ref": "v1beta1.SubjectAccessReviewStatus", "$ref": "v1beta1.SubjectAccessReviewStatus",
...@@ -370,9 +438,9 @@ ...@@ -370,9 +438,9 @@
} }
} }
}, },
"v1beta1.SubjectAccessReviewSpec": { "v1beta1.SelfSubjectAccessReviewSpec": {
"id": "v1beta1.SubjectAccessReviewSpec", "id": "v1beta1.SelfSubjectAccessReviewSpec",
"description": "SubjectAccessReviewSpec is a description of the access request. Exactly one of ResourceAuthorizationAttributes and NonResourceAuthorizationAttributes must be set", "description": "SelfSubjectAccessReviewSpec is a description of the access request. Exactly one of ResourceAuthorizationAttributes and NonResourceAuthorizationAttributes must be set",
"properties": { "properties": {
"resourceAttributes": { "resourceAttributes": {
"$ref": "v1beta1.ResourceAttributes", "$ref": "v1beta1.ResourceAttributes",
...@@ -381,21 +449,34 @@ ...@@ -381,21 +449,34 @@
"nonResourceAttributes": { "nonResourceAttributes": {
"$ref": "v1beta1.NonResourceAttributes", "$ref": "v1beta1.NonResourceAttributes",
"description": "NonResourceAttributes describes information for a non-resource access request" "description": "NonResourceAttributes describes information for a non-resource access request"
}
}
},
"v1beta1.SubjectAccessReview": {
"id": "v1beta1.SubjectAccessReview",
"description": "SubjectAccessReview checks whether or not a user or group can perform an action.",
"required": [
"spec"
],
"properties": {
"kind": {
"type": "string",
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#types-kinds"
}, },
"user": { "apiVersion": {
"type": "string", "type": "string",
"description": "User is the user you're testing for. If you specify \"User\" but not \"Group\", then is it interpreted as \"What if User were not a member of any groups" "description": "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#resources"
}, },
"group": { "metadata": {
"type": "array", "$ref": "v1.ObjectMeta"
"items": {
"type": "string"
},
"description": "Groups is the groups you're testing for."
}, },
"extra": { "spec": {
"type": "object", "$ref": "v1beta1.SubjectAccessReviewSpec",
"description": "Extra corresponds to the user.Info.GetExtra() method from the authenticator. Since that is input to the authorizer it needs a reflection here." "description": "Spec holds information about the request being evaluated"
},
"status": {
"$ref": "v1beta1.SubjectAccessReviewStatus",
"description": "Status is filled in by the server and indicates whether the request is allowed or not"
} }
} }
}, },
......
...@@ -56,6 +56,9 @@ type SelfSubjectAccessReview struct { ...@@ -56,6 +56,9 @@ type SelfSubjectAccessReview struct {
Status SubjectAccessReviewStatus Status SubjectAccessReviewStatus
} }
// +genclient=true
// +noMethods=true
// LocalSubjectAccessReview checks whether or not a user or group can perform an action in a given namespace. // LocalSubjectAccessReview checks whether or not a user or group can perform an action in a given namespace.
// Having a namespace scoped resource makes it much easier to grant namespace scoped policy that includes permissions // Having a namespace scoped resource makes it much easier to grant namespace scoped policy that includes permissions
// checking. // checking.
......
...@@ -57,6 +57,9 @@ type SelfSubjectAccessReview struct { ...@@ -57,6 +57,9 @@ type SelfSubjectAccessReview struct {
Status SubjectAccessReviewStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` Status SubjectAccessReviewStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"`
} }
// +genclient=true
// +noMethods=true
// LocalSubjectAccessReview checks whether or not a user or group can perform an action in a given namespace. // LocalSubjectAccessReview checks whether or not a user or group can perform an action in a given namespace.
// Having a namespace scoped resource makes it much easier to grant namespace scoped policy that includes permissions // Having a namespace scoped resource makes it much easier to grant namespace scoped policy that includes permissions
// checking. // checking.
......
...@@ -67,8 +67,19 @@ func ValidateSelfSubjectAccessReview(sar *authorizationapi.SelfSubjectAccessRevi ...@@ -67,8 +67,19 @@ func ValidateSelfSubjectAccessReview(sar *authorizationapi.SelfSubjectAccessRevi
func ValidateLocalSubjectAccessReview(sar *authorizationapi.LocalSubjectAccessReview) field.ErrorList { func ValidateLocalSubjectAccessReview(sar *authorizationapi.LocalSubjectAccessReview) field.ErrorList {
allErrs := ValidateSubjectAccessReviewSpec(sar.Spec, field.NewPath("spec")) allErrs := ValidateSubjectAccessReviewSpec(sar.Spec, field.NewPath("spec"))
if !api.Semantic.DeepEqual(api.ObjectMeta{}, sar.ObjectMeta) {
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata"), sar.ObjectMeta, `must be empty`)) objectMetaShallowCopy := sar.ObjectMeta
objectMetaShallowCopy.Namespace = ""
if !api.Semantic.DeepEqual(api.ObjectMeta{}, objectMetaShallowCopy) {
allErrs = append(allErrs, field.Invalid(field.NewPath("metadata"), sar.ObjectMeta, `must be empty except for namespace`))
} }
if sar.Spec.ResourceAttributes != nil && sar.Spec.ResourceAttributes.Namespace != sar.Namespace {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.resourceAttributes.namespace"), sar.Spec.ResourceAttributes.Namespace, `must match metadata.namespace`))
}
if sar.Spec.NonResourceAttributes != nil {
allErrs = append(allErrs, field.Invalid(field.NewPath("spec.nonResourceAttributes"), sar.Spec.NonResourceAttributes, `disallowed on this kind of request`))
}
return allErrs return allErrs
} }
...@@ -20,6 +20,7 @@ import ( ...@@ -20,6 +20,7 @@ import (
"strings" "strings"
"testing" "testing"
"k8s.io/kubernetes/pkg/api"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
"k8s.io/kubernetes/pkg/util/validation/field" "k8s.io/kubernetes/pkg/util/validation/field"
) )
...@@ -133,3 +134,68 @@ func TestValidateSelfSAR(t *testing.T) { ...@@ -133,3 +134,68 @@ func TestValidateSelfSAR(t *testing.T) {
} }
} }
} }
func TestValidateLocalSAR(t *testing.T) {
successCases := []authorizationapi.LocalSubjectAccessReview{
{
Spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{},
User: "user",
},
},
}
for _, successCase := range successCases {
if errs := ValidateLocalSubjectAccessReview(&successCase); len(errs) != 0 {
t.Errorf("expected success: %v", errs)
}
}
errorCases := []struct {
name string
obj *authorizationapi.LocalSubjectAccessReview
msg string
}{
{
name: "name",
obj: &authorizationapi.LocalSubjectAccessReview{
ObjectMeta: api.ObjectMeta{Name: "a"},
Spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{},
User: "user",
},
},
msg: "must be empty except for namespace",
},
{
name: "namespace conflict",
obj: &authorizationapi.LocalSubjectAccessReview{
ObjectMeta: api.ObjectMeta{Namespace: "a"},
Spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{},
User: "user",
},
},
msg: "must match metadata.namespace",
},
{
name: "nonresource",
obj: &authorizationapi.LocalSubjectAccessReview{
ObjectMeta: api.ObjectMeta{Namespace: "a"},
Spec: authorizationapi.SubjectAccessReviewSpec{
NonResourceAttributes: &authorizationapi.NonResourceAttributes{},
User: "user",
},
},
msg: "disallowed on this kind of request",
},
}
for _, c := range errorCases {
errs := ValidateLocalSubjectAccessReview(c.obj)
if len(errs) == 0 {
t.Errorf("%s: expected failure for %q", c.name, c.msg)
} else if !strings.Contains(errs[0].Error(), c.msg) {
t.Errorf("%s: unexpected error: %q, expected: %q", c.name, errs[0], c.msg)
}
}
}
...@@ -24,6 +24,7 @@ import ( ...@@ -24,6 +24,7 @@ import (
type AuthorizationInterface interface { type AuthorizationInterface interface {
GetRESTClient() *restclient.RESTClient GetRESTClient() *restclient.RESTClient
LocalSubjectAccessReviewsGetter
SelfSubjectAccessReviewsGetter SelfSubjectAccessReviewsGetter
SubjectAccessReviewsGetter SubjectAccessReviewsGetter
} }
...@@ -33,6 +34,10 @@ type AuthorizationClient struct { ...@@ -33,6 +34,10 @@ type AuthorizationClient struct {
*restclient.RESTClient *restclient.RESTClient
} }
func (c *AuthorizationClient) LocalSubjectAccessReviews(namespace string) LocalSubjectAccessReviewInterface {
return newLocalSubjectAccessReviews(c, namespace)
}
func (c *AuthorizationClient) SelfSubjectAccessReviews() SelfSubjectAccessReviewInterface { func (c *AuthorizationClient) SelfSubjectAccessReviews() SelfSubjectAccessReviewInterface {
return newSelfSubjectAccessReviews(c) return newSelfSubjectAccessReviews(c)
} }
......
...@@ -26,6 +26,10 @@ type FakeAuthorization struct { ...@@ -26,6 +26,10 @@ type FakeAuthorization struct {
*core.Fake *core.Fake
} }
func (c *FakeAuthorization) LocalSubjectAccessReviews(namespace string) unversioned.LocalSubjectAccessReviewInterface {
return &FakeLocalSubjectAccessReviews{c, namespace}
}
func (c *FakeAuthorization) SelfSubjectAccessReviews() unversioned.SelfSubjectAccessReviewInterface { func (c *FakeAuthorization) SelfSubjectAccessReviews() unversioned.SelfSubjectAccessReviewInterface {
return &FakeSelfSubjectAccessReviews{c} return &FakeSelfSubjectAccessReviews{c}
} }
......
...@@ -31,3 +31,8 @@ func (c *FakeSelfSubjectAccessReviews) Create(sar *authorizationapi.SelfSubjectA ...@@ -31,3 +31,8 @@ func (c *FakeSelfSubjectAccessReviews) Create(sar *authorizationapi.SelfSubjectA
obj, err := c.Fake.Invokes(core.NewRootCreateAction(authorizationapi.SchemeGroupVersion.WithResource("selfsubjectaccessreviews"), sar), &authorizationapi.SelfSubjectAccessReview{}) obj, err := c.Fake.Invokes(core.NewRootCreateAction(authorizationapi.SchemeGroupVersion.WithResource("selfsubjectaccessreviews"), sar), &authorizationapi.SelfSubjectAccessReview{})
return obj.(*authorizationapi.SelfSubjectAccessReview), err return obj.(*authorizationapi.SelfSubjectAccessReview), err
} }
func (c *FakeLocalSubjectAccessReviews) Create(sar *authorizationapi.LocalSubjectAccessReview) (result *authorizationapi.LocalSubjectAccessReview, err error) {
obj, err := c.Fake.Invokes(core.NewCreateAction(authorizationapi.SchemeGroupVersion.WithResource("localsubjectaccessreviews"), c.ns, sar), &authorizationapi.SubjectAccessReview{})
return obj.(*authorizationapi.LocalSubjectAccessReview), err
}
/*
Copyright 2016 The Kubernetes Authors.
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 fake
// FakeLocalSubjectAccessReviews implements LocalSubjectAccessReviewInterface
type FakeLocalSubjectAccessReviews struct {
Fake *FakeAuthorization
ns string
}
/*
Copyright 2016 The Kubernetes Authors.
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 unversioned
// LocalSubjectAccessReviewsGetter has a method to return a LocalSubjectAccessReviewInterface.
// A group's client should implement this interface.
type LocalSubjectAccessReviewsGetter interface {
LocalSubjectAccessReviews(namespace string) LocalSubjectAccessReviewInterface
}
// LocalSubjectAccessReviewInterface has methods to work with LocalSubjectAccessReview resources.
type LocalSubjectAccessReviewInterface interface {
LocalSubjectAccessReviewExpansion
}
// localSubjectAccessReviews implements LocalSubjectAccessReviewInterface
type localSubjectAccessReviews struct {
client *AuthorizationClient
ns string
}
// newLocalSubjectAccessReviews returns a LocalSubjectAccessReviews
func newLocalSubjectAccessReviews(c *AuthorizationClient, namespace string) *localSubjectAccessReviews {
return &localSubjectAccessReviews{
client: c,
ns: namespace,
}
}
/*
Copyright 2016 The Kubernetes Authors.
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 unversioned
import (
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
)
type LocalSubjectAccessReviewExpansion interface {
Create(sar *authorizationapi.LocalSubjectAccessReview) (result *authorizationapi.LocalSubjectAccessReview, err error)
}
func (c *localSubjectAccessReviews) Create(sar *authorizationapi.LocalSubjectAccessReview) (result *authorizationapi.LocalSubjectAccessReview, err error) {
result = &authorizationapi.LocalSubjectAccessReview{}
err = c.client.Post().
Namespace(c.ns).
Resource("localsubjectaccessreviews").
Body(sar).
Do().
Into(result)
return
}
...@@ -25,6 +25,7 @@ import ( ...@@ -25,6 +25,7 @@ import (
type AuthorizationInterface interface { type AuthorizationInterface interface {
GetRESTClient() *restclient.RESTClient GetRESTClient() *restclient.RESTClient
LocalSubjectAccessReviewsGetter
SelfSubjectAccessReviewsGetter SelfSubjectAccessReviewsGetter
SubjectAccessReviewsGetter SubjectAccessReviewsGetter
} }
...@@ -34,6 +35,10 @@ type AuthorizationClient struct { ...@@ -34,6 +35,10 @@ type AuthorizationClient struct {
*restclient.RESTClient *restclient.RESTClient
} }
func (c *AuthorizationClient) LocalSubjectAccessReviews(namespace string) LocalSubjectAccessReviewInterface {
return newLocalSubjectAccessReviews(c, namespace)
}
func (c *AuthorizationClient) SelfSubjectAccessReviews() SelfSubjectAccessReviewInterface { func (c *AuthorizationClient) SelfSubjectAccessReviews() SelfSubjectAccessReviewInterface {
return newSelfSubjectAccessReviews(c) return newSelfSubjectAccessReviews(c)
} }
......
...@@ -26,6 +26,10 @@ type FakeAuthorization struct { ...@@ -26,6 +26,10 @@ type FakeAuthorization struct {
*core.Fake *core.Fake
} }
func (c *FakeAuthorization) LocalSubjectAccessReviews(namespace string) v1beta1.LocalSubjectAccessReviewInterface {
return &FakeLocalSubjectAccessReviews{c, namespace}
}
func (c *FakeAuthorization) SelfSubjectAccessReviews() v1beta1.SelfSubjectAccessReviewInterface { func (c *FakeAuthorization) SelfSubjectAccessReviews() v1beta1.SelfSubjectAccessReviewInterface {
return &FakeSelfSubjectAccessReviews{c} return &FakeSelfSubjectAccessReviews{c}
} }
......
/*
Copyright 2016 The Kubernetes Authors.
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 fake
// FakeLocalSubjectAccessReviews implements LocalSubjectAccessReviewInterface
type FakeLocalSubjectAccessReviews struct {
Fake *FakeAuthorization
ns string
}
/*
Copyright 2016 The Kubernetes Authors.
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 fake
import (
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization/v1beta1"
"k8s.io/kubernetes/pkg/client/testing/core"
)
func (c *FakeLocalSubjectAccessReviews) Create(sar *authorizationapi.LocalSubjectAccessReview) (result *authorizationapi.LocalSubjectAccessReview, err error) {
obj, err := c.Fake.Invokes(core.NewCreateAction(authorizationapi.SchemeGroupVersion.WithResource("localsubjectaccessreviews"), c.ns, sar), &authorizationapi.SubjectAccessReview{})
return obj.(*authorizationapi.LocalSubjectAccessReview), err
}
/*
Copyright 2016 The Kubernetes Authors.
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
// LocalSubjectAccessReviewsGetter has a method to return a LocalSubjectAccessReviewInterface.
// A group's client should implement this interface.
type LocalSubjectAccessReviewsGetter interface {
LocalSubjectAccessReviews(namespace string) LocalSubjectAccessReviewInterface
}
// LocalSubjectAccessReviewInterface has methods to work with LocalSubjectAccessReview resources.
type LocalSubjectAccessReviewInterface interface {
LocalSubjectAccessReviewExpansion
}
// localSubjectAccessReviews implements LocalSubjectAccessReviewInterface
type localSubjectAccessReviews struct {
client *AuthorizationClient
ns string
}
// newLocalSubjectAccessReviews returns a LocalSubjectAccessReviews
func newLocalSubjectAccessReviews(c *AuthorizationClient, namespace string) *localSubjectAccessReviews {
return &localSubjectAccessReviews{
client: c,
ns: namespace,
}
}
/*
Copyright 2016 The Kubernetes Authors.
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 (
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization/v1beta1"
)
type LocalSubjectAccessReviewExpansion interface {
Create(sar *authorizationapi.LocalSubjectAccessReview) (result *authorizationapi.LocalSubjectAccessReview, err error)
}
func (c *localSubjectAccessReviews) Create(sar *authorizationapi.LocalSubjectAccessReview) (result *authorizationapi.LocalSubjectAccessReview, err error) {
result = &authorizationapi.LocalSubjectAccessReview{}
err = c.client.Post().
Namespace(c.ns).
Resource("localsubjectaccessreviews").
Body(sar).
Do().
Into(result)
return
}
...@@ -525,13 +525,14 @@ func (gc *GarbageCollector) monitorFor(resource unversioned.GroupVersionResource ...@@ -525,13 +525,14 @@ func (gc *GarbageCollector) monitorFor(resource unversioned.GroupVersionResource
} }
var ignoredResources = map[unversioned.GroupVersionResource]struct{}{ var ignoredResources = map[unversioned.GroupVersionResource]struct{}{
unversioned.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "replicationcontrollers"}: {}, unversioned.GroupVersionResource{Group: "extensions", Version: "v1beta1", Resource: "replicationcontrollers"}: {},
unversioned.GroupVersionResource{Group: "", Version: "v1", Resource: "bindings"}: {}, unversioned.GroupVersionResource{Group: "", Version: "v1", Resource: "bindings"}: {},
unversioned.GroupVersionResource{Group: "", Version: "v1", Resource: "componentstatuses"}: {}, unversioned.GroupVersionResource{Group: "", Version: "v1", Resource: "componentstatuses"}: {},
unversioned.GroupVersionResource{Group: "", Version: "v1", Resource: "events"}: {}, unversioned.GroupVersionResource{Group: "", Version: "v1", Resource: "events"}: {},
unversioned.GroupVersionResource{Group: "authentication.k8s.io", Version: "v1beta1", Resource: "tokenreviews"}: {}, unversioned.GroupVersionResource{Group: "authentication.k8s.io", Version: "v1beta1", Resource: "tokenreviews"}: {},
unversioned.GroupVersionResource{Group: "authorization.k8s.io", Version: "v1beta1", Resource: "subjectaccessreviews"}: {}, unversioned.GroupVersionResource{Group: "authorization.k8s.io", Version: "v1beta1", Resource: "subjectaccessreviews"}: {},
unversioned.GroupVersionResource{Group: "authorization.k8s.io", Version: "v1beta1", Resource: "selfsubjectaccessreviews"}: {}, unversioned.GroupVersionResource{Group: "authorization.k8s.io", Version: "v1beta1", Resource: "selfsubjectaccessreviews"}: {},
unversioned.GroupVersionResource{Group: "authorization.k8s.io", Version: "v1beta1", Resource: "localsubjectaccessreviews"}: {},
} }
func NewGarbageCollector(metaOnlyClientPool dynamic.ClientPool, clientPool dynamic.ClientPool, resources []unversioned.GroupVersionResource) (*GarbageCollector, error) { func NewGarbageCollector(metaOnlyClientPool dynamic.ClientPool, clientPool dynamic.ClientPool, resources []unversioned.GroupVersionResource) (*GarbageCollector, error) {
......
...@@ -22,6 +22,7 @@ import ( ...@@ -22,6 +22,7 @@ import (
authorizationv1beta1 "k8s.io/kubernetes/pkg/apis/authorization/v1beta1" authorizationv1beta1 "k8s.io/kubernetes/pkg/apis/authorization/v1beta1"
"k8s.io/kubernetes/pkg/auth/authorizer" "k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/genericapiserver" "k8s.io/kubernetes/pkg/genericapiserver"
"k8s.io/kubernetes/pkg/registry/authorization/localsubjectaccessreview"
"k8s.io/kubernetes/pkg/registry/authorization/selfsubjectaccessreview" "k8s.io/kubernetes/pkg/registry/authorization/selfsubjectaccessreview"
"k8s.io/kubernetes/pkg/registry/authorization/subjectaccessreview" "k8s.io/kubernetes/pkg/registry/authorization/subjectaccessreview"
) )
...@@ -57,6 +58,9 @@ func (p AuthorizationRESTStorageProvider) v1beta1Storage(apiResourceConfigSource ...@@ -57,6 +58,9 @@ func (p AuthorizationRESTStorageProvider) v1beta1Storage(apiResourceConfigSource
if apiResourceConfigSource.ResourceEnabled(version.WithResource("selfsubjectaccessreviews")) { if apiResourceConfigSource.ResourceEnabled(version.WithResource("selfsubjectaccessreviews")) {
storage["selfsubjectaccessreviews"] = selfsubjectaccessreview.NewREST(p.Authorizer) storage["selfsubjectaccessreviews"] = selfsubjectaccessreview.NewREST(p.Authorizer)
} }
if apiResourceConfigSource.ResourceEnabled(version.WithResource("localsubjectaccessreviews")) {
storage["localsubjectaccessreviews"] = localsubjectaccessreview.NewREST(p.Authorizer)
}
return storage return storage
} }
/*
Copyright 2016 The Kubernetes Authors.
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 localsubjectaccessreview
import (
"fmt"
kapi "k8s.io/kubernetes/pkg/api"
kapierrors "k8s.io/kubernetes/pkg/api/errors"
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation"
"k8s.io/kubernetes/pkg/auth/authorizer"
authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util"
"k8s.io/kubernetes/pkg/runtime"
)
type REST struct {
authorizer authorizer.Authorizer
}
func NewREST(authorizer authorizer.Authorizer) *REST {
return &REST{authorizer}
}
func (r *REST) New() runtime.Object {
return &authorizationapi.LocalSubjectAccessReview{}
}
func (r *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, error) {
localSubjectAccessReview, ok := obj.(*authorizationapi.LocalSubjectAccessReview)
if !ok {
return nil, kapierrors.NewBadRequest(fmt.Sprintf("not a LocaLocalSubjectAccessReview: %#v", obj))
}
if errs := authorizationvalidation.ValidateLocalSubjectAccessReview(localSubjectAccessReview); len(errs) > 0 {
return nil, kapierrors.NewInvalid(authorizationapi.Kind(localSubjectAccessReview.Kind), "", errs)
}
namespace := kapi.NamespaceValue(ctx)
if len(namespace) == 0 {
return nil, kapierrors.NewBadRequest(fmt.Sprintf("namespace is required on this type: %v", namespace))
}
if namespace != localSubjectAccessReview.Namespace {
return nil, kapierrors.NewBadRequest(fmt.Sprintf("spec.resourceAttributes.namespace must match namespace: %v", namespace))
}
authorizationAttributes := authorizationutil.AuthorizationAttributesFrom(localSubjectAccessReview.Spec)
allowed, reason, evaluationErr := r.authorizer.Authorize(authorizationAttributes)
localSubjectAccessReview.Status = authorizationapi.SubjectAccessReviewStatus{
Allowed: allowed,
Reason: reason,
}
if evaluationErr != nil {
localSubjectAccessReview.Status.EvaluationError = evaluationErr.Error()
}
return localSubjectAccessReview, nil
}
...@@ -24,7 +24,6 @@ import ( ...@@ -24,7 +24,6 @@ import (
authorizationapi "k8s.io/kubernetes/pkg/apis/authorization" authorizationapi "k8s.io/kubernetes/pkg/apis/authorization"
authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation" authorizationvalidation "k8s.io/kubernetes/pkg/apis/authorization/validation"
"k8s.io/kubernetes/pkg/auth/authorizer" "k8s.io/kubernetes/pkg/auth/authorizer"
"k8s.io/kubernetes/pkg/auth/user"
authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util" authorizationutil "k8s.io/kubernetes/pkg/registry/authorization/util"
"k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/runtime"
) )
...@@ -50,19 +49,7 @@ func (r *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, err ...@@ -50,19 +49,7 @@ func (r *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, err
return nil, kapierrors.NewInvalid(authorizationapi.Kind(subjectAccessReview.Kind), "", errs) return nil, kapierrors.NewInvalid(authorizationapi.Kind(subjectAccessReview.Kind), "", errs)
} }
userToCheck := &user.DefaultInfo{ authorizationAttributes := authorizationutil.AuthorizationAttributesFrom(subjectAccessReview.Spec)
Name: subjectAccessReview.Spec.User,
Groups: subjectAccessReview.Spec.Groups,
Extra: convertToUserInfoExtra(subjectAccessReview.Spec.Extra),
}
var authorizationAttributes authorizer.AttributesRecord
if subjectAccessReview.Spec.ResourceAttributes != nil {
authorizationAttributes = authorizationutil.ResourceAttributesFrom(userToCheck, *subjectAccessReview.Spec.ResourceAttributes)
} else {
authorizationAttributes = authorizationutil.NonResourceAttributesFrom(userToCheck, *subjectAccessReview.Spec.NonResourceAttributes)
}
allowed, reason, evaluationErr := r.authorizer.Authorize(authorizationAttributes) allowed, reason, evaluationErr := r.authorizer.Authorize(authorizationAttributes)
subjectAccessReview.Status = authorizationapi.SubjectAccessReviewStatus{ subjectAccessReview.Status = authorizationapi.SubjectAccessReviewStatus{
...@@ -75,15 +62,3 @@ func (r *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, err ...@@ -75,15 +62,3 @@ func (r *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, err
return subjectAccessReview, nil return subjectAccessReview, nil
} }
func convertToUserInfoExtra(extra map[string]authorizationapi.ExtraValue) map[string][]string {
if extra == nil {
return nil
}
ret := map[string][]string{}
for k, v := range extra {
ret[k] = []string(v)
}
return ret
}
...@@ -42,3 +42,33 @@ func NonResourceAttributesFrom(user user.Info, in authorizationapi.NonResourceAt ...@@ -42,3 +42,33 @@ func NonResourceAttributesFrom(user user.Info, in authorizationapi.NonResourceAt
Path: in.Path, Path: in.Path,
} }
} }
func convertToUserInfoExtra(extra map[string]authorizationapi.ExtraValue) map[string][]string {
if extra == nil {
return nil
}
ret := map[string][]string{}
for k, v := range extra {
ret[k] = []string(v)
}
return ret
}
// AuthorizationAttributesFrom takes a spec and returns the proper authz attributes to check it.
func AuthorizationAttributesFrom(spec authorizationapi.SubjectAccessReviewSpec) authorizer.AttributesRecord {
userToCheck := &user.DefaultInfo{
Name: spec.User,
Groups: spec.Groups,
Extra: convertToUserInfoExtra(spec.Extra),
}
var authorizationAttributes authorizer.AttributesRecord
if spec.ResourceAttributes != nil {
authorizationAttributes = ResourceAttributesFrom(userToCheck, *spec.ResourceAttributes)
} else {
authorizationAttributes = NonResourceAttributesFrom(userToCheck, *spec.NonResourceAttributes)
}
return authorizationAttributes
}
...@@ -245,3 +245,130 @@ func TestSelfSubjectAccessReview(t *testing.T) { ...@@ -245,3 +245,130 @@ func TestSelfSubjectAccessReview(t *testing.T) {
} }
} }
} }
func TestLocalSubjectAccessReview(t *testing.T) {
var m *master.Master
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
m.Handler.ServeHTTP(w, req)
}))
defer s.Close()
masterConfig := framework.NewIntegrationTestMasterConfig()
masterConfig.Authenticator = authenticator.RequestFunc(alwaysAlice)
masterConfig.Authorizer = sarAuthorizer{}
masterConfig.AdmissionControl = admit.NewAlwaysAdmit()
m, err := master.New(masterConfig)
if err != nil {
t.Fatalf("error in bringing up the master: %v", err)
}
clientset := clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL, ContentConfig: restclient.ContentConfig{GroupVersion: testapi.Default.GroupVersion()}})
tests := []struct {
name string
namespace string
sar *authorizationapi.LocalSubjectAccessReview
expectedError string
expectedStatus authorizationapi.SubjectAccessReviewStatus
}{
{
name: "simple allow",
namespace: "foo",
sar: &authorizationapi.LocalSubjectAccessReview{
ObjectMeta: api.ObjectMeta{Namespace: "foo"},
Spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
Verb: "list",
Group: api.GroupName,
Version: "v1",
Resource: "pods",
Namespace: "foo",
},
User: "alice",
},
},
expectedStatus: authorizationapi.SubjectAccessReviewStatus{
Allowed: true,
Reason: "you're not dave",
},
},
{
name: "simple deny",
namespace: "foo",
sar: &authorizationapi.LocalSubjectAccessReview{
ObjectMeta: api.ObjectMeta{Namespace: "foo"},
Spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
Verb: "list",
Group: api.GroupName,
Version: "v1",
Resource: "pods",
Namespace: "foo",
},
User: "dave",
},
},
expectedStatus: authorizationapi.SubjectAccessReviewStatus{
Allowed: false,
Reason: "no",
EvaluationError: "I'm sorry, Dave",
},
},
{
name: "conflicting namespace",
namespace: "foo",
sar: &authorizationapi.LocalSubjectAccessReview{
ObjectMeta: api.ObjectMeta{Namespace: "foo"},
Spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
Verb: "list",
Group: api.GroupName,
Version: "v1",
Resource: "pods",
Namespace: "bar",
},
User: "dave",
},
},
expectedError: "must match metadata.namespace",
},
{
name: "missing namespace",
namespace: "foo",
sar: &authorizationapi.LocalSubjectAccessReview{
ObjectMeta: api.ObjectMeta{Namespace: "foo"},
Spec: authorizationapi.SubjectAccessReviewSpec{
ResourceAttributes: &authorizationapi.ResourceAttributes{
Verb: "list",
Group: api.GroupName,
Version: "v1",
Resource: "pods",
},
User: "dave",
},
},
expectedError: "must match metadata.namespace",
},
}
for _, test := range tests {
response, err := clientset.Authorization().LocalSubjectAccessReviews(test.namespace).Create(test.sar)
switch {
case err == nil && len(test.expectedError) == 0:
case err != nil && strings.Contains(err.Error(), test.expectedError):
continue
case err != nil && len(test.expectedError) != 0:
t.Errorf("%s: unexpected error: %v", test.name, err)
continue
default:
t.Errorf("%s: expected %v, got %v", test.name, test.expectedError, err)
continue
}
if response.Status != test.expectedStatus {
t.Errorf("%s: expected %v, got %v", test.name, test.expectedStatus, response.Status)
continue
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment