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  }