Add admission plugins for APIDeprecation, CertificateRequestApproval&Identity, ResourceValidation
Signed-off-by: James Munnelly <jmunnelly@apple.com>
This commit is contained in:
parent
dd560bca6a
commit
9583050538
@ -17,6 +17,7 @@ filegroup(
|
||||
"//internal/apis/meta:all-srcs",
|
||||
"//internal/controller/feature:all-srcs",
|
||||
"//internal/ingress:all-srcs",
|
||||
"//internal/plugin:all-srcs",
|
||||
"//internal/test/paths:all-srcs",
|
||||
"//internal/vault:all-srcs",
|
||||
"//internal/webhook:all-srcs",
|
||||
|
||||
36
internal/plugin/BUILD.bazel
Normal file
36
internal/plugin/BUILD.bazel
Normal file
@ -0,0 +1,36 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["plugins.go"],
|
||||
importpath = "github.com/jetstack/cert-manager/internal/plugin",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/plugin/admission/apideprecation:go_default_library",
|
||||
"//internal/plugin/admission/certificaterequest/approval:go_default_library",
|
||||
"//internal/plugin/admission/certificaterequest/identity:go_default_library",
|
||||
"//internal/plugin/admission/resourcevalidation:go_default_library",
|
||||
"//pkg/webhook/admission:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/util/sets:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [
|
||||
":package-srcs",
|
||||
"//internal/plugin/admission/apideprecation:all-srcs",
|
||||
"//internal/plugin/admission/certificaterequest/approval:all-srcs",
|
||||
"//internal/plugin/admission/certificaterequest/identity:all-srcs",
|
||||
"//internal/plugin/admission/resourcevalidation:all-srcs",
|
||||
],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
39
internal/plugin/admission/apideprecation/BUILD.bazel
Normal file
39
internal/plugin/admission/apideprecation/BUILD.bazel
Normal file
@ -0,0 +1,39 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["apideprecation.go"],
|
||||
importpath = "github.com/jetstack/cert-manager/internal/plugin/admission/apideprecation",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//pkg/apis/acme:go_default_library",
|
||||
"//pkg/apis/certmanager:go_default_library",
|
||||
"//pkg/webhook/admission:go_default_library",
|
||||
"@io_k8s_api//admission/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/runtime:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["apideprecation_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"@io_k8s_api//admission/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
|
||||
],
|
||||
)
|
||||
64
internal/plugin/admission/apideprecation/apideprecation.go
Normal file
64
internal/plugin/admission/apideprecation/apideprecation.go
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2021 The cert-manager 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 apideprecation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
"github.com/jetstack/cert-manager/pkg/apis/acme"
|
||||
"github.com/jetstack/cert-manager/pkg/apis/certmanager"
|
||||
"github.com/jetstack/cert-manager/pkg/webhook/admission"
|
||||
)
|
||||
|
||||
const PluginName = "APIDeprecation"
|
||||
|
||||
type apiDeprecation struct{}
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func() (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &apiDeprecation{}
|
||||
|
||||
func (p apiDeprecation) Handles(_ admissionv1.Operation) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p apiDeprecation) Validate(ctx context.Context, request admissionv1.AdmissionRequest, oldObj, obj runtime.Object) (warnings []string, err error) {
|
||||
// Only generate warning messages for cert-manager.io and acme.cert-manager.io APIs
|
||||
if request.RequestResource.Group != certmanager.GroupName &&
|
||||
request.RequestResource.Group != acme.GroupName {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// All non-v1 API resources in cert-manager.io and acme.cert-manager.io are now deprecated
|
||||
if request.RequestResource.Version == "v1" {
|
||||
return nil, nil
|
||||
}
|
||||
return []string{fmt.Sprintf("%s.%s/%s is deprecated in v1.4+, unavailable in v1.6+; use %s.%s/v1", request.RequestResource.Resource, request.RequestResource.Group, request.RequestResource.Version, request.RequestResource.Resource, request.RequestResource.Group)}, nil
|
||||
}
|
||||
|
||||
func NewPlugin() admission.Interface {
|
||||
return new(apiDeprecation)
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
/*
|
||||
Copyright 2021 The cert-manager 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 apideprecation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestAPIDeprecation(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
req *admissionv1.AdmissionRequest
|
||||
warnings []string
|
||||
}{
|
||||
"should print warnings for all non-v1 cert-manager.io types": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: "cert-manager.io",
|
||||
Version: "something-not-v1",
|
||||
Resource: "somethings",
|
||||
},
|
||||
},
|
||||
warnings: []string{"somethings.cert-manager.io/something-not-v1 is deprecated in v1.4+, unavailable in v1.6+; use somethings.cert-manager.io/v1"},
|
||||
},
|
||||
"should print warnings for all non-v1 acme.cert-manager.io types": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: "acme.cert-manager.io",
|
||||
Version: "something-not-v1",
|
||||
Resource: "somethings",
|
||||
},
|
||||
},
|
||||
warnings: []string{"somethings.acme.cert-manager.io/something-not-v1 is deprecated in v1.4+, unavailable in v1.6+; use somethings.acme.cert-manager.io/v1"},
|
||||
},
|
||||
"should not print warnings for non-v1 types in other groups": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: "some-other-group-name",
|
||||
Version: "something-not-v1",
|
||||
Resource: "somethings",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
p := NewPlugin().(*apiDeprecation)
|
||||
warnings, err := p.Validate(context.Background(), *test.req, nil, nil)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error")
|
||||
}
|
||||
if !reflect.DeepEqual(warnings, test.warnings) {
|
||||
t.Errorf("unexpected warnings, exp=%q, got=%q", test.warnings, warnings)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["certificaterequest_approval.go"],
|
||||
importpath = "github.com/jetstack/cert-manager/internal/plugin/admission/certificaterequest/approval",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/apis/certmanager:go_default_library",
|
||||
"//internal/apis/certmanager/validation/util:go_default_library",
|
||||
"//pkg/webhook/admission:go_default_library",
|
||||
"//pkg/webhook/admission/initializer:go_default_library",
|
||||
"@io_k8s_api//admission/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/runtime:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/runtime/schema:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/util/validation/field:go_default_library",
|
||||
"@io_k8s_apiserver//pkg/authentication/user:go_default_library",
|
||||
"@io_k8s_apiserver//pkg/authorization/authorizer:go_default_library",
|
||||
"@io_k8s_client_go//discovery:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["certificaterequest_approval_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//internal/apis/certmanager:go_default_library",
|
||||
"//internal/apis/meta:go_default_library",
|
||||
"//test/unit/discovery:go_default_library",
|
||||
"@io_k8s_api//admission/v1:go_default_library",
|
||||
"@io_k8s_api//authentication/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/util/validation/field:go_default_library",
|
||||
"@io_k8s_apiserver//pkg/authorization/authorizer:go_default_library",
|
||||
"@io_k8s_client_go//discovery:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@ -0,0 +1,284 @@
|
||||
/*
|
||||
Copyright 2021 The cert-manager 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 approval
|
||||
|
||||
// CertificateRequestApproval is a plugin that ensures entities that are attempting to
|
||||
// modify `status.conditions[type="Approved"]` or `status.conditions[type="Denied"]`
|
||||
// have permission to do so (granted via RBAC).
|
||||
// Entities will need to be able to `approve` (verb) `signers` (resource type) in
|
||||
// `cert-manager.io` (group) with the name `<issuer-type>.<issuer-group>/[<certificaterequest-namespace>.]<issuer-name>`.
|
||||
// For example: `issuers.cert-manager.io/my-namespace.my-issuer-name`.
|
||||
// A wildcard signerName format is also supported: `issuers.cert-manager.io/*`.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/discovery"
|
||||
|
||||
"github.com/jetstack/cert-manager/internal/apis/certmanager"
|
||||
"github.com/jetstack/cert-manager/internal/apis/certmanager/validation/util"
|
||||
"github.com/jetstack/cert-manager/pkg/webhook/admission"
|
||||
"github.com/jetstack/cert-manager/pkg/webhook/admission/initializer"
|
||||
)
|
||||
|
||||
const PluginName = "CertificateRequestApproval"
|
||||
|
||||
type certificateRequestApproval struct {
|
||||
*admission.Handler
|
||||
|
||||
authorizer authorizer.Authorizer
|
||||
discovery discovery.DiscoveryInterface
|
||||
|
||||
// resourceCache stores the associated APIResource for a given GroupKind
|
||||
// to making multiple queries to the API server for every approval.
|
||||
resourceCache map[schema.GroupKind]metav1.APIResource
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &certificateRequestApproval{}
|
||||
var _ initializer.WantsAuthorizer = &certificateRequestApproval{}
|
||||
var _ initializer.WantsDiscoveryCache = &certificateRequestApproval{}
|
||||
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func() (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
func NewPlugin() admission.Interface {
|
||||
return &certificateRequestApproval{
|
||||
Handler: admission.NewHandler(admissionv1.Update),
|
||||
resourceCache: map[schema.GroupKind]metav1.APIResource{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *certificateRequestApproval) Validate(ctx context.Context, request admissionv1.AdmissionRequest, oldObj, obj runtime.Object) (warnings []string, err error) {
|
||||
if request.RequestResource.Group != "cert-manager.io" ||
|
||||
request.RequestResource.Resource != "certificaterequests" ||
|
||||
request.RequestSubResource != "status" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
oldCR, cr := oldObj.(*certmanager.CertificateRequest), obj.(*certmanager.CertificateRequest)
|
||||
if !approvalConditionsHaveChanged(oldCR, cr) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
group := cr.Spec.IssuerRef.Group
|
||||
kind := cr.Spec.IssuerRef.Kind
|
||||
// TODO: move this defaulting into the Scheme (registered as default functions) so
|
||||
// these will be set when the CertificateRequest is decoded.
|
||||
if group == "" {
|
||||
group = "cert-manager.io"
|
||||
}
|
||||
if kind == "" {
|
||||
kind = "Issuer"
|
||||
}
|
||||
apiResource, err := c.apiResourceForGroupKind(schema.GroupKind{Group: group, Kind: kind})
|
||||
switch {
|
||||
case err == errNoResourceExists:
|
||||
return nil, field.Forbidden(field.NewPath("spec.issuerRef"),
|
||||
fmt.Sprintf("referenced signer resource does not exist: %v", cr.Spec.IssuerRef))
|
||||
case err != nil:
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signerName := signerNameForAPIResource(cr.Spec.IssuerRef.Name, cr.Namespace, *apiResource)
|
||||
if !isAuthorizedForSignerName(ctx, c.authorizer, userInfoForRequest(request), signerName) {
|
||||
return nil, field.Forbidden(field.NewPath("status.conditions"),
|
||||
fmt.Sprintf("user %q does not have permissions to set approved/denied conditions for issuer %v", request.UserInfo.Username, cr.Spec.IssuerRef))
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// approvalConditionsHaveChanged returns true if either the Approved or Denied conditions
|
||||
// have been added to the CertificateRequest.
|
||||
func approvalConditionsHaveChanged(oldCR, cr *certmanager.CertificateRequest) bool {
|
||||
oldCRApproving := util.GetCertificateRequestCondition(oldCR.Status.Conditions, certmanager.CertificateRequestConditionApproved)
|
||||
newCRApproving := util.GetCertificateRequestCondition(cr.Status.Conditions, certmanager.CertificateRequestConditionApproved)
|
||||
oldCRDenying := util.GetCertificateRequestCondition(oldCR.Status.Conditions, certmanager.CertificateRequestConditionDenied)
|
||||
newCRDenying := util.GetCertificateRequestCondition(cr.Status.Conditions, certmanager.CertificateRequestConditionDenied)
|
||||
return (oldCRApproving == nil && newCRApproving != nil) || (oldCRDenying == nil && newCRDenying != nil)
|
||||
}
|
||||
|
||||
// apiResourceForGroupKind returns the metav1.APIResource descriptor for a given GroupKind.
|
||||
// This is required to properly construct the `signerName` used as part of validating
|
||||
// requests that approve or deny the CertificateRequest.
|
||||
// namespaced will be true if the resource is namespaced.
|
||||
// 'resource' may be nil even if err is also nil.
|
||||
func (c *certificateRequestApproval) apiResourceForGroupKind(groupKind schema.GroupKind) (resource *metav1.APIResource, err error) {
|
||||
// fast path if resource is in the cache already
|
||||
if resource := c.readAPIResourceFromCache(groupKind); resource != nil {
|
||||
return resource, nil
|
||||
}
|
||||
|
||||
// otherwise, query the apiserver
|
||||
// TODO: we should enhance caching here to avoid performing discovery queries
|
||||
// many times if many CertificateRequest resources exist that reference
|
||||
// a resource that doesn't exist
|
||||
groups, err := c.discovery.ServerGroups()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, apiGroup := range groups.Groups {
|
||||
if apiGroup.Name != groupKind.Group {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, version := range apiGroup.Versions {
|
||||
apiResources, err := c.discovery.ServerResourcesForGroupVersion(version.GroupVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, resource := range apiResources.APIResources {
|
||||
if resource.Kind != groupKind.Kind {
|
||||
continue
|
||||
}
|
||||
|
||||
r := resource.DeepCopy()
|
||||
// the Group field is not always populated in responses, so explicitly set it
|
||||
r.Group = apiGroup.Name
|
||||
c.cacheAPIResource(groupKind, *r)
|
||||
return r, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errNoResourceExists
|
||||
}
|
||||
|
||||
func (c *certificateRequestApproval) readAPIResourceFromCache(groupKind schema.GroupKind) *metav1.APIResource {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
if resource, ok := c.resourceCache[groupKind]; ok {
|
||||
return &resource
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *certificateRequestApproval) cacheAPIResource(groupKind schema.GroupKind, resource metav1.APIResource) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.resourceCache[groupKind] = resource
|
||||
}
|
||||
|
||||
var errNoResourceExists = fmt.Errorf("no resource registered")
|
||||
|
||||
// signerNameForAPIResource returns the computed signerName for a given API resource
|
||||
// referenced by a CertificateRequest in a namespace.
|
||||
func signerNameForAPIResource(name, namespace string, apiResource metav1.APIResource) string {
|
||||
if apiResource.Namespaced {
|
||||
return fmt.Sprintf("%s.%s/%s.%s", apiResource.Name, apiResource.Group, namespace, name)
|
||||
}
|
||||
return fmt.Sprintf("%s.%s/%s", apiResource.Name, apiResource.Group, name)
|
||||
}
|
||||
|
||||
// userInfoForRequest constructs a user.Info suitable for using with the authorizer interface
|
||||
// from an AdmissionRequest.
|
||||
func userInfoForRequest(req admissionv1.AdmissionRequest) user.Info {
|
||||
extra := make(map[string][]string)
|
||||
for k, v := range req.UserInfo.Extra {
|
||||
extra[k] = v
|
||||
}
|
||||
return &user.DefaultInfo{
|
||||
Name: req.UserInfo.Username,
|
||||
UID: req.UserInfo.UID,
|
||||
Groups: req.UserInfo.Groups,
|
||||
Extra: extra,
|
||||
}
|
||||
}
|
||||
|
||||
// isAuthorizedForSignerName checks whether an entity is authorized to 'approve' certificaterequests
|
||||
// for a given signerName.
|
||||
func isAuthorizedForSignerName(ctx context.Context, authz authorizer.Authorizer, info user.Info, signerName string) bool {
|
||||
verb := "approve"
|
||||
// First check if the user has explicit permission to 'approve' for the given signerName.
|
||||
attr := buildAttributes(info, verb, signerName)
|
||||
decision, _, err := authz.Authorize(ctx, attr)
|
||||
switch {
|
||||
case err != nil:
|
||||
return false
|
||||
case decision == authorizer.DecisionAllow:
|
||||
return true
|
||||
}
|
||||
|
||||
// If not, check if the user has wildcard permissions to 'approve' for the domain portion of the signerName, e.g.
|
||||
// 'issuers.cert-manager.io/*'.
|
||||
attr = buildWildcardAttributes(info, verb, signerName)
|
||||
decision, _, err = authz.Authorize(ctx, attr)
|
||||
switch {
|
||||
case err != nil:
|
||||
return false
|
||||
case decision == authorizer.DecisionAllow:
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func buildAttributes(info user.Info, verb, signerName string) authorizer.Attributes {
|
||||
return authorizer.AttributesRecord{
|
||||
User: info,
|
||||
Verb: verb,
|
||||
Name: signerName,
|
||||
APIGroup: "cert-manager.io",
|
||||
APIVersion: "*",
|
||||
Resource: "signers",
|
||||
ResourceRequest: true,
|
||||
}
|
||||
}
|
||||
|
||||
func buildWildcardAttributes(info user.Info, verb, signerName string) authorizer.Attributes {
|
||||
parts := strings.Split(signerName, "/")
|
||||
domain := parts[0]
|
||||
return buildAttributes(info, verb, domain+"/*")
|
||||
}
|
||||
|
||||
func (c *certificateRequestApproval) SetAuthorizer(a authorizer.Authorizer) {
|
||||
c.authorizer = a
|
||||
}
|
||||
|
||||
func (c *certificateRequestApproval) SetDiscoveryCache(discovery discovery.CachedDiscoveryInterface) {
|
||||
c.discovery = discovery
|
||||
}
|
||||
|
||||
func (c *certificateRequestApproval) ValidateInitialization() error {
|
||||
if c.authorizer == nil {
|
||||
return fmt.Errorf("authorizer not set")
|
||||
}
|
||||
if c.discovery == nil {
|
||||
return fmt.Errorf("discovery client not set")
|
||||
}
|
||||
_, _, err := c.discovery.ServerGroupsAndResources()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -0,0 +1,358 @@
|
||||
/*
|
||||
Copyright 2021 The cert-manager 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 approval
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
authnv1 "k8s.io/api/authentication/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/discovery"
|
||||
|
||||
"github.com/jetstack/cert-manager/internal/apis/certmanager"
|
||||
"github.com/jetstack/cert-manager/internal/apis/meta"
|
||||
discoveryfake "github.com/jetstack/cert-manager/test/unit/discovery"
|
||||
)
|
||||
|
||||
var (
|
||||
expNoDiscovery = discovery.DiscoveryInterface(nil)
|
||||
)
|
||||
|
||||
func TestValidate(t *testing.T) {
|
||||
baseCR := &certmanager.CertificateRequest{
|
||||
ObjectMeta: metav1.ObjectMeta{Namespace: "testns"},
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
IssuerRef: meta.ObjectReference{
|
||||
Name: "my-issuer",
|
||||
Kind: "Issuer",
|
||||
Group: "example.io",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
approvedCR := baseCR.DeepCopy()
|
||||
approvedCR.Status = certmanager.CertificateRequestStatus{
|
||||
Conditions: []certmanager.CertificateRequestCondition{
|
||||
{
|
||||
Type: certmanager.CertificateRequestConditionApproved,
|
||||
Status: meta.ConditionTrue,
|
||||
Reason: "cert-manager.io",
|
||||
Message: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var alwaysPanicAuthorizer *fakeAuthorizer
|
||||
tests := map[string]struct {
|
||||
req *admissionv1.AdmissionRequest
|
||||
oldCR, newCR *certmanager.CertificateRequest
|
||||
|
||||
authorizer *fakeAuthorizer
|
||||
discoverclient discovery.DiscoveryInterface
|
||||
|
||||
expErr error
|
||||
}{
|
||||
"if the request is not for CertificateRequest, exit nil": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Update,
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: "cert-manager.io",
|
||||
Resource: "issuers",
|
||||
},
|
||||
RequestSubResource: "status",
|
||||
},
|
||||
authorizer: alwaysPanicAuthorizer,
|
||||
discoverclient: expNoDiscovery,
|
||||
expErr: nil,
|
||||
},
|
||||
"if the request is not for cert-manager.io, exit nil": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Update,
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: "foo.cert-manager.io",
|
||||
Resource: "certificaterequests",
|
||||
},
|
||||
RequestSubResource: "status",
|
||||
},
|
||||
authorizer: alwaysPanicAuthorizer,
|
||||
expErr: nil,
|
||||
},
|
||||
"if the CertificateRequest references a signer that doesn't exist, error": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Update,
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: "cert-manager.io",
|
||||
Resource: "certificaterequests",
|
||||
},
|
||||
RequestSubResource: "status",
|
||||
},
|
||||
oldCR: baseCR,
|
||||
newCR: approvedCR,
|
||||
authorizer: alwaysPanicAuthorizer,
|
||||
discoverclient: discoveryfake.NewDiscovery().
|
||||
WithServerGroups(func() (*metav1.APIGroupList, error) {
|
||||
return &metav1.APIGroupList{}, nil
|
||||
}),
|
||||
expErr: field.Forbidden(field.NewPath("spec.issuerRef"),
|
||||
"referenced signer resource does not exist: {my-issuer Issuer example.io}"),
|
||||
},
|
||||
"if the CertificateRequest references a signer that the approver doesn't have permissions for, error": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
UserInfo: authnv1.UserInfo{
|
||||
Username: "user-1",
|
||||
},
|
||||
Operation: admissionv1.Update,
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: "cert-manager.io",
|
||||
Resource: "certificaterequests",
|
||||
},
|
||||
RequestSubResource: "status",
|
||||
},
|
||||
oldCR: baseCR,
|
||||
newCR: approvedCR,
|
||||
discoverclient: discoveryfake.NewDiscovery().
|
||||
WithServerGroups(func() (*metav1.APIGroupList, error) {
|
||||
return &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{
|
||||
{
|
||||
Name: "example.io",
|
||||
Versions: []metav1.GroupVersionForDiscovery{
|
||||
{GroupVersion: "example.io/a-version", Version: "a-version"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}).
|
||||
WithServerResourcesForGroupVersion(func(groupVersion string) (*metav1.APIResourceList, error) {
|
||||
return &metav1.APIResourceList{
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Name: "issuers",
|
||||
Namespaced: true,
|
||||
Kind: "Issuer",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
authorizer: &fakeAuthorizer{
|
||||
verb: "approve",
|
||||
allowedName: "issuers.example.io/testns.my-issuer",
|
||||
decision: authorizer.DecisionNoOpinion,
|
||||
},
|
||||
expErr: field.Forbidden(field.NewPath("status.conditions"),
|
||||
`user "user-1" does not have permissions to set approved/denied conditions for issuer {my-issuer Issuer example.io}`),
|
||||
},
|
||||
"if the CertificateRequest references a signer that the approver has permissions for, return nil": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
UserInfo: authnv1.UserInfo{
|
||||
Username: "user-1",
|
||||
},
|
||||
Operation: admissionv1.Update,
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: "cert-manager.io",
|
||||
Resource: "certificaterequests",
|
||||
},
|
||||
RequestSubResource: "status",
|
||||
},
|
||||
oldCR: baseCR,
|
||||
newCR: approvedCR,
|
||||
discoverclient: discoveryfake.NewDiscovery().
|
||||
WithServerGroups(func() (*metav1.APIGroupList, error) {
|
||||
return &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{
|
||||
{
|
||||
Name: "example.io",
|
||||
Versions: []metav1.GroupVersionForDiscovery{
|
||||
{GroupVersion: "example.io/a-version", Version: "a-version"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}).
|
||||
WithServerResourcesForGroupVersion(func(groupVersion string) (*metav1.APIResourceList, error) {
|
||||
return &metav1.APIResourceList{
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Name: "issuers",
|
||||
Namespaced: true,
|
||||
Kind: "Issuer",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
authorizer: &fakeAuthorizer{
|
||||
verb: "approve",
|
||||
allowedName: "issuers.example.io/testns.my-issuer",
|
||||
decision: authorizer.DecisionAllow,
|
||||
},
|
||||
},
|
||||
"if the CertificateRequest references a signer that the approver has permissions for the wildcard of, return nil": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
UserInfo: authnv1.UserInfo{
|
||||
Username: "user-1",
|
||||
},
|
||||
Operation: admissionv1.Update,
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: "cert-manager.io",
|
||||
Resource: "certificaterequests",
|
||||
},
|
||||
RequestSubResource: "status",
|
||||
},
|
||||
oldCR: baseCR,
|
||||
newCR: approvedCR,
|
||||
discoverclient: discoveryfake.NewDiscovery().
|
||||
WithServerGroups(func() (*metav1.APIGroupList, error) {
|
||||
return &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{
|
||||
{
|
||||
Name: "example.io",
|
||||
Versions: []metav1.GroupVersionForDiscovery{
|
||||
{GroupVersion: "example.io/a-version", Version: "a-version"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}).
|
||||
WithServerResourcesForGroupVersion(func(groupVersion string) (*metav1.APIResourceList, error) {
|
||||
return &metav1.APIResourceList{
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Name: "issuers",
|
||||
Namespaced: true,
|
||||
Kind: "Issuer",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
authorizer: &fakeAuthorizer{
|
||||
verb: "approve",
|
||||
allowedName: "issuers.example.io/*",
|
||||
decision: authorizer.DecisionAllow,
|
||||
},
|
||||
},
|
||||
"should error if the authorizer returns an error": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
UserInfo: authnv1.UserInfo{
|
||||
Username: "user-1",
|
||||
},
|
||||
Operation: admissionv1.Update,
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: "cert-manager.io",
|
||||
Resource: "certificaterequests",
|
||||
},
|
||||
RequestSubResource: "status",
|
||||
},
|
||||
oldCR: baseCR,
|
||||
newCR: approvedCR,
|
||||
discoverclient: discoveryfake.NewDiscovery().
|
||||
WithServerGroups(func() (*metav1.APIGroupList, error) {
|
||||
return &metav1.APIGroupList{
|
||||
Groups: []metav1.APIGroup{
|
||||
{
|
||||
Name: "example.io",
|
||||
Versions: []metav1.GroupVersionForDiscovery{
|
||||
{GroupVersion: "example.io/a-version", Version: "a-version"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}).
|
||||
WithServerResourcesForGroupVersion(func(groupVersion string) (*metav1.APIResourceList, error) {
|
||||
return &metav1.APIResourceList{
|
||||
APIResources: []metav1.APIResource{
|
||||
{
|
||||
Name: "issuers",
|
||||
Namespaced: true,
|
||||
Kind: "Issuer",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}),
|
||||
authorizer: &fakeAuthorizer{
|
||||
err: fmt.Errorf("authorizer error"),
|
||||
},
|
||||
expErr: field.Forbidden(field.NewPath("status.conditions"),
|
||||
`user "user-1" does not have permissions to set approved/denied conditions for issuer {my-issuer Issuer example.io}`),
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
a := NewPlugin().(*certificateRequestApproval)
|
||||
a.discovery = test.discoverclient
|
||||
if test.authorizer != nil {
|
||||
test.authorizer.t = t
|
||||
}
|
||||
a.authorizer = test.authorizer
|
||||
|
||||
warnings, err := a.Validate(context.TODO(), *test.req, test.oldCR, test.newCR)
|
||||
if len(warnings) > 0 {
|
||||
t.Errorf("expected no warnings but got: %v", warnings)
|
||||
}
|
||||
compareErrors(t, test.expErr, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeAuthorizer struct {
|
||||
t *testing.T
|
||||
verb string
|
||||
allowedName string
|
||||
decision authorizer.Decision
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||
if f.err != nil {
|
||||
return f.decision, "forced error", f.err
|
||||
}
|
||||
if a.GetVerb() != f.verb {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised verb '%s'", a.GetVerb()), nil
|
||||
}
|
||||
if a.GetAPIGroup() != "cert-manager.io" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised groupName '%s'", a.GetAPIGroup()), nil
|
||||
}
|
||||
if a.GetAPIVersion() != "*" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised apiVersion '%s'", a.GetAPIVersion()), nil
|
||||
}
|
||||
if a.GetResource() != "signers" {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource '%s'", a.GetResource()), nil
|
||||
}
|
||||
if a.GetName() != f.allowedName {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised resource name '%s'", a.GetName()), nil
|
||||
}
|
||||
if !a.IsResourceRequest() {
|
||||
return authorizer.DecisionDeny, fmt.Sprintf("unrecognised IsResourceRequest '%t'", a.IsResourceRequest()), nil
|
||||
}
|
||||
return f.decision, "", nil
|
||||
}
|
||||
|
||||
func compareErrors(t *testing.T, exp, act error) {
|
||||
if exp == nil && act == nil {
|
||||
return
|
||||
}
|
||||
if exp == nil && act != nil ||
|
||||
exp != nil && act == nil ||
|
||||
exp.Error() != act.Error() {
|
||||
t.Errorf("error not as expected. exp=%v, act=%v", exp, act)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["certificaterequest_identity.go"],
|
||||
importpath = "github.com/jetstack/cert-manager/internal/plugin/admission/certificaterequest/identity",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/apis/certmanager:go_default_library",
|
||||
"//pkg/util:go_default_library",
|
||||
"//pkg/webhook/admission:go_default_library",
|
||||
"@io_k8s_api//admission/v1:go_default_library",
|
||||
"@io_k8s_api//authentication/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/runtime:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/util/validation/field:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "go_default_test",
|
||||
srcs = ["certificaterequest_identity_test.go"],
|
||||
embed = [":go_default_library"],
|
||||
deps = [
|
||||
"//internal/apis/certmanager:go_default_library",
|
||||
"@io_k8s_api//admission/v1:go_default_library",
|
||||
"@io_k8s_api//authentication/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/apis/meta/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/util/validation/field:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@ -0,0 +1,158 @@
|
||||
/*
|
||||
Copyright 2021 The cert-manager 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 identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
"github.com/jetstack/cert-manager/internal/apis/certmanager"
|
||||
"github.com/jetstack/cert-manager/pkg/util"
|
||||
"github.com/jetstack/cert-manager/pkg/webhook/admission"
|
||||
)
|
||||
|
||||
const PluginName = "CertificateRequestIdentity"
|
||||
|
||||
type certificateRequestIdentity struct {
|
||||
*admission.Handler
|
||||
}
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission.Plugins) {
|
||||
plugins.Register(PluginName, func() (admission.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ admission.ValidationInterface = &certificateRequestIdentity{}
|
||||
var _ admission.MutationInterface = &certificateRequestIdentity{}
|
||||
|
||||
func NewPlugin() admission.Interface {
|
||||
return &certificateRequestIdentity{
|
||||
Handler: admission.NewHandler(admissionv1.Create, admissionv1.Update),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *certificateRequestIdentity) Mutate(ctx context.Context, request admissionv1.AdmissionRequest, obj runtime.Object) error {
|
||||
// Only run this admission plugin for the certificaterequests/status sub-resource
|
||||
if request.RequestResource.Group != "cert-manager.io" ||
|
||||
request.RequestResource.Resource != "certificaterequests" ||
|
||||
request.Operation != admissionv1.Create {
|
||||
return nil
|
||||
}
|
||||
|
||||
cr := obj.(*certmanager.CertificateRequest)
|
||||
cr.Spec.UID = request.UserInfo.UID
|
||||
cr.Spec.Username = request.UserInfo.Username
|
||||
cr.Spec.Groups = request.UserInfo.Groups
|
||||
cr.Spec.Extra = make(map[string][]string)
|
||||
for k, v := range request.UserInfo.Extra {
|
||||
cr.Spec.Extra[k] = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *certificateRequestIdentity) Validate(ctx context.Context, request admissionv1.AdmissionRequest, oldObj, obj runtime.Object) ([]string, error) {
|
||||
// Only run this admission plugin for CertificateRequest resources
|
||||
if request.RequestResource.Group != "cert-manager.io" ||
|
||||
request.RequestResource.Resource != "certificaterequests" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Cast the obj to a CertificateRequest
|
||||
cr, ok := obj.(*certmanager.CertificateRequest)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("internal error: object in admission request is not of type *certmanager.CertificateRequest")
|
||||
}
|
||||
|
||||
switch request.Operation {
|
||||
case admissionv1.Create:
|
||||
return nil, validateCreate(request, cr)
|
||||
case admissionv1.Update:
|
||||
oldCR, ok := oldObj.(*certmanager.CertificateRequest)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("internal error: oldObject in admission request is not of type *certmanager.CertificateRequest")
|
||||
}
|
||||
return nil, validateUpdate(oldCR, cr)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("internal error: request operation has changed - this should never be possible")
|
||||
}
|
||||
|
||||
func validateUpdate(oldCR *certmanager.CertificateRequest, cr *certmanager.CertificateRequest) error {
|
||||
fldPath := field.NewPath("spec")
|
||||
|
||||
var el field.ErrorList
|
||||
if oldCR.Spec.UID != cr.Spec.UID {
|
||||
el = append(el, field.Forbidden(fldPath.Child("uid"), "uid identity cannot be changed once set"))
|
||||
}
|
||||
if oldCR.Spec.Username != cr.Spec.Username {
|
||||
el = append(el, field.Forbidden(fldPath.Child("username"), "username identity cannot be changed once set"))
|
||||
}
|
||||
if !util.EqualUnsorted(oldCR.Spec.Groups, cr.Spec.Groups) {
|
||||
el = append(el, field.Forbidden(fldPath.Child("groups"), "groups identity cannot be changed once set"))
|
||||
}
|
||||
if !reflect.DeepEqual(oldCR.Spec.Extra, cr.Spec.Extra) {
|
||||
el = append(el, field.Forbidden(fldPath.Child("extra"), "extra identity cannot be changed once set"))
|
||||
}
|
||||
return el.ToAggregate()
|
||||
}
|
||||
|
||||
func validateCreate(request admissionv1.AdmissionRequest, cr *certmanager.CertificateRequest) error {
|
||||
fldPath := field.NewPath("spec")
|
||||
|
||||
var el field.ErrorList
|
||||
if cr.Spec.UID != request.UserInfo.UID {
|
||||
el = append(el, field.Forbidden(fldPath.Child("uid"), "uid identity must be that of the requester"))
|
||||
}
|
||||
if cr.Spec.Username != request.UserInfo.Username {
|
||||
el = append(el, field.Forbidden(fldPath.Child("username"), "username identity must be that of the requester"))
|
||||
}
|
||||
if !util.EqualUnsorted(cr.Spec.Groups, request.UserInfo.Groups) {
|
||||
el = append(el, field.Forbidden(fldPath.Child("groups"), "groups identity must be that of the requester"))
|
||||
}
|
||||
if !extrasMatch(cr.Spec.Extra, request.UserInfo.Extra) {
|
||||
el = append(el, field.Forbidden(fldPath.Child("extra"), "extra identity must be that of the requester"))
|
||||
}
|
||||
return el.ToAggregate()
|
||||
}
|
||||
|
||||
func extrasMatch(crExtra map[string][]string, reqExtra map[string]authenticationv1.ExtraValue) bool {
|
||||
if len(crExtra) != len(reqExtra) {
|
||||
return false
|
||||
}
|
||||
|
||||
for k, v := range crExtra {
|
||||
reqv, ok := reqExtra[k]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !util.EqualUnsorted(v, reqv) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -0,0 +1,467 @@
|
||||
/*
|
||||
Copyright 2021 The cert-manager 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 identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
"github.com/jetstack/cert-manager/internal/apis/certmanager"
|
||||
)
|
||||
|
||||
var correctRequestResource = &metav1.GroupVersionResource{
|
||||
Group: "cert-manager.io",
|
||||
Version: "v1",
|
||||
Resource: "certificaterequests",
|
||||
}
|
||||
|
||||
func TestMutate(t *testing.T) {
|
||||
plugin := NewPlugin().(*certificateRequestIdentity)
|
||||
cr := &certmanager.CertificateRequest{}
|
||||
err := plugin.Mutate(context.Background(), admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Create,
|
||||
RequestResource: &metav1.GroupVersionResource{
|
||||
Group: "cert-manager.io",
|
||||
Version: "v1",
|
||||
Resource: "certificaterequests",
|
||||
},
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
Username: "testuser",
|
||||
UID: "testuid",
|
||||
Groups: []string{"testgroup"},
|
||||
Extra: map[string]authenticationv1.ExtraValue{
|
||||
"testkey": []string{"testvalue"},
|
||||
},
|
||||
}}, cr)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if cr.Spec.Username != "testuser" {
|
||||
t.Errorf("unexpected username. got: %q, expected %q", cr.Spec.UID, "testuser")
|
||||
}
|
||||
if cr.Spec.UID != "testuid" {
|
||||
t.Errorf("unexpected uid. got: %q, expected %q", cr.Spec.UID, "testuid")
|
||||
}
|
||||
if len(cr.Spec.Groups) != 1 || cr.Spec.Groups[0] != "testgroup" {
|
||||
t.Errorf("unexpected groups. got: %q, expected %q", cr.Spec.Groups, "[testgroup]")
|
||||
}
|
||||
if len(cr.Spec.Extra) != 1 || len(cr.Spec.Extra["testkey"]) != 1 || cr.Spec.Extra["testkey"][0] != "testvalue" {
|
||||
t.Errorf("unexpected uid. got: %q, expected %q", cr.Spec.Extra, "{testkey=testvalue}")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMutate_Ignores(t *testing.T) {
|
||||
plugin := NewPlugin().(*certificateRequestIdentity)
|
||||
tests := map[string]struct {
|
||||
op admissionv1.Operation
|
||||
gvr *metav1.GroupVersionResource
|
||||
}{
|
||||
"ignores if resource is not 'certificaterequests'": {
|
||||
op: admissionv1.Create,
|
||||
gvr: &metav1.GroupVersionResource{
|
||||
Group: "cert-manager.io",
|
||||
Version: "v1",
|
||||
Resource: "not-certificaterequests",
|
||||
},
|
||||
},
|
||||
"ignores if group is not 'cert-manager.io'": {
|
||||
op: admissionv1.Create,
|
||||
gvr: &metav1.GroupVersionResource{
|
||||
Group: "not-cert-manager.io",
|
||||
Version: "v1",
|
||||
Resource: "certificaterequests",
|
||||
},
|
||||
},
|
||||
"ignores if operation is not Create": {
|
||||
op: admissionv1.Update,
|
||||
gvr: &metav1.GroupVersionResource{
|
||||
Group: "cert-manager.io",
|
||||
Version: "v1",
|
||||
Resource: "certificaterequests",
|
||||
},
|
||||
},
|
||||
}
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cr := &certmanager.CertificateRequest{}
|
||||
err := plugin.Mutate(context.Background(), admissionv1.AdmissionRequest{
|
||||
Operation: test.op,
|
||||
RequestResource: test.gvr,
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
Username: "testuser",
|
||||
UID: "testuid",
|
||||
Groups: []string{"testgroup"},
|
||||
Extra: map[string]authenticationv1.ExtraValue{
|
||||
"testkey": []string{"testvalue"},
|
||||
},
|
||||
}}, cr)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if cr.Spec.UID != "" || cr.Spec.Extra != nil || cr.Spec.Username != "" || len(cr.Spec.Groups) != 0 {
|
||||
t.Errorf("unexpected mutation")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCreate(t *testing.T) {
|
||||
fldPath := field.NewPath("spec")
|
||||
|
||||
tests := map[string]struct {
|
||||
req *admissionv1.AdmissionRequest
|
||||
cr *certmanager.CertificateRequest
|
||||
wantE error
|
||||
wantW []string
|
||||
}{
|
||||
"if identity fields don't match that of requester, should fail": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Create,
|
||||
RequestResource: correctRequestResource,
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authenticationv1.ExtraValue{
|
||||
"1": []string{"abc", "efg"},
|
||||
"2": []string{"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cr: &certmanager.CertificateRequest{
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
UID: "efg",
|
||||
Username: "user-2",
|
||||
Groups: []string{"group-3", "group-4"},
|
||||
Extra: map[string][]string{
|
||||
"1": {"123", "456"},
|
||||
"2": {"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantE: field.ErrorList{
|
||||
field.Forbidden(fldPath.Child("uid"), "uid identity must be that of the requester"),
|
||||
field.Forbidden(fldPath.Child("username"), "username identity must be that of the requester"),
|
||||
field.Forbidden(fldPath.Child("groups"), "groups identity must be that of the requester"),
|
||||
field.Forbidden(fldPath.Child("extra"), "extra identity must be that of the requester"),
|
||||
}.ToAggregate(),
|
||||
},
|
||||
"if identity fields match that of requester, should pass": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Create,
|
||||
RequestResource: correctRequestResource,
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authenticationv1.ExtraValue{
|
||||
"1": []string{"abc", "efg"},
|
||||
"2": []string{"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
cr: &certmanager.CertificateRequest{
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string][]string{
|
||||
"1": {"abc", "efg"},
|
||||
"2": {"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantE: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
p := NewPlugin().(*certificateRequestIdentity)
|
||||
gotW, gotE := p.Validate(context.Background(), *test.req, nil, test.cr)
|
||||
compareErrors(t, test.wantE, gotE)
|
||||
if !reflect.DeepEqual(gotW, test.wantW) {
|
||||
t.Errorf("warnings from ValidateCreate() = %v, want %v", gotW, test.wantW)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func compareErrors(t *testing.T, exp, act error) {
|
||||
if exp == nil && act == nil {
|
||||
return
|
||||
}
|
||||
if exp == nil && act != nil ||
|
||||
exp != nil && act == nil ||
|
||||
exp.Error() != act.Error() {
|
||||
t.Errorf("error not as expected. exp=%v, act=%v", exp, act)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateUpdate(t *testing.T) {
|
||||
fldPath := field.NewPath("spec")
|
||||
|
||||
tests := map[string]struct {
|
||||
req *admissionv1.AdmissionRequest
|
||||
oldCR, newCR *certmanager.CertificateRequest
|
||||
wantE error
|
||||
wantW []string
|
||||
}{
|
||||
"if identity fields don't match that of the old CertificateRequest, should fail": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Update,
|
||||
RequestResource: correctRequestResource,
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authenticationv1.ExtraValue{
|
||||
"1": []string{"abc", "efg"},
|
||||
"2": []string{"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
oldCR: &certmanager.CertificateRequest{
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string][]string{
|
||||
"1": {"abc", "efg"},
|
||||
"2": {"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
newCR: &certmanager.CertificateRequest{
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
UID: "efg",
|
||||
Username: "user-2",
|
||||
Groups: []string{"group-3", "group-4"},
|
||||
Extra: map[string][]string{
|
||||
"1": {"123", "456"},
|
||||
"2": {"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantE: field.ErrorList{
|
||||
field.Forbidden(fldPath.Child("uid"), "uid identity cannot be changed once set"),
|
||||
field.Forbidden(fldPath.Child("username"), "username identity cannot be changed once set"),
|
||||
field.Forbidden(fldPath.Child("groups"), "groups identity cannot be changed once set"),
|
||||
field.Forbidden(fldPath.Child("extra"), "extra identity cannot be changed once set"),
|
||||
}.ToAggregate(),
|
||||
},
|
||||
"if identity fields match that of requester, should pass": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Update,
|
||||
RequestResource: correctRequestResource,
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authenticationv1.ExtraValue{
|
||||
"1": []string{"abc", "efg"},
|
||||
"2": []string{"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
oldCR: &certmanager.CertificateRequest{
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string][]string{
|
||||
"1": {"abc", "efg"},
|
||||
"2": {"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
newCR: &certmanager.CertificateRequest{
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string][]string{
|
||||
"1": {"abc", "efg"},
|
||||
"2": {"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantE: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
p := NewPlugin().(*certificateRequestIdentity)
|
||||
gotW, gotE := p.Validate(context.Background(), *test.req, test.oldCR, test.newCR)
|
||||
compareErrors(t, test.wantE, gotE)
|
||||
if !reflect.DeepEqual(gotW, test.wantW) {
|
||||
t.Errorf("warnings from ValidateUpdate() = %v, want %v", gotW, test.wantW)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMutateCreate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
req *admissionv1.AdmissionRequest
|
||||
existingCR, expectedCR *certmanager.CertificateRequest
|
||||
}{
|
||||
"should set the identity of CertificateRequest to that of the requester": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Create,
|
||||
RequestResource: correctRequestResource,
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authenticationv1.ExtraValue{
|
||||
"1": []string{"abc", "efg"},
|
||||
"2": []string{"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingCR: new(certmanager.CertificateRequest),
|
||||
expectedCR: &certmanager.CertificateRequest{
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string][]string{
|
||||
"1": {"abc", "efg"},
|
||||
"2": {"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"should overwrite user info fields if already present during a CREATE operation": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Create,
|
||||
RequestResource: correctRequestResource,
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authenticationv1.ExtraValue{
|
||||
"1": []string{"abc", "efg"},
|
||||
"2": []string{"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingCR: &certmanager.CertificateRequest{
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
UID: "1234",
|
||||
Username: "user-2",
|
||||
Groups: []string{"group-3", "group-4"},
|
||||
Extra: map[string][]string{
|
||||
"3": {"abc", "efg"},
|
||||
"4": {"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedCR: &certmanager.CertificateRequest{
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string][]string{
|
||||
"1": {"abc", "efg"},
|
||||
"2": {"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cr := test.existingCR.DeepCopy()
|
||||
p := NewPlugin().(*certificateRequestIdentity)
|
||||
if err := p.Mutate(context.Background(), *test.req, cr); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(test.expectedCR, cr) {
|
||||
t.Errorf("MutateCreate() = %v, want %v", cr, test.expectedCR)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMutateUpdate(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
req *admissionv1.AdmissionRequest
|
||||
existingCR, expectedCR *certmanager.CertificateRequest
|
||||
}{
|
||||
"should not overwrite user info fields during an Update operation": {
|
||||
req: &admissionv1.AdmissionRequest{
|
||||
Operation: admissionv1.Update,
|
||||
RequestResource: correctRequestResource,
|
||||
UserInfo: authenticationv1.UserInfo{
|
||||
UID: "abc",
|
||||
Username: "user-1",
|
||||
Groups: []string{"group-1", "group-2"},
|
||||
Extra: map[string]authenticationv1.ExtraValue{
|
||||
"1": []string{"abc", "efg"},
|
||||
"2": []string{"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
existingCR: &certmanager.CertificateRequest{
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
UID: "1234",
|
||||
Username: "user-2",
|
||||
Groups: []string{"group-3", "group-4"},
|
||||
Extra: map[string][]string{
|
||||
"3": {"abc", "efg"},
|
||||
"4": {"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedCR: &certmanager.CertificateRequest{
|
||||
Spec: certmanager.CertificateRequestSpec{
|
||||
UID: "1234",
|
||||
Username: "user-2",
|
||||
Groups: []string{"group-3", "group-4"},
|
||||
Extra: map[string][]string{
|
||||
"3": {"abc", "efg"},
|
||||
"4": {"efg", "abc"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
cr := test.existingCR.DeepCopy()
|
||||
p := NewPlugin().(*certificateRequestIdentity)
|
||||
if err := p.Mutate(context.Background(), *test.req, cr); err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(test.expectedCR, cr) {
|
||||
t.Errorf("MutateCreate() = %v, want %v", cr, test.expectedCR)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
34
internal/plugin/admission/resourcevalidation/BUILD.bazel
Normal file
34
internal/plugin/admission/resourcevalidation/BUILD.bazel
Normal file
@ -0,0 +1,34 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "go_default_library",
|
||||
srcs = ["plugin.go"],
|
||||
importpath = "github.com/jetstack/cert-manager/internal/plugin/admission/resourcevalidation",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = [
|
||||
"//internal/api/validation:go_default_library",
|
||||
"//internal/apis/acme/validation:go_default_library",
|
||||
"//internal/apis/certmanager/validation:go_default_library",
|
||||
"//pkg/apis/acme/v1:go_default_library",
|
||||
"//pkg/apis/certmanager/v1:go_default_library",
|
||||
"//pkg/webhook/admission:go_default_library",
|
||||
"@io_k8s_api//admission/v1:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/runtime:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/runtime/schema:go_default_library",
|
||||
"@io_k8s_apimachinery//pkg/util/validation/field:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "package-srcs",
|
||||
srcs = glob(["**"]),
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "all-srcs",
|
||||
srcs = [":package-srcs"],
|
||||
tags = ["automanaged"],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
112
internal/plugin/admission/resourcevalidation/plugin.go
Normal file
112
internal/plugin/admission/resourcevalidation/plugin.go
Normal file
@ -0,0 +1,112 @@
|
||||
/*
|
||||
Copyright 2021 The cert-manager 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 resourcevalidation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/jetstack/cert-manager/internal/api/validation"
|
||||
acmevalidation "github.com/jetstack/cert-manager/internal/apis/acme/validation"
|
||||
acmev1 "github.com/jetstack/cert-manager/pkg/apis/acme/v1"
|
||||
admission2 "github.com/jetstack/cert-manager/pkg/webhook/admission"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
cmvalidation "github.com/jetstack/cert-manager/internal/apis/certmanager/validation"
|
||||
certmanagerv1 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
|
||||
)
|
||||
|
||||
const PluginName = "ResourceValidation"
|
||||
|
||||
type resourceValidation struct {
|
||||
*admission2.Handler
|
||||
}
|
||||
|
||||
// Register registers a plugin
|
||||
func Register(plugins *admission2.Plugins) {
|
||||
plugins.Register(PluginName, func() (admission2.Interface, error) {
|
||||
return NewPlugin(), nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ admission2.ValidationInterface = &resourceValidation{}
|
||||
|
||||
var certificateGVR = certmanagerv1.SchemeGroupVersion.WithResource("certificates")
|
||||
var certificateRequestGVR = certmanagerv1.SchemeGroupVersion.WithResource("certificaterequests")
|
||||
var issuerGVR = certmanagerv1.SchemeGroupVersion.WithResource("issuers")
|
||||
var clusterIssuerGVR = certmanagerv1.SchemeGroupVersion.WithResource("clusterissuers")
|
||||
var orderGVR = acmev1.SchemeGroupVersion.WithResource("orders")
|
||||
var challengeGVR = acmev1.SchemeGroupVersion.WithResource("challenges")
|
||||
|
||||
type validateCreateFunc func(a *admissionv1.AdmissionRequest, obj runtime.Object) (field.ErrorList, validation.WarningList)
|
||||
type validateUpdateFunc func(a *admissionv1.AdmissionRequest, oldObj, obj runtime.Object) (field.ErrorList, validation.WarningList)
|
||||
|
||||
type validationPair struct {
|
||||
create validateCreateFunc
|
||||
update validateUpdateFunc
|
||||
}
|
||||
|
||||
func newValidationPair(create validateCreateFunc, update validateUpdateFunc) validationPair {
|
||||
return validationPair{create: create, update: update}
|
||||
}
|
||||
|
||||
var validationMapping = map[schema.GroupVersionResource]validationPair{
|
||||
certificateGVR: newValidationPair(cmvalidation.ValidateCertificate, cmvalidation.ValidateUpdateCertificate),
|
||||
certificateRequestGVR: newValidationPair(cmvalidation.ValidateCertificateRequest, cmvalidation.ValidateUpdateCertificateRequest),
|
||||
issuerGVR: newValidationPair(cmvalidation.ValidateIssuer, cmvalidation.ValidateUpdateIssuer),
|
||||
clusterIssuerGVR: newValidationPair(cmvalidation.ValidateClusterIssuer, cmvalidation.ValidateUpdateClusterIssuer),
|
||||
orderGVR: newValidationPair(acmevalidation.ValidateOrder, acmevalidation.ValidateOrderUpdate),
|
||||
challengeGVR: newValidationPair(acmevalidation.ValidateChallenge, acmevalidation.ValidateChallengeUpdate),
|
||||
}
|
||||
|
||||
func NewPlugin() admission2.Interface {
|
||||
return &resourceValidation{
|
||||
Handler: admission2.NewHandler(admissionv1.Create, admissionv1.Update),
|
||||
}
|
||||
}
|
||||
|
||||
func (p resourceValidation) Validate(_ context.Context, request admissionv1.AdmissionRequest, oldObj, obj runtime.Object) ([]string, error) {
|
||||
requestResource := schema.GroupVersionResource{
|
||||
Group: request.RequestResource.Group,
|
||||
Version: request.RequestResource.Version,
|
||||
Resource: request.RequestResource.Resource,
|
||||
}
|
||||
|
||||
pair, ok := validationMapping[requestResource]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch request.Operation {
|
||||
case admissionv1.Create:
|
||||
if pair.create == nil {
|
||||
return nil, nil
|
||||
}
|
||||
errs, warnings := pair.create(&request, obj)
|
||||
return warnings, errs.ToAggregate()
|
||||
case admissionv1.Update:
|
||||
if pair.update == nil {
|
||||
return nil, nil
|
||||
}
|
||||
errs, warnings := pair.update(&request, oldObj, obj)
|
||||
return warnings, errs.ToAggregate()
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
54
internal/plugin/plugins.go
Normal file
54
internal/plugin/plugins.go
Normal file
@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2021 The cert-manager 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 plugin
|
||||
|
||||
import (
|
||||
"github.com/jetstack/cert-manager/internal/plugin/admission/apideprecation"
|
||||
certificaterequestapproval "github.com/jetstack/cert-manager/internal/plugin/admission/certificaterequest/approval"
|
||||
certificaterequestidentity "github.com/jetstack/cert-manager/internal/plugin/admission/certificaterequest/identity"
|
||||
"github.com/jetstack/cert-manager/internal/plugin/admission/resourcevalidation"
|
||||
"github.com/jetstack/cert-manager/pkg/webhook/admission"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
)
|
||||
|
||||
var AllOrderedPlugins = []string{
|
||||
apideprecation.PluginName,
|
||||
resourcevalidation.PluginName,
|
||||
certificaterequestidentity.PluginName,
|
||||
certificaterequestapproval.PluginName,
|
||||
}
|
||||
|
||||
func RegisterAllPlugins(plugins *admission.Plugins) {
|
||||
apideprecation.Register(plugins)
|
||||
certificaterequestidentity.Register(plugins)
|
||||
certificaterequestapproval.Register(plugins)
|
||||
resourcevalidation.Register(plugins)
|
||||
}
|
||||
|
||||
func DefaultOnAdmissionPlugins() sets.String {
|
||||
return sets.NewString(
|
||||
apideprecation.PluginName,
|
||||
resourcevalidation.PluginName,
|
||||
certificaterequestidentity.PluginName,
|
||||
certificaterequestapproval.PluginName,
|
||||
)
|
||||
}
|
||||
|
||||
// DefaultOffAdmissionPlugins gets admission plugins off by default for the webhook.
|
||||
func DefaultOffAdmissionPlugins() sets.String {
|
||||
return sets.NewString(AllOrderedPlugins...).Difference(DefaultOnAdmissionPlugins())
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user