cert-manager/internal/plugin/admission/certificaterequest/approval/certificaterequest_approval.go
James Munnelly 07a0171e98 Use regular discovery client instead of cache
Signed-off-by: James Munnelly <jmunnelly@apple.com>
2022-01-20 10:56:50 +00:00

286 lines
9.9 KiB
Go

/*
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"
"k8s.io/client-go/kubernetes"
"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.WantsExternalKubeClientSet = &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) SetExternalKubeClientSet(client kubernetes.Interface) {
c.discovery = client.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.ServerGroups()
if err != nil {
return err
}
return nil
}