From d3f48136d05ba58550744233b60caa4b8d71012f Mon Sep 17 00:00:00 2001 From: Cici Huang Date: Mon, 7 Nov 2022 21:29:56 +0000 Subject: [PATCH] Add Authz check to validate policy and binding. Co-authored-by: Jiahui Feng Co-authored-by: Jordan Liggitt --- pkg/controlplane/instance.go | 10 +- .../resolver/resolver.go | 72 ++++++++++ .../rest/storage_apiserver.go | 21 ++- .../validatingadmissionpolicy/authz.go | 105 ++++++++++++++ .../validatingadmissionpolicy/authz_test.go | 110 ++++++++++++++ .../storage/storage.go | 19 ++- .../storage/storage_test.go | 24 ++-- .../validatingadmissionpolicy/strategy.go | 54 +++++-- .../strategy_test.go | 13 +- .../validatingadmissionpolicybinding/authz.go | 110 ++++++++++++++ .../authz_test.go | 135 ++++++++++++++++++ .../storage/storage.go | 40 +++++- .../storage/storage_test.go | 24 ++-- .../strategy.go | 66 ++++++--- .../strategy_test.go | 13 +- 15 files changed, 740 insertions(+), 76 deletions(-) create mode 100644 pkg/registry/admissionregistration/resolver/resolver.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicy/authz.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicy/authz_test.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz.go create mode 100644 pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz_test.go diff --git a/pkg/controlplane/instance.go b/pkg/controlplane/instance.go index a31efd0e6bf..c1fbb7f8c8a 100644 --- a/pkg/controlplane/instance.go +++ b/pkg/controlplane/instance.go @@ -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 { diff --git a/pkg/registry/admissionregistration/resolver/resolver.go b/pkg/registry/admissionregistration/resolver/resolver.go new file mode 100644 index 00000000000..2f18d01dcd1 --- /dev/null +++ b/pkg/registry/admissionregistration/resolver/resolver.go @@ -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) +} diff --git a/pkg/registry/admissionregistration/rest/storage_apiserver.go b/pkg/registry/admissionregistration/rest/storage_apiserver.go index 7ea72f6e973..49dd31f028d 100644 --- a/pkg/registry/admissionregistration/rest/storage_apiserver.go +++ b/pkg/registry/admissionregistration/rest/storage_apiserver.go @@ -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 } diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/authz.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/authz.go new file mode 100644 index 00000000000..772d3a000cf --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/authz.go @@ -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 +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/authz_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/authz_test.go new file mode 100644 index 00000000000..dc4a7e5d017 --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/authz_test.go @@ -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) +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage.go index 6985ad96e4e..2c2f765e740 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage.go @@ -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 diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go index 1f1bfceb5db..02c2d134848 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/storage/storage_test.go @@ -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"} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy.go index f12ecca4fe5..70bc547048f 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy.go @@ -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 } diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go index c39008a8703..cb993f72f71 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicy/strategy_test.go @@ -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") } diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz.go new file mode 100644 index 00000000000..a68e6c727e1 --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz.go @@ -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 +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz_test.go new file mode 100644 index 00000000000..f42fcd380d2 --- /dev/null +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/authz_test.go @@ -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) +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage.go index 97773cc86ac..0f0afa4b39b 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage.go @@ -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 +} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go index 8cf94d04c35..6961873463f 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/storage/storage_test.go @@ -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"} diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy.go index 8fef843e6c6..9c43c7855f3 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy.go @@ -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 } diff --git a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go index 415932962b8..a3a2bf91841 100644 --- a/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go +++ b/pkg/registry/admissionregistration/validatingadmissionpolicybinding/strategy_test.go @@ -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") }