Add admission plugins for APIDeprecation, CertificateRequestApproval&Identity, ResourceValidation

Signed-off-by: James Munnelly <jmunnelly@apple.com>
This commit is contained in:
James Munnelly 2021-12-16 09:57:33 +00:00
parent dd560bca6a
commit 9583050538
14 changed files with 1779 additions and 0 deletions

View File

@ -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",

View 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"],
)

View 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",
],
)

View 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)
}

View File

@ -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)
}
})
}
}

View File

@ -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"],
)

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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"],
)

View File

@ -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
}

View File

@ -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)
}
})
}
}

View 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"],
)

View 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
}

View 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())
}