k8s.io/apiserver@v0.31.1/pkg/admission/plugin/webhook/request/admissionreview.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package request
    18  
    19  import (
    20  	"fmt"
    21  
    22  	admissionv1 "k8s.io/api/admission/v1"
    23  	admissionv1beta1 "k8s.io/api/admission/v1beta1"
    24  	authenticationv1 "k8s.io/api/authentication/v1"
    25  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    26  	"k8s.io/apimachinery/pkg/runtime"
    27  	"k8s.io/apimachinery/pkg/types"
    28  	"k8s.io/apimachinery/pkg/util/uuid"
    29  	"k8s.io/apiserver/pkg/admission"
    30  	"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
    31  )
    32  
    33  // AdmissionResponse contains the fields extracted from an AdmissionReview response
    34  type AdmissionResponse struct {
    35  	AuditAnnotations map[string]string
    36  	Allowed          bool
    37  	Patch            []byte
    38  	PatchType        admissionv1.PatchType
    39  	Result           *metav1.Status
    40  	Warnings         []string
    41  }
    42  
    43  // VerifyAdmissionResponse checks the validity of the provided admission review object, and returns the
    44  // audit annotations, whether the response allowed the request, any provided patch/patchType/status,
    45  // or an error if the provided admission review was not valid.
    46  func VerifyAdmissionResponse(uid types.UID, mutating bool, review runtime.Object) (*AdmissionResponse, error) {
    47  	switch r := review.(type) {
    48  	case *admissionv1.AdmissionReview:
    49  		if r.Response == nil {
    50  			return nil, fmt.Errorf("webhook response was absent")
    51  		}
    52  
    53  		// Verify UID matches
    54  		if r.Response.UID != uid {
    55  			return nil, fmt.Errorf("expected response.uid=%q, got %q", uid, r.Response.UID)
    56  		}
    57  
    58  		// Verify GVK
    59  		v1GVK := admissionv1.SchemeGroupVersion.WithKind("AdmissionReview")
    60  		if r.GroupVersionKind() != v1GVK {
    61  			return nil, fmt.Errorf("expected webhook response of %v, got %v", v1GVK.String(), r.GroupVersionKind().String())
    62  		}
    63  
    64  		patch := []byte(nil)
    65  		patchType := admissionv1.PatchType("")
    66  
    67  		if mutating {
    68  			// Ensure a mutating webhook provides both patch and patchType together
    69  			if len(r.Response.Patch) > 0 && r.Response.PatchType == nil {
    70  				return nil, fmt.Errorf("webhook returned response.patch but not response.patchType")
    71  			}
    72  			if len(r.Response.Patch) == 0 && r.Response.PatchType != nil {
    73  				return nil, fmt.Errorf("webhook returned response.patchType but not response.patch")
    74  			}
    75  			patch = r.Response.Patch
    76  			if r.Response.PatchType != nil {
    77  				patchType = *r.Response.PatchType
    78  				if len(patchType) == 0 {
    79  					return nil, fmt.Errorf("webhook returned invalid response.patchType of %q", patchType)
    80  				}
    81  			}
    82  		} else {
    83  			// Ensure a validating webhook doesn't return patch or patchType
    84  			if len(r.Response.Patch) > 0 {
    85  				return nil, fmt.Errorf("validating webhook may not return response.patch")
    86  			}
    87  			if r.Response.PatchType != nil {
    88  				return nil, fmt.Errorf("validating webhook may not return response.patchType")
    89  			}
    90  		}
    91  
    92  		return &AdmissionResponse{
    93  			AuditAnnotations: r.Response.AuditAnnotations,
    94  			Allowed:          r.Response.Allowed,
    95  			Patch:            patch,
    96  			PatchType:        patchType,
    97  			Result:           r.Response.Result,
    98  			Warnings:         r.Response.Warnings,
    99  		}, nil
   100  
   101  	case *admissionv1beta1.AdmissionReview:
   102  		if r.Response == nil {
   103  			return nil, fmt.Errorf("webhook response was absent")
   104  		}
   105  
   106  		// Response GVK and response.uid were not verified in v1beta1 handling, allow any
   107  
   108  		patch := []byte(nil)
   109  		patchType := admissionv1.PatchType("")
   110  		if mutating {
   111  			patch = r.Response.Patch
   112  			if len(r.Response.Patch) > 0 {
   113  				// patch type was not verified in v1beta1 admissionreview handling. pin to only supported version if a patch is provided.
   114  				patchType = admissionv1.PatchTypeJSONPatch
   115  			}
   116  		}
   117  
   118  		return &AdmissionResponse{
   119  			AuditAnnotations: r.Response.AuditAnnotations,
   120  			Allowed:          r.Response.Allowed,
   121  			Patch:            patch,
   122  			PatchType:        patchType,
   123  			Result:           r.Response.Result,
   124  			Warnings:         r.Response.Warnings,
   125  		}, nil
   126  
   127  	default:
   128  		return nil, fmt.Errorf("unexpected response type %T", review)
   129  	}
   130  }
   131  
   132  // CreateAdmissionObjects returns the unique request uid, the AdmissionReview object to send the webhook and to decode the response into,
   133  // or an error if the webhook does not support receiving any of the admission review versions we know to send
   134  func CreateAdmissionObjects(versionedAttributes *admission.VersionedAttributes, invocation *generic.WebhookInvocation) (uid types.UID, request, response runtime.Object, err error) {
   135  	for _, version := range invocation.Webhook.GetAdmissionReviewVersions() {
   136  		switch version {
   137  		case admissionv1.SchemeGroupVersion.Version:
   138  			uid := types.UID(uuid.NewUUID())
   139  			request := CreateV1AdmissionReview(uid, versionedAttributes, invocation)
   140  			response := &admissionv1.AdmissionReview{}
   141  			return uid, request, response, nil
   142  
   143  		case admissionv1beta1.SchemeGroupVersion.Version:
   144  			uid := types.UID(uuid.NewUUID())
   145  			request := CreateV1beta1AdmissionReview(uid, versionedAttributes, invocation)
   146  			response := &admissionv1beta1.AdmissionReview{}
   147  			return uid, request, response, nil
   148  
   149  		}
   150  	}
   151  	return "", nil, nil, fmt.Errorf("webhook does not accept known AdmissionReview versions (v1, v1beta1)")
   152  }
   153  
   154  // CreateV1AdmissionReview creates an AdmissionReview for the provided admission.Attributes
   155  func CreateV1AdmissionReview(uid types.UID, versionedAttributes *admission.VersionedAttributes, invocation *generic.WebhookInvocation) *admissionv1.AdmissionReview {
   156  	attr := versionedAttributes.Attributes
   157  	gvk := invocation.Kind
   158  	gvr := invocation.Resource
   159  	subresource := invocation.Subresource
   160  	requestGVK := attr.GetKind()
   161  	requestGVR := attr.GetResource()
   162  	requestSubResource := attr.GetSubresource()
   163  	aUserInfo := attr.GetUserInfo()
   164  	userInfo := authenticationv1.UserInfo{
   165  		Extra:    make(map[string]authenticationv1.ExtraValue),
   166  		Groups:   aUserInfo.GetGroups(),
   167  		UID:      aUserInfo.GetUID(),
   168  		Username: aUserInfo.GetName(),
   169  	}
   170  	dryRun := attr.IsDryRun()
   171  
   172  	// Convert the extra information in the user object
   173  	for key, val := range aUserInfo.GetExtra() {
   174  		userInfo.Extra[key] = authenticationv1.ExtraValue(val)
   175  	}
   176  
   177  	return &admissionv1.AdmissionReview{
   178  		Request: &admissionv1.AdmissionRequest{
   179  			UID: uid,
   180  			Kind: metav1.GroupVersionKind{
   181  				Group:   gvk.Group,
   182  				Kind:    gvk.Kind,
   183  				Version: gvk.Version,
   184  			},
   185  			Resource: metav1.GroupVersionResource{
   186  				Group:    gvr.Group,
   187  				Resource: gvr.Resource,
   188  				Version:  gvr.Version,
   189  			},
   190  			SubResource: subresource,
   191  			RequestKind: &metav1.GroupVersionKind{
   192  				Group:   requestGVK.Group,
   193  				Kind:    requestGVK.Kind,
   194  				Version: requestGVK.Version,
   195  			},
   196  			RequestResource: &metav1.GroupVersionResource{
   197  				Group:    requestGVR.Group,
   198  				Resource: requestGVR.Resource,
   199  				Version:  requestGVR.Version,
   200  			},
   201  			RequestSubResource: requestSubResource,
   202  			Name:               attr.GetName(),
   203  			Namespace:          attr.GetNamespace(),
   204  			Operation:          admissionv1.Operation(attr.GetOperation()),
   205  			UserInfo:           userInfo,
   206  			Object: runtime.RawExtension{
   207  				Object: versionedAttributes.VersionedObject,
   208  			},
   209  			OldObject: runtime.RawExtension{
   210  				Object: versionedAttributes.VersionedOldObject,
   211  			},
   212  			DryRun: &dryRun,
   213  			Options: runtime.RawExtension{
   214  				Object: attr.GetOperationOptions(),
   215  			},
   216  		},
   217  	}
   218  }
   219  
   220  // CreateV1beta1AdmissionReview creates an AdmissionReview for the provided admission.Attributes
   221  func CreateV1beta1AdmissionReview(uid types.UID, versionedAttributes *admission.VersionedAttributes, invocation *generic.WebhookInvocation) *admissionv1beta1.AdmissionReview {
   222  	attr := versionedAttributes.Attributes
   223  	gvk := invocation.Kind
   224  	gvr := invocation.Resource
   225  	subresource := invocation.Subresource
   226  	requestGVK := attr.GetKind()
   227  	requestGVR := attr.GetResource()
   228  	requestSubResource := attr.GetSubresource()
   229  	aUserInfo := attr.GetUserInfo()
   230  	userInfo := authenticationv1.UserInfo{
   231  		Extra:    make(map[string]authenticationv1.ExtraValue),
   232  		Groups:   aUserInfo.GetGroups(),
   233  		UID:      aUserInfo.GetUID(),
   234  		Username: aUserInfo.GetName(),
   235  	}
   236  	dryRun := attr.IsDryRun()
   237  
   238  	// Convert the extra information in the user object
   239  	for key, val := range aUserInfo.GetExtra() {
   240  		userInfo.Extra[key] = authenticationv1.ExtraValue(val)
   241  	}
   242  
   243  	return &admissionv1beta1.AdmissionReview{
   244  		Request: &admissionv1beta1.AdmissionRequest{
   245  			UID: uid,
   246  			Kind: metav1.GroupVersionKind{
   247  				Group:   gvk.Group,
   248  				Kind:    gvk.Kind,
   249  				Version: gvk.Version,
   250  			},
   251  			Resource: metav1.GroupVersionResource{
   252  				Group:    gvr.Group,
   253  				Resource: gvr.Resource,
   254  				Version:  gvr.Version,
   255  			},
   256  			SubResource: subresource,
   257  			RequestKind: &metav1.GroupVersionKind{
   258  				Group:   requestGVK.Group,
   259  				Kind:    requestGVK.Kind,
   260  				Version: requestGVK.Version,
   261  			},
   262  			RequestResource: &metav1.GroupVersionResource{
   263  				Group:    requestGVR.Group,
   264  				Resource: requestGVR.Resource,
   265  				Version:  requestGVR.Version,
   266  			},
   267  			RequestSubResource: requestSubResource,
   268  			Name:               attr.GetName(),
   269  			Namespace:          attr.GetNamespace(),
   270  			Operation:          admissionv1beta1.Operation(attr.GetOperation()),
   271  			UserInfo:           userInfo,
   272  			Object: runtime.RawExtension{
   273  				Object: versionedAttributes.VersionedObject,
   274  			},
   275  			OldObject: runtime.RawExtension{
   276  				Object: versionedAttributes.VersionedOldObject,
   277  			},
   278  			DryRun: &dryRun,
   279  			Options: runtime.RawExtension{
   280  				Object: attr.GetOperationOptions(),
   281  			},
   282  		},
   283  	}
   284  }