mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 19:56:01 +00:00
Add Authz check to validate policy and binding.
Co-authored-by: Jiahui Feng <jhf@google.com> Co-authored-by: Jordan Liggitt <liggitt@google.com>
This commit is contained in:
parent
c8a089de46
commit
d3f48136d0
@ -388,6 +388,14 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientset, err := kubernetes.NewForConfig(c.GenericConfig.LoopbackClientConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: update to a version that caches success but will recheck on failure, unlike memcache discovery
|
||||
discoveryClientForAdmissionRegistration := clientset.Discovery()
|
||||
|
||||
// The order here is preserved in discovery.
|
||||
// If resources with identical names exist in more than one of these groups (e.g. "deployments.apps"" and "deployments.extensions"),
|
||||
// the order of this list determines which group an unqualified resource name (e.g. "deployments") should prefer.
|
||||
@ -414,7 +422,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
|
||||
// keep apps after extensions so legacy clients resolve the extensions versions of shared resource names.
|
||||
// See https://github.com/kubernetes/kubernetes/issues/42392
|
||||
appsrest.StorageProvider{},
|
||||
admissionregistrationrest.RESTStorageProvider{},
|
||||
admissionregistrationrest.RESTStorageProvider{Authorizer: c.GenericConfig.Authorization.Authorizer, DiscoveryClient: discoveryClientForAdmissionRegistration},
|
||||
eventsrest.RESTStorageProvider{TTL: c.ExtraConfig.EventTTL},
|
||||
}
|
||||
if err := m.InstallAPIs(c.ExtraConfig.APIResourceConfigSource, c.GenericConfig.RESTOptionsGetter, restStorageProviders...); err != nil {
|
||||
|
72
pkg/registry/admissionregistration/resolver/resolver.go
Normal file
72
pkg/registry/admissionregistration/resolver/resolver.go
Normal file
@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2022 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 resolver
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/discovery"
|
||||
)
|
||||
|
||||
type ResourceResolver interface {
|
||||
Resolve(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error)
|
||||
}
|
||||
|
||||
type discoveryResourceResolver struct {
|
||||
client discovery.DiscoveryInterface
|
||||
}
|
||||
|
||||
func (d *discoveryResourceResolver) Resolve(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||
gv := gvk.GroupVersion()
|
||||
// TODO: refactor this into an efficient gvk --> gvr resolver that remembers hits and re-resolves group/version info on misses
|
||||
resources, err := d.client.ServerResourcesForGroupVersion(gv.String())
|
||||
if err != nil {
|
||||
return schema.GroupVersionResource{}, err
|
||||
}
|
||||
for _, resource := range resources.APIResources {
|
||||
if resource.Kind != gvk.Kind {
|
||||
// ignore unrelated kinds
|
||||
continue
|
||||
}
|
||||
if strings.Contains(resource.Name, "/") {
|
||||
// ignore subresources
|
||||
continue
|
||||
}
|
||||
if resource.Group != "" && resource.Group != gvk.Group {
|
||||
// group didn't match
|
||||
continue
|
||||
}
|
||||
if resource.Version != "" && resource.Version != gvk.Version {
|
||||
// version didn't match
|
||||
continue
|
||||
}
|
||||
return gv.WithResource(resource.Name), nil
|
||||
}
|
||||
return schema.GroupVersionResource{}, &meta.NoKindMatchError{GroupKind: gvk.GroupKind(), SearchedVersions: []string{gvk.Version}}
|
||||
}
|
||||
|
||||
func NewDiscoveryResourceResolver(client discovery.DiscoveryInterface) (ResourceResolver, error) {
|
||||
return &discoveryResourceResolver{client: client}, nil
|
||||
}
|
||||
|
||||
type ResourceResolverFunc func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error)
|
||||
|
||||
func (f ResourceResolverFunc) Resolve(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||
return f(gvk)
|
||||
}
|
@ -19,19 +19,25 @@ package rest
|
||||
import (
|
||||
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
serverstorage "k8s.io/apiserver/pkg/server/storage"
|
||||
"k8s.io/client-go/discovery"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
mutatingwebhookconfigurationstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingwebhookconfiguration/storage"
|
||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||
validatingadmissionpolicystorage "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicy/storage"
|
||||
policybindingstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage"
|
||||
validatingwebhookconfigurationstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingwebhookconfiguration/storage"
|
||||
)
|
||||
|
||||
type RESTStorageProvider struct{}
|
||||
type RESTStorageProvider struct {
|
||||
Authorizer authorizer.Authorizer
|
||||
DiscoveryClient discovery.DiscoveryInterface
|
||||
}
|
||||
|
||||
func (p RESTStorageProvider) NewRESTStorage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (genericapiserver.APIGroupInfo, error) {
|
||||
apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(admissionregistration.GroupName, legacyscheme.Scheme, legacyscheme.ParameterCodec, legacyscheme.Codecs)
|
||||
@ -79,18 +85,27 @@ func (p RESTStorageProvider) v1Storage(apiResourceConfigSource serverstorage.API
|
||||
func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstorage.APIResourceConfigSource, restOptionsGetter generic.RESTOptionsGetter) (map[string]rest.Storage, error) {
|
||||
storage := map[string]rest.Storage{}
|
||||
|
||||
// use a simple wrapper so that initialization order won't cause a nil getter
|
||||
var policyGetter rest.Getter
|
||||
|
||||
r, err := resolver.NewDiscoveryResourceResolver(p.DiscoveryClient)
|
||||
if err != nil {
|
||||
return storage, err
|
||||
}
|
||||
|
||||
// validatingadmissionpolicies
|
||||
if resource := "validatingadmissionpolicies"; apiResourceConfigSource.ResourceEnabled(admissionregistrationv1alpha1.SchemeGroupVersion.WithResource(resource)) {
|
||||
policyStorage, err := validatingadmissionpolicystorage.NewREST(restOptionsGetter)
|
||||
policyStorage, err := validatingadmissionpolicystorage.NewREST(restOptionsGetter, p.Authorizer, r)
|
||||
if err != nil {
|
||||
return storage, err
|
||||
}
|
||||
policyGetter = policyStorage
|
||||
storage[resource] = policyStorage
|
||||
}
|
||||
|
||||
// validatingadmissionpolicybindings
|
||||
if resource := "validatingadmissionpolicybindings"; apiResourceConfigSource.ResourceEnabled(admissionregistrationv1alpha1.SchemeGroupVersion.WithResource(resource)) {
|
||||
policyBindingStorage, err := policybindingstorage.NewREST(restOptionsGetter)
|
||||
policyBindingStorage, err := policybindingstorage.NewREST(restOptionsGetter, p.Authorizer, &policybindingstorage.DefaultPolicyGetter{Getter: policyGetter}, r)
|
||||
if err != nil {
|
||||
return storage, err
|
||||
}
|
||||
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
Copyright 2022 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 validatingadmissionpolicy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
|
||||
)
|
||||
|
||||
func (v *validatingAdmissionPolicyStrategy) authorizeCreate(ctx context.Context, obj runtime.Object) error {
|
||||
policy := obj.(*admissionregistration.ValidatingAdmissionPolicy)
|
||||
if policy.Spec.ParamKind == nil {
|
||||
// no paramRef in new object
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.authorize(ctx, policy)
|
||||
}
|
||||
|
||||
func (v *validatingAdmissionPolicyStrategy) authorizeUpdate(ctx context.Context, obj, old runtime.Object) error {
|
||||
policy := obj.(*admissionregistration.ValidatingAdmissionPolicy)
|
||||
if policy.Spec.ParamKind == nil {
|
||||
// no paramRef in new object
|
||||
return nil
|
||||
}
|
||||
|
||||
oldPolicy := old.(*admissionregistration.ValidatingAdmissionPolicy)
|
||||
if oldPolicy.Spec.ParamKind != nil && *oldPolicy.Spec.ParamKind == *policy.Spec.ParamKind {
|
||||
// identical paramKind to old object
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.authorize(ctx, policy)
|
||||
}
|
||||
|
||||
func (v *validatingAdmissionPolicyStrategy) authorize(ctx context.Context, policy *admissionregistration.ValidatingAdmissionPolicy) error {
|
||||
if v.authorizer == nil || policy.Spec.ParamKind == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// for superuser, skip all checks
|
||||
if rbacregistry.EscalationAllowed(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
user, ok := genericapirequest.UserFrom(ctx)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot identify user to authorize read access to paramKind resources")
|
||||
}
|
||||
|
||||
paramKind := policy.Spec.ParamKind
|
||||
// default to requiring permissions on all group/version/resources
|
||||
resource, apiGroup, apiVersion := "*", "*", "*"
|
||||
if gv, err := schema.ParseGroupVersion(paramKind.APIVersion); err == nil {
|
||||
// we only need to authorize the parsed group/version
|
||||
apiGroup = gv.Group
|
||||
apiVersion = gv.Version
|
||||
if gvr, err := v.resourceResolver.Resolve(gv.WithKind(paramKind.Kind)); err == nil {
|
||||
// we only need to authorize the resolved resource
|
||||
resource = gvr.Resource
|
||||
}
|
||||
}
|
||||
|
||||
// require that the user can read (verb "get") the referred kind.
|
||||
attrs := authorizer.AttributesRecord{
|
||||
User: user,
|
||||
Verb: "get",
|
||||
ResourceRequest: true,
|
||||
Name: "*",
|
||||
Namespace: "*",
|
||||
APIGroup: apiGroup,
|
||||
APIVersion: apiVersion,
|
||||
Resource: resource,
|
||||
}
|
||||
|
||||
d, _, err := v.authorizer.Authorize(ctx, attrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d != authorizer.DecisionAllow {
|
||||
return fmt.Errorf(`user %v must have "get" permission on all objects of the referenced paramKind (kind=%s, apiVersion=%s)`, user, paramKind.Kind, paramKind.APIVersion)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
/*
|
||||
Copyright 2022 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 validatingadmissionpolicy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||
)
|
||||
|
||||
func TestAuthorization(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
userInfo user.Info
|
||||
auth AuthFunc
|
||||
resourceResolver resolver.ResourceResolverFunc
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "superuser",
|
||||
userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}},
|
||||
expectErr: false, // success despite always-denying authorizer
|
||||
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
return authorizer.DecisionDeny, "", nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authorized",
|
||||
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
||||
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
if a.GetResource() == "replicalimits" {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", nil
|
||||
},
|
||||
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||
return schema.GroupVersionResource{
|
||||
Group: "rules.example.com",
|
||||
Version: "v1",
|
||||
Resource: "replicalimits",
|
||||
}, nil
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "denied",
|
||||
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
||||
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
if a.GetResource() == "configmaps" {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", nil
|
||||
},
|
||||
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||
return schema.GroupVersionResource{
|
||||
Group: "rules.example.com",
|
||||
Version: "v1",
|
||||
Resource: "replicalimits",
|
||||
}, nil
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
strategy := NewStrategy(tc.auth, tc.resourceResolver)
|
||||
t.Run("create", func(t *testing.T) {
|
||||
ctx := request.WithUser(context.Background(), tc.userInfo)
|
||||
errs := strategy.Validate(ctx, validValidatingAdmissionPolicy())
|
||||
if len(errs) > 0 != tc.expectErr {
|
||||
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
|
||||
}
|
||||
})
|
||||
t.Run("update", func(t *testing.T) {
|
||||
ctx := request.WithUser(context.Background(), tc.userInfo)
|
||||
obj := validValidatingAdmissionPolicy()
|
||||
objWithUpdatedParamKind := obj.DeepCopy()
|
||||
objWithUpdatedParamKind.Spec.ParamKind.APIVersion += "1"
|
||||
errs := strategy.ValidateUpdate(ctx, obj, objWithUpdatedParamKind)
|
||||
if len(errs) > 0 != tc.expectErr {
|
||||
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type AuthFunc func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error)
|
||||
|
||||
func (f AuthFunc) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
return f(ctx, a)
|
||||
}
|
@ -18,6 +18,7 @@ package storage
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
@ -25,6 +26,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/printers"
|
||||
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
|
||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicy"
|
||||
)
|
||||
|
||||
@ -33,19 +35,23 @@ type REST struct {
|
||||
*genericregistry.Store
|
||||
}
|
||||
|
||||
var groupResource = admissionregistration.Resource("validatingadmissionpolicies")
|
||||
|
||||
// NewREST returns a RESTStorage object that will work against validatingAdmissionPolicy.
|
||||
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
|
||||
func NewREST(optsGetter generic.RESTOptionsGetter, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, error) {
|
||||
r := &REST{}
|
||||
strategy := validatingadmissionpolicy.NewStrategy(authorizer, resourceResolver)
|
||||
store := &genericregistry.Store{
|
||||
NewFunc: func() runtime.Object { return &admissionregistration.ValidatingAdmissionPolicy{} },
|
||||
NewListFunc: func() runtime.Object { return &admissionregistration.ValidatingAdmissionPolicyList{} },
|
||||
ObjectNameFunc: func(obj runtime.Object) (string, error) {
|
||||
return obj.(*admissionregistration.ValidatingAdmissionPolicy).Name, nil
|
||||
},
|
||||
DefaultQualifiedResource: admissionregistration.Resource("validatingadmissionpolicies"),
|
||||
DefaultQualifiedResource: groupResource,
|
||||
|
||||
CreateStrategy: validatingadmissionpolicy.Strategy,
|
||||
UpdateStrategy: validatingadmissionpolicy.Strategy,
|
||||
DeleteStrategy: validatingadmissionpolicy.Strategy,
|
||||
CreateStrategy: strategy,
|
||||
UpdateStrategy: strategy,
|
||||
DeleteStrategy: strategy,
|
||||
|
||||
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
||||
}
|
||||
@ -53,7 +59,8 @@ func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
|
||||
if err := store.CompleteWithOptions(options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &REST{store}, nil
|
||||
r.Store = store
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Implement CategoriesProvider
|
||||
|
@ -23,10 +23,12 @@ import (
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||
|
||||
// Ensure that admissionregistration package is initialized.
|
||||
@ -34,7 +36,7 @@ import (
|
||||
)
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -48,7 +50,7 @@ func TestCreate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -72,7 +74,7 @@ func TestUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -80,7 +82,7 @@ func TestGet(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -88,7 +90,7 @@ func TestList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -96,7 +98,7 @@ func TestDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -198,14 +200,18 @@ func newValidatingAdmissionPolicy(name string) *admissionregistration.Validating
|
||||
}
|
||||
}
|
||||
|
||||
func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||
func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||
return newStorage(t, nil, nil)
|
||||
}
|
||||
|
||||
func newStorage(t *testing.T, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) {
|
||||
etcdStorage, server := registrytest.NewEtcdStorageForResource(t, admissionregistration.Resource("validatingadmissionpolicies"))
|
||||
restOptions := generic.RESTOptions{
|
||||
StorageConfig: etcdStorage,
|
||||
Decorator: generic.UndecoratedStorage,
|
||||
DeleteCollectionWorkers: 1,
|
||||
ResourcePrefix: "validatingadmissionpolicies"}
|
||||
storage, err := NewREST(restOptions)
|
||||
storage, err := NewREST(restOptions, authorizer, resourceResolver)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||
}
|
||||
@ -213,7 +219,7 @@ func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||
}
|
||||
|
||||
func TestCategories(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
expected := []string{"api-extensions"}
|
||||
|
@ -18,37 +18,49 @@ package validatingadmissionpolicy
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration/validation"
|
||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||
)
|
||||
|
||||
// validatingAdmissionPolicyStrategy implements verification logic for ValidatingAdmissionPolicy.
|
||||
type validatingAdmissionPolicyStrategy struct {
|
||||
runtime.ObjectTyper
|
||||
names.NameGenerator
|
||||
authorizer authorizer.Authorizer
|
||||
resourceResolver resolver.ResourceResolver
|
||||
}
|
||||
|
||||
// Strategy is the default logic that applies when creating and updating validatingAdmissionPolicy objects.
|
||||
var Strategy = validatingAdmissionPolicyStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
|
||||
// NewStrategy is the default logic that applies when creating and updating validatingAdmissionPolicy objects.
|
||||
func NewStrategy(authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) *validatingAdmissionPolicyStrategy {
|
||||
return &validatingAdmissionPolicyStrategy{
|
||||
ObjectTyper: legacyscheme.Scheme,
|
||||
NameGenerator: names.SimpleNameGenerator,
|
||||
authorizer: authorizer,
|
||||
resourceResolver: resourceResolver,
|
||||
}
|
||||
}
|
||||
|
||||
// NamespaceScoped returns false because ValidatingAdmissionPolicy is cluster-scoped resource.
|
||||
func (validatingAdmissionPolicyStrategy) NamespaceScoped() bool {
|
||||
func (v *validatingAdmissionPolicyStrategy) NamespaceScoped() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// PrepareForCreate clears the status of an validatingAdmissionPolicy before creation.
|
||||
func (validatingAdmissionPolicyStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||
func (v *validatingAdmissionPolicyStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||
ic := obj.(*admissionregistration.ValidatingAdmissionPolicy)
|
||||
ic.Generation = 1
|
||||
}
|
||||
|
||||
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
|
||||
func (validatingAdmissionPolicyStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||
func (v *validatingAdmissionPolicyStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||
newIC := obj.(*admissionregistration.ValidatingAdmissionPolicy)
|
||||
oldIC := old.(*admissionregistration.ValidatingAdmissionPolicy)
|
||||
|
||||
@ -61,36 +73,50 @@ func (validatingAdmissionPolicyStrategy) PrepareForUpdate(ctx context.Context, o
|
||||
}
|
||||
|
||||
// Validate validates a new validatingAdmissionPolicy.
|
||||
func (validatingAdmissionPolicyStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||
return validation.ValidateValidatingAdmissionPolicy(obj.(*admissionregistration.ValidatingAdmissionPolicy))
|
||||
func (v *validatingAdmissionPolicyStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||
errs := validation.ValidateValidatingAdmissionPolicy(obj.(*admissionregistration.ValidatingAdmissionPolicy))
|
||||
if len(errs) == 0 {
|
||||
// if the object is well-formed, also authorize the paramKind
|
||||
if err := v.authorizeCreate(ctx, obj); err != nil {
|
||||
errs = append(errs, field.Forbidden(field.NewPath("spec", "paramKind"), err.Error()))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// WarningsOnCreate returns warnings for the creation of the given object.
|
||||
func (validatingAdmissionPolicyStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||
func (v *validatingAdmissionPolicyStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Canonicalize normalizes the object after validation.
|
||||
func (validatingAdmissionPolicyStrategy) Canonicalize(obj runtime.Object) {
|
||||
func (v *validatingAdmissionPolicyStrategy) Canonicalize(obj runtime.Object) {
|
||||
}
|
||||
|
||||
// AllowCreateOnUpdate is true for validatingAdmissionPolicy; this means you may create one with a PUT request.
|
||||
func (validatingAdmissionPolicyStrategy) AllowCreateOnUpdate() bool {
|
||||
func (v *validatingAdmissionPolicyStrategy) AllowCreateOnUpdate() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateUpdate is the default update validation for an end user.
|
||||
func (validatingAdmissionPolicyStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||
return validation.ValidateValidatingAdmissionPolicyUpdate(obj.(*admissionregistration.ValidatingAdmissionPolicy), old.(*admissionregistration.ValidatingAdmissionPolicy))
|
||||
func (v *validatingAdmissionPolicyStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||
errs := validation.ValidateValidatingAdmissionPolicyUpdate(obj.(*admissionregistration.ValidatingAdmissionPolicy), old.(*admissionregistration.ValidatingAdmissionPolicy))
|
||||
if len(errs) == 0 {
|
||||
// if the object is well-formed, also authorize the paramKind
|
||||
if err := v.authorizeUpdate(ctx, obj, old); err != nil {
|
||||
errs = append(errs, field.Forbidden(field.NewPath("spec", "paramKind"), err.Error()))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// WarningsOnUpdate returns warnings for the given update.
|
||||
func (validatingAdmissionPolicyStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||
func (v *validatingAdmissionPolicyStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllowUnconditionalUpdate is the default update policy for validatingAdmissionPolicy objects. Status update should
|
||||
// only be allowed if version match.
|
||||
func (validatingAdmissionPolicyStrategy) AllowUnconditionalUpdate() bool {
|
||||
func (v *validatingAdmissionPolicyStrategy) AllowUnconditionalUpdate() bool {
|
||||
return false
|
||||
}
|
||||
|
@ -25,25 +25,26 @@ import (
|
||||
)
|
||||
|
||||
func TestValidatingAdmissionPolicyStrategy(t *testing.T) {
|
||||
strategy := NewStrategy(nil, nil)
|
||||
ctx := genericapirequest.NewDefaultContext()
|
||||
if Strategy.NamespaceScoped() {
|
||||
if strategy.NamespaceScoped() {
|
||||
t.Error("ValidatingAdmissionPolicy strategy must be cluster scoped")
|
||||
}
|
||||
if Strategy.AllowCreateOnUpdate() {
|
||||
if strategy.AllowCreateOnUpdate() {
|
||||
t.Errorf("ValidatingAdmissionPolicy should not allow create on update")
|
||||
}
|
||||
|
||||
configuration := validValidatingAdmissionPolicy()
|
||||
Strategy.PrepareForCreate(ctx, configuration)
|
||||
errs := Strategy.Validate(ctx, configuration)
|
||||
strategy.PrepareForCreate(ctx, configuration)
|
||||
errs := strategy.Validate(ctx, configuration)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("Unexpected error validating %v", errs)
|
||||
}
|
||||
invalidConfiguration := &admissionregistration.ValidatingAdmissionPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: ""},
|
||||
}
|
||||
Strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration)
|
||||
errs = Strategy.ValidateUpdate(ctx, invalidConfiguration, configuration)
|
||||
strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration)
|
||||
errs = strategy.ValidateUpdate(ctx, invalidConfiguration, configuration)
|
||||
if len(errs) == 0 {
|
||||
t.Errorf("Expected a validation error")
|
||||
}
|
||||
|
@ -0,0 +1,110 @@
|
||||
/*
|
||||
Copyright 2022 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 validatingadmissionpolicybinding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
|
||||
)
|
||||
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) authorizeCreate(ctx context.Context, obj runtime.Object) error {
|
||||
binding := obj.(*admissionregistration.ValidatingAdmissionPolicyBinding)
|
||||
if binding.Spec.ParamRef == nil {
|
||||
// no paramRef in new object
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.authorize(ctx, binding)
|
||||
}
|
||||
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) authorizeUpdate(ctx context.Context, obj, old runtime.Object) error {
|
||||
binding := obj.(*admissionregistration.ValidatingAdmissionPolicyBinding)
|
||||
if binding.Spec.ParamRef == nil {
|
||||
// no paramRef in new object
|
||||
return nil
|
||||
}
|
||||
|
||||
oldBinding := old.(*admissionregistration.ValidatingAdmissionPolicyBinding)
|
||||
if oldBinding.Spec.ParamRef != nil && *oldBinding.Spec.ParamRef == *binding.Spec.ParamRef && oldBinding.Spec.PolicyName == binding.Spec.PolicyName {
|
||||
// identical paramRef and policy to old object
|
||||
return nil
|
||||
}
|
||||
|
||||
return v.authorize(ctx, binding)
|
||||
}
|
||||
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) authorize(ctx context.Context, binding *admissionregistration.ValidatingAdmissionPolicyBinding) error {
|
||||
if v.authorizer == nil || v.resourceResolver == nil || binding.Spec.ParamRef == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// for superuser, skip all checks
|
||||
if rbacregistry.EscalationAllowed(ctx) {
|
||||
return nil
|
||||
}
|
||||
|
||||
user, ok := genericapirequest.UserFrom(ctx)
|
||||
if !ok {
|
||||
return fmt.Errorf("cannot identify user to authorize read access to paramRef object")
|
||||
}
|
||||
|
||||
// default to requiring permissions on all group/version/resources
|
||||
resource, apiGroup, apiVersion := "*", "*", "*"
|
||||
|
||||
if policy, err := v.policyGetter.GetValidatingAdmissionPolicy(ctx, binding.Spec.PolicyName); err == nil && policy.Spec.ParamKind != nil {
|
||||
paramKind := policy.Spec.ParamKind
|
||||
if gv, err := schema.ParseGroupVersion(paramKind.APIVersion); err == nil {
|
||||
// we only need to authorize the parsed group/version
|
||||
apiGroup = gv.Group
|
||||
apiVersion = gv.Version
|
||||
if gvr, err := v.resourceResolver.Resolve(gv.WithKind(paramKind.Kind)); err == nil {
|
||||
// we only need to authorize the resolved resource
|
||||
resource = gvr.Resource
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paramRef := binding.Spec.ParamRef
|
||||
|
||||
// require that the user can read (verb "get") the referred resource.
|
||||
attrs := authorizer.AttributesRecord{
|
||||
User: user,
|
||||
Verb: "get",
|
||||
ResourceRequest: true,
|
||||
Name: paramRef.Name,
|
||||
Namespace: paramRef.Namespace,
|
||||
APIGroup: apiGroup,
|
||||
APIVersion: apiVersion,
|
||||
Resource: resource,
|
||||
}
|
||||
|
||||
d, _, err := v.authorizer.Authorize(ctx, attrs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d != authorizer.DecisionAllow {
|
||||
return fmt.Errorf(`user %v does not have "get" permission on the object referenced by paramRef`, user)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
/*
|
||||
Copyright 2022 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 validatingadmissionpolicybinding
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||
)
|
||||
|
||||
func TestAuthorization(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
userInfo user.Info
|
||||
auth AuthFunc
|
||||
policyGetter PolicyGetterFunc
|
||||
resourceResolver resolver.ResourceResolverFunc
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "superuser",
|
||||
userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}},
|
||||
expectErr: false, // success despite always-denying authorizer
|
||||
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
return authorizer.DecisionDeny, "", nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "authorized",
|
||||
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
||||
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
if a.GetResource() == "configmaps" {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", nil
|
||||
},
|
||||
policyGetter: func(ctx context.Context, name string) (*admissionregistration.ValidatingAdmissionPolicy, error) {
|
||||
return &admissionregistration.ValidatingAdmissionPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"},
|
||||
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||
ParamKind: &admissionregistration.ParamKind{Kind: "ConfigMap", APIVersion: "v1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||
return schema.GroupVersionResource{
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Resource: "configmaps",
|
||||
}, nil
|
||||
},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "denied",
|
||||
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
|
||||
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
if a.GetResource() == "configmaps" {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", nil
|
||||
},
|
||||
policyGetter: func(ctx context.Context, name string) (*admissionregistration.ValidatingAdmissionPolicy, error) {
|
||||
return &admissionregistration.ValidatingAdmissionPolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"},
|
||||
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||
ParamKind: &admissionregistration.ParamKind{Kind: "Params", APIVersion: "foo.example.com/v1"},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
|
||||
return schema.GroupVersionResource{
|
||||
Group: "foo.example.com",
|
||||
Version: "v1",
|
||||
Resource: "params",
|
||||
}, nil
|
||||
},
|
||||
expectErr: true,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
strategy := NewStrategy(tc.auth, tc.policyGetter, tc.resourceResolver)
|
||||
t.Run("create", func(t *testing.T) {
|
||||
ctx := request.WithUser(context.Background(), tc.userInfo)
|
||||
errs := strategy.Validate(ctx, validPolicyBinding())
|
||||
if len(errs) > 0 != tc.expectErr {
|
||||
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
|
||||
}
|
||||
})
|
||||
t.Run("update", func(t *testing.T) {
|
||||
ctx := request.WithUser(context.Background(), tc.userInfo)
|
||||
obj := validPolicyBinding()
|
||||
objWithChangedParamRef := obj.DeepCopy()
|
||||
objWithChangedParamRef.Spec.ParamRef.Name = "changed"
|
||||
errs := strategy.ValidateUpdate(ctx, obj, objWithChangedParamRef)
|
||||
if len(errs) > 0 != tc.expectErr {
|
||||
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type AuthFunc func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error)
|
||||
|
||||
func (f AuthFunc) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
return f(ctx, a)
|
||||
}
|
||||
|
||||
type PolicyGetterFunc func(ctx context.Context, name string) (*admissionregistration.ValidatingAdmissionPolicy, error)
|
||||
|
||||
func (f PolicyGetterFunc) GetValidatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.ValidatingAdmissionPolicy, error) {
|
||||
return f(ctx, name)
|
||||
}
|
@ -17,7 +17,11 @@ limitations under the License.
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
@ -25,6 +29,7 @@ import (
|
||||
"k8s.io/kubernetes/pkg/printers"
|
||||
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
|
||||
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
|
||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicybinding"
|
||||
)
|
||||
|
||||
@ -33,19 +38,23 @@ type REST struct {
|
||||
*genericregistry.Store
|
||||
}
|
||||
|
||||
var groupResource = admissionregistration.Resource("validatingadmissionpolicybindings")
|
||||
|
||||
// NewREST returns a RESTStorage object that will work against policyBinding.
|
||||
func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
|
||||
func NewREST(optsGetter generic.RESTOptionsGetter, authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) (*REST, error) {
|
||||
r := &REST{}
|
||||
strategy := validatingadmissionpolicybinding.NewStrategy(authorizer, policyGetter, resourceResolver)
|
||||
store := &genericregistry.Store{
|
||||
NewFunc: func() runtime.Object { return &admissionregistration.ValidatingAdmissionPolicyBinding{} },
|
||||
NewListFunc: func() runtime.Object { return &admissionregistration.ValidatingAdmissionPolicyBindingList{} },
|
||||
ObjectNameFunc: func(obj runtime.Object) (string, error) {
|
||||
return obj.(*admissionregistration.ValidatingAdmissionPolicyBinding).Name, nil
|
||||
},
|
||||
DefaultQualifiedResource: admissionregistration.Resource("validatingadmissionpolicybindings"),
|
||||
DefaultQualifiedResource: groupResource,
|
||||
|
||||
CreateStrategy: validatingadmissionpolicybinding.Strategy,
|
||||
UpdateStrategy: validatingadmissionpolicybinding.Strategy,
|
||||
DeleteStrategy: validatingadmissionpolicybinding.Strategy,
|
||||
CreateStrategy: strategy,
|
||||
UpdateStrategy: strategy,
|
||||
DeleteStrategy: strategy,
|
||||
|
||||
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
|
||||
}
|
||||
@ -53,7 +62,8 @@ func NewREST(optsGetter generic.RESTOptionsGetter) (*REST, error) {
|
||||
if err := store.CompleteWithOptions(options); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &REST{store}, nil
|
||||
r.Store = store
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Implement CategoriesProvider
|
||||
@ -63,3 +73,21 @@ var _ rest.CategoriesProvider = &REST{}
|
||||
func (r *REST) Categories() []string {
|
||||
return []string{"api-extensions"}
|
||||
}
|
||||
|
||||
type PolicyGetter interface {
|
||||
// GetValidatingAdmissionPolicy returns a GetValidatingAdmissionPolicy
|
||||
// by its name. There is no namespace because it is cluster-scoped.
|
||||
GetValidatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.ValidatingAdmissionPolicy, error)
|
||||
}
|
||||
|
||||
type DefaultPolicyGetter struct {
|
||||
Getter rest.Getter
|
||||
}
|
||||
|
||||
func (g *DefaultPolicyGetter) GetValidatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.ValidatingAdmissionPolicy, error) {
|
||||
p, err := g.Getter.Get(ctx, name, &metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.(*admissionregistration.ValidatingAdmissionPolicy), err
|
||||
}
|
||||
|
@ -23,10 +23,12 @@ import (
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||
|
||||
// Ensure that admissionregistration package is initialized.
|
||||
@ -34,7 +36,7 @@ import (
|
||||
)
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -48,7 +50,7 @@ func TestCreate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -72,7 +74,7 @@ func TestUpdate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -80,7 +82,7 @@ func TestGet(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestList(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -88,7 +90,7 @@ func TestList(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -96,7 +98,7 @@ func TestDelete(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWatch(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
test := genericregistrytest.New(t, storage.Store).ClusterScope()
|
||||
@ -169,14 +171,18 @@ func newPolicyBinding(name string) *admissionregistration.ValidatingAdmissionPol
|
||||
}
|
||||
}
|
||||
|
||||
func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||
func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||
return newStorage(t, nil, nil, nil)
|
||||
}
|
||||
|
||||
func newStorage(t *testing.T, authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) {
|
||||
etcdStorage, server := registrytest.NewEtcdStorageForResource(t, admissionregistration.Resource("validatingadmissionpolicybindings"))
|
||||
restOptions := generic.RESTOptions{
|
||||
StorageConfig: etcdStorage,
|
||||
Decorator: generic.UndecoratedStorage,
|
||||
DeleteCollectionWorkers: 1,
|
||||
ResourcePrefix: "validatingadmissionpolicybindings"}
|
||||
storage, err := NewREST(restOptions)
|
||||
storage, err := NewREST(restOptions, authorizer, policyGetter, resourceResolver)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error from REST storage: %v", err)
|
||||
}
|
||||
@ -184,7 +190,7 @@ func newStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
|
||||
}
|
||||
|
||||
func TestCategories(t *testing.T) {
|
||||
storage, server := newStorage(t)
|
||||
storage, server := newInsecureStorage(t)
|
||||
defer server.Terminate(t)
|
||||
defer storage.Store.DestroyFunc()
|
||||
expected := []string{"api-extensions"}
|
||||
|
@ -18,37 +18,57 @@ package validatingadmissionpolicybinding
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/storage/names"
|
||||
"k8s.io/kubernetes/pkg/api/legacyscheme"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||
"k8s.io/kubernetes/pkg/apis/admissionregistration/validation"
|
||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||
)
|
||||
|
||||
// ValidatingAdmissionPolicyBindingStrategy implements verification logic for ValidatingAdmissionPolicyBinding.
|
||||
type ValidatingAdmissionPolicyBindingStrategy struct {
|
||||
// validatingAdmissionPolicyBindingStrategy implements verification logic for ValidatingAdmissionPolicyBinding.
|
||||
type validatingAdmissionPolicyBindingStrategy struct {
|
||||
runtime.ObjectTyper
|
||||
names.NameGenerator
|
||||
authorizer authorizer.Authorizer
|
||||
policyGetter PolicyGetter
|
||||
resourceResolver resolver.ResourceResolver
|
||||
}
|
||||
|
||||
// Strategy is the default logic that applies when creating and updating ValidatingAdmissionPolicyBinding objects.
|
||||
var Strategy = ValidatingAdmissionPolicyBindingStrategy{legacyscheme.Scheme, names.SimpleNameGenerator}
|
||||
type PolicyGetter interface {
|
||||
// GetValidatingAdmissionPolicy returns a GetValidatingAdmissionPolicy
|
||||
// by its name. There is no namespace because it is cluster-scoped.
|
||||
GetValidatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.ValidatingAdmissionPolicy, error)
|
||||
}
|
||||
|
||||
// NewStrategy is the default logic that applies when creating and updating ValidatingAdmissionPolicyBinding objects.
|
||||
func NewStrategy(authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) *validatingAdmissionPolicyBindingStrategy {
|
||||
return &validatingAdmissionPolicyBindingStrategy{
|
||||
ObjectTyper: legacyscheme.Scheme,
|
||||
NameGenerator: names.SimpleNameGenerator,
|
||||
authorizer: authorizer,
|
||||
policyGetter: policyGetter,
|
||||
resourceResolver: resourceResolver,
|
||||
}
|
||||
}
|
||||
|
||||
// NamespaceScoped returns false because ValidatingAdmissionPolicyBinding is cluster-scoped resource.
|
||||
func (ValidatingAdmissionPolicyBindingStrategy) NamespaceScoped() bool {
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) NamespaceScoped() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// PrepareForCreate clears the status of an ValidatingAdmissionPolicyBinding before creation.
|
||||
func (ValidatingAdmissionPolicyBindingStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
|
||||
ic := obj.(*admissionregistration.ValidatingAdmissionPolicyBinding)
|
||||
ic.Generation = 1
|
||||
}
|
||||
|
||||
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
|
||||
func (ValidatingAdmissionPolicyBindingStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
|
||||
newIC := obj.(*admissionregistration.ValidatingAdmissionPolicyBinding)
|
||||
oldIC := old.(*admissionregistration.ValidatingAdmissionPolicyBinding)
|
||||
|
||||
@ -61,36 +81,50 @@ func (ValidatingAdmissionPolicyBindingStrategy) PrepareForUpdate(ctx context.Con
|
||||
}
|
||||
|
||||
// Validate validates a new ValidatingAdmissionPolicyBinding.
|
||||
func (ValidatingAdmissionPolicyBindingStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||
return validation.ValidateValidatingAdmissionPolicyBinding(obj.(*admissionregistration.ValidatingAdmissionPolicyBinding))
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
|
||||
errs := validation.ValidateValidatingAdmissionPolicyBinding(obj.(*admissionregistration.ValidatingAdmissionPolicyBinding))
|
||||
if len(errs) == 0 {
|
||||
// if the object is well-formed, also authorize the paramRef
|
||||
if err := v.authorizeCreate(ctx, obj); err != nil {
|
||||
errs = append(errs, field.Forbidden(field.NewPath("spec", "paramRef"), err.Error()))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// WarningsOnCreate returns warnings for the creation of the given object.
|
||||
func (ValidatingAdmissionPolicyBindingStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Canonicalize normalizes the object after validation.
|
||||
func (ValidatingAdmissionPolicyBindingStrategy) Canonicalize(obj runtime.Object) {
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) Canonicalize(obj runtime.Object) {
|
||||
}
|
||||
|
||||
// AllowCreateOnUpdate is true for ValidatingAdmissionPolicyBinding; this means you may create one with a PUT request.
|
||||
func (ValidatingAdmissionPolicyBindingStrategy) AllowCreateOnUpdate() bool {
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) AllowCreateOnUpdate() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// ValidateUpdate is the default update validation for an end user.
|
||||
func (ValidatingAdmissionPolicyBindingStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||
return validation.ValidateValidatingAdmissionPolicyBindingUpdate(obj.(*admissionregistration.ValidatingAdmissionPolicyBinding), old.(*admissionregistration.ValidatingAdmissionPolicyBinding))
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
|
||||
errs := validation.ValidateValidatingAdmissionPolicyBindingUpdate(obj.(*admissionregistration.ValidatingAdmissionPolicyBinding), old.(*admissionregistration.ValidatingAdmissionPolicyBinding))
|
||||
if len(errs) == 0 {
|
||||
// if the object is well-formed, also authorize the paramRef
|
||||
if err := v.authorizeUpdate(ctx, obj, old); err != nil {
|
||||
errs = append(errs, field.Forbidden(field.NewPath("spec", "paramRef"), err.Error()))
|
||||
}
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// WarningsOnUpdate returns warnings for the given update.
|
||||
func (ValidatingAdmissionPolicyBindingStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllowUnconditionalUpdate is the default update policy for ValidatingAdmissionPolicyBinding objects. Status update should
|
||||
// only be allowed if version match.
|
||||
func (ValidatingAdmissionPolicyBindingStrategy) AllowUnconditionalUpdate() bool {
|
||||
func (v *validatingAdmissionPolicyBindingStrategy) AllowUnconditionalUpdate() bool {
|
||||
return false
|
||||
}
|
||||
|
@ -25,25 +25,26 @@ import (
|
||||
)
|
||||
|
||||
func TestPolicyBindingStrategy(t *testing.T) {
|
||||
strategy := NewStrategy(nil, nil, nil)
|
||||
ctx := genericapirequest.NewDefaultContext()
|
||||
if Strategy.NamespaceScoped() {
|
||||
if strategy.NamespaceScoped() {
|
||||
t.Error("PolicyBinding strategy must be cluster scoped")
|
||||
}
|
||||
if Strategy.AllowCreateOnUpdate() {
|
||||
if strategy.AllowCreateOnUpdate() {
|
||||
t.Errorf("PolicyBinding should not allow create on update")
|
||||
}
|
||||
|
||||
configuration := validPolicyBinding()
|
||||
Strategy.PrepareForCreate(ctx, configuration)
|
||||
errs := Strategy.Validate(ctx, configuration)
|
||||
strategy.PrepareForCreate(ctx, configuration)
|
||||
errs := strategy.Validate(ctx, configuration)
|
||||
if len(errs) != 0 {
|
||||
t.Errorf("Unexpected error validating %v", errs)
|
||||
}
|
||||
invalidConfiguration := &admissionregistration.ValidatingAdmissionPolicyBinding{
|
||||
ObjectMeta: metav1.ObjectMeta{Name: ""},
|
||||
}
|
||||
Strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration)
|
||||
errs = Strategy.ValidateUpdate(ctx, invalidConfiguration, configuration)
|
||||
strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration)
|
||||
errs = strategy.ValidateUpdate(ctx, invalidConfiguration, configuration)
|
||||
if len(errs) == 0 {
|
||||
t.Errorf("Expected a validation error")
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user