github.com/verrazzano/verrazzano@v1.7.1/authproxy/src/apiserver/apiserver.go (about) 1 // Copyright (c) 2023, Oracle and/or its affiliates. 2 // Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl. 3 4 package apiserver 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 "net/http" 11 "net/url" 12 "strings" 13 14 "github.com/hashicorp/go-retryablehttp" 15 "github.com/verrazzano/verrazzano/authproxy/internal/httputil" 16 "github.com/verrazzano/verrazzano/authproxy/src/auth" 17 "github.com/verrazzano/verrazzano/authproxy/src/cors" 18 "go.uber.org/zap" 19 ) 20 21 const ( 22 localClusterPrefix = "/clusters/local" 23 kubernetesAPIServerHostname = "kubernetes.default.svc.cluster.local" 24 contentTypeHeader = "Content-Type" 25 26 userImpersontaionHeader = "Impersonate-User" 27 groupImpersonationHeader = "Impersonate-Group" 28 ) 29 30 // APIRequest stores the data necessary to make a request to the API server 31 type APIRequest struct { 32 RW http.ResponseWriter 33 Request *http.Request 34 Authenticator auth.Authenticator 35 Client *retryablehttp.Client 36 APIServerURL string 37 CallbackPath string 38 BearerToken string 39 Log *zap.SugaredLogger 40 } 41 42 // ForwardAPIRequest forwards a given API request to the API server 43 func (a *APIRequest) ForwardAPIRequest() { 44 reformattedReq, err := a.preprocessAPIRequest() 45 if err != nil || reformattedReq == nil { 46 return 47 } 48 a.sendAndReturnAPIRequest(reformattedReq) 49 } 50 51 // preprocessAPIRequest validates and processes an incoming API request 52 func (a *APIRequest) preprocessAPIRequest() (*retryablehttp.Request, error) { 53 rw := a.RW 54 req := a.Request 55 56 err := validateRequest(req) 57 if err != nil { 58 a.Log.Debugf("Failed to validate request: %s", err.Error()) 59 http.Error(rw, err.Error(), http.StatusUnprocessableEntity) 60 return nil, err 61 } 62 63 ingressHost := getIngressHost(req) 64 if statusCode, err := cors.AddCORSHeaders(req, rw, ingressHost); err != nil { 65 http.Error(rw, err.Error(), statusCode) 66 return nil, err 67 } 68 69 if req.Method == http.MethodOptions { 70 rw.Header().Set("Content-Length", "0") 71 rw.WriteHeader(http.StatusOK) 72 return nil, err 73 } 74 75 a.Authenticator.SetCallbackURL(fmt.Sprintf("https://%s%s", ingressHost, a.CallbackPath)) 76 continueProcessing, err := a.Authenticator.AuthenticateRequest(req, rw) 77 if err != nil { 78 http.Error(rw, err.Error(), http.StatusUnauthorized) 79 return nil, err 80 } 81 if !continueProcessing { 82 return nil, nil 83 } 84 85 reformattedReq, err := a.reformatAPIRequest(req) 86 if err != nil { 87 http.Error(rw, "Failed to reformat request for the Kubernetes API server", http.StatusUnprocessableEntity) 88 return nil, err 89 } 90 a.Log.Debug("Outgoing request: %+v", httputil.ObfuscateRequestData(reformattedReq.Request)) 91 92 return reformattedReq, nil 93 } 94 95 // reformatAPIRequest reformats an incoming HTTP request to be sent to the Kubernetes API Server 96 func (a *APIRequest) reformatAPIRequest(req *http.Request) (*retryablehttp.Request, error) { 97 formattedReq := req.Clone(context.TODO()) 98 formattedReq.Host = kubernetesAPIServerHostname 99 formattedReq.RequestURI = "" 100 101 path := strings.Replace(req.URL.Path, localClusterPrefix, "", 1) 102 newReq, err := url.JoinPath(a.APIServerURL, path) 103 if err != nil { 104 a.Log.Errorf("Failed to format request path for path %s: %v", path, err) 105 return nil, err 106 } 107 108 formattedURL, err := url.Parse(newReq) 109 if err != nil { 110 a.Log.Errorf("Failed to format incoming url: %v", err) 111 return nil, err 112 } 113 formattedURL.RawQuery = req.URL.RawQuery 114 formattedReq.URL = formattedURL 115 116 err = setImpersonationHeaders(formattedReq) 117 if err != nil { 118 a.Log.Errorf("Failed to set impersonation headers for request: %v", err) 119 return nil, err 120 } 121 formattedReq.Header.Set("Authorization", "Bearer "+a.BearerToken) 122 123 retryableReq, err := retryablehttp.FromRequest(formattedReq) 124 if err != nil { 125 a.Log.Errorf("Failed to convert reformatted request to a retryable request: %v", err) 126 return retryableReq, err 127 } 128 129 return retryableReq, nil 130 } 131 132 // sendAndReturnAPIRequest send the reformatted request to the API server and returns the result 133 func (a *APIRequest) sendAndReturnAPIRequest(reformattedReq *retryablehttp.Request) { 134 rw := a.RW 135 136 resp, err := a.Client.Do(reformattedReq) 137 if err != nil { 138 errResponse := fmt.Sprintf("Failed to forward request to the Kubernetes API server: %s", err.Error()) 139 http.Error(rw, errResponse, http.StatusBadRequest) 140 return 141 } 142 defer func() { 143 err = resp.Body.Close() 144 if err != nil { 145 a.Log.Errorf("Failed to close response body: %v", err) 146 } 147 }() 148 149 var responseBody = io.NopCloser(strings.NewReader("")) 150 if resp != nil { 151 responseBody = resp.Body 152 } 153 154 if _, ok := resp.Header[contentTypeHeader]; ok { 155 for _, h := range resp.Header[contentTypeHeader] { 156 rw.Header().Set(contentTypeHeader, h) 157 } 158 } else { 159 bodyData, err := io.ReadAll(responseBody) 160 if err != nil { 161 a.Log.Errorf("Failed to read response body for content type detection: %v", err) 162 return 163 } 164 165 rw.Header().Set(contentTypeHeader, http.DetectContentType(bodyData)) 166 } 167 168 _, err = io.Copy(rw, responseBody) 169 if err != nil { 170 a.Log.Errorf("Failed to copy server response to read writer: %v", err) 171 return 172 } 173 } 174 175 // getIngressHost determines the ingress host from the request headers 176 func getIngressHost(req *http.Request) string { 177 if host := req.Header.Get("x-forwarded-host"); host != "" { 178 return host 179 } 180 if host := req.Header.Get("host"); host != "" { 181 return host 182 } 183 if host := req.Host; host != "" { 184 return host 185 } 186 return "invalid-hostname" 187 } 188 189 // setImpersonationHeaders sets the impersonation headers from the JWT token given a request 190 func setImpersonationHeaders(req *http.Request) error { 191 impersonationHeaders, err := auth.GetImpersonationHeadersFromRequest(req) 192 if err != nil { 193 return err 194 } 195 196 req.Header.Set(userImpersontaionHeader, impersonationHeaders.User) 197 for _, group := range impersonationHeaders.Groups { 198 req.Header.Add(groupImpersonationHeader, group) 199 } 200 return nil 201 } 202 203 // validateRequest performs request validation before the request is processed 204 func validateRequest(req *http.Request) error { 205 if !strings.HasPrefix(req.URL.Path, localClusterPrefix) { 206 return fmt.Errorf("request path: '%v' does not have expected cluster path, i.e. '/clusters/local/api/v1'", req.URL.Path) 207 } 208 return nil 209 }