sigs.k8s.io/controller-runtime@v0.18.2/pkg/webhook/authentication/http.go (about) 1 /* 2 Copyright 2021 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 authentication 18 19 import ( 20 "encoding/json" 21 "errors" 22 "fmt" 23 "io" 24 "net/http" 25 26 authenticationv1 "k8s.io/api/authentication/v1" 27 authenticationv1beta1 "k8s.io/api/authentication/v1beta1" 28 "k8s.io/apimachinery/pkg/runtime" 29 "k8s.io/apimachinery/pkg/runtime/schema" 30 "k8s.io/apimachinery/pkg/runtime/serializer" 31 utilruntime "k8s.io/apimachinery/pkg/util/runtime" 32 ) 33 34 var authenticationScheme = runtime.NewScheme() 35 var authenticationCodecs = serializer.NewCodecFactory(authenticationScheme) 36 37 // The TokenReview resource mostly contains a bearer token which 38 // at most should have a few KB's of size, so we picked 1 MB to 39 // have plenty of buffer. 40 // If your use case requires larger max request sizes, please 41 // open an issue (https://github.com/kubernetes-sigs/controller-runtime/issues/new). 42 const maxRequestSize = int64(1 * 1024 * 1024) 43 44 func init() { 45 utilruntime.Must(authenticationv1.AddToScheme(authenticationScheme)) 46 utilruntime.Must(authenticationv1beta1.AddToScheme(authenticationScheme)) 47 } 48 49 var _ http.Handler = &Webhook{} 50 51 func (wh *Webhook) ServeHTTP(w http.ResponseWriter, r *http.Request) { 52 ctx := r.Context() 53 if wh.WithContextFunc != nil { 54 ctx = wh.WithContextFunc(ctx, r) 55 } 56 57 if r.Body == nil || r.Body == http.NoBody { 58 err := errors.New("request body is empty") 59 wh.getLogger(nil).Error(err, "bad request") 60 wh.writeResponse(w, Errored(err)) 61 return 62 } 63 64 defer r.Body.Close() 65 limitedReader := &io.LimitedReader{R: r.Body, N: maxRequestSize} 66 body, err := io.ReadAll(limitedReader) 67 if err != nil { 68 wh.getLogger(nil).Error(err, "unable to read the body from the incoming request") 69 wh.writeResponse(w, Errored(err)) 70 return 71 } 72 if limitedReader.N <= 0 { 73 err := fmt.Errorf("request entity is too large; limit is %d bytes", maxRequestSize) 74 wh.getLogger(nil).Error(err, "unable to read the body from the incoming request; limit reached") 75 wh.writeResponse(w, Errored(err)) 76 return 77 } 78 79 // verify the content type is accurate 80 if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { 81 err := fmt.Errorf("contentType=%s, expected application/json", contentType) 82 wh.getLogger(nil).Error(err, "unable to process a request with unknown content type") 83 wh.writeResponse(w, Errored(err)) 84 return 85 } 86 87 // Both v1 and v1beta1 TokenReview types are exactly the same, so the v1beta1 type can 88 // be decoded into the v1 type. The v1beta1 api is deprecated as of 1.19 and will be 89 // removed in authenticationv1.22. However the runtime codec's decoder guesses which type to 90 // decode into by type name if an Object's TypeMeta isn't set. By setting TypeMeta of an 91 // unregistered type to the v1 GVK, the decoder will coerce a v1beta1 TokenReview to authenticationv1. 92 // The actual TokenReview GVK will be used to write a typed response in case the 93 // webhook config permits multiple versions, otherwise this response will fail. 94 req := Request{} 95 ar := unversionedTokenReview{} 96 // avoid an extra copy 97 ar.TokenReview = &req.TokenReview 98 ar.SetGroupVersionKind(authenticationv1.SchemeGroupVersion.WithKind("TokenReview")) 99 _, actualTokRevGVK, err := authenticationCodecs.UniversalDeserializer().Decode(body, nil, &ar) 100 if err != nil { 101 wh.getLogger(nil).Error(err, "unable to decode the request") 102 wh.writeResponse(w, Errored(err)) 103 return 104 } 105 wh.getLogger(&req).V(5).Info("received request") 106 107 if req.Spec.Token == "" { 108 err := errors.New("token is empty") 109 wh.getLogger(&req).Error(err, "bad request") 110 wh.writeResponse(w, Errored(err)) 111 return 112 } 113 114 wh.writeResponseTyped(w, wh.Handle(ctx, req), actualTokRevGVK) 115 } 116 117 // writeResponse writes response to w generically, i.e. without encoding GVK information. 118 func (wh *Webhook) writeResponse(w io.Writer, response Response) { 119 wh.writeTokenResponse(w, response.TokenReview) 120 } 121 122 // writeResponseTyped writes response to w with GVK set to tokRevGVK, which is necessary 123 // if multiple TokenReview versions are permitted by the webhook. 124 func (wh *Webhook) writeResponseTyped(w io.Writer, response Response, tokRevGVK *schema.GroupVersionKind) { 125 ar := response.TokenReview 126 127 // Default to a v1 TokenReview, otherwise the API server may not recognize the request 128 // if multiple TokenReview versions are permitted by the webhook config. 129 if tokRevGVK == nil || *tokRevGVK == (schema.GroupVersionKind{}) { 130 ar.SetGroupVersionKind(authenticationv1.SchemeGroupVersion.WithKind("TokenReview")) 131 } else { 132 ar.SetGroupVersionKind(*tokRevGVK) 133 } 134 wh.writeTokenResponse(w, ar) 135 } 136 137 // writeTokenResponse writes ar to w. 138 func (wh *Webhook) writeTokenResponse(w io.Writer, ar authenticationv1.TokenReview) { 139 if err := json.NewEncoder(w).Encode(ar); err != nil { 140 wh.getLogger(nil).Error(err, "unable to encode the response") 141 wh.writeResponse(w, Errored(err)) 142 } 143 res := ar 144 wh.getLogger(nil).V(5).Info("wrote response", "requestID", res.UID, "authenticated", res.Status.Authenticated) 145 } 146 147 // unversionedTokenReview is used to decode both v1 and v1beta1 TokenReview types. 148 type unversionedTokenReview struct { 149 *authenticationv1.TokenReview 150 } 151 152 var _ runtime.Object = &unversionedTokenReview{}