github.com/oam-dev/cluster-gateway@v1.9.0/pkg/apis/cluster/v1alpha1/clustergateway_proxy.go (about)

     1  /*
     2  Licensed under the Apache License, Version 2.0 (the "License");
     3  you may not use this file except in compliance with the License.
     4  You may obtain a copy of the License at
     5  
     6      http://www.apache.org/licenses/LICENSE-2.0
     7  
     8  Unless required by applicable law or agreed to in writing, software
     9  distributed under the License is distributed on an "AS IS" BASIS,
    10  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  See the License for the specific language governing permissions and
    12  limitations under the License.
    13  */
    14  
    15  package v1alpha1
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"net"
    21  	"net/http"
    22  	"net/url"
    23  	"os"
    24  	gopath "path"
    25  	"regexp"
    26  	"strings"
    27  	"time"
    28  
    29  	"k8s.io/apiserver/pkg/server"
    30  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    31  	"k8s.io/klog/v2"
    32  	"k8s.io/utils/strings/slices"
    33  
    34  	"github.com/oam-dev/cluster-gateway/pkg/config"
    35  	"github.com/oam-dev/cluster-gateway/pkg/featuregates"
    36  	"github.com/oam-dev/cluster-gateway/pkg/metrics"
    37  
    38  	"github.com/pkg/errors"
    39  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    40  	"k8s.io/apimachinery/pkg/runtime"
    41  	utilnet "k8s.io/apimachinery/pkg/util/net"
    42  	apiproxy "k8s.io/apimachinery/pkg/util/proxy"
    43  	"k8s.io/apimachinery/pkg/util/sets"
    44  	"k8s.io/apiserver/pkg/authorization/authorizer"
    45  	"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
    46  	"k8s.io/apiserver/pkg/endpoints/request"
    47  	registryrest "k8s.io/apiserver/pkg/registry/rest"
    48  	restclient "k8s.io/client-go/rest"
    49  	"k8s.io/client-go/transport"
    50  	"sigs.k8s.io/apiserver-runtime/pkg/builder/resource"
    51  	"sigs.k8s.io/apiserver-runtime/pkg/builder/resource/resourcerest"
    52  	contextutil "sigs.k8s.io/apiserver-runtime/pkg/util/context"
    53  	"sigs.k8s.io/apiserver-runtime/pkg/util/loopback"
    54  )
    55  
    56  var _ resource.SubResource = &ClusterGatewayProxy{}
    57  var _ registryrest.Storage = &ClusterGatewayProxy{}
    58  var _ resourcerest.Connecter = &ClusterGatewayProxy{}
    59  
    60  var proxyMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}
    61  
    62  // ClusterGatewayProxy is a subresource for ClusterGateway which allows user to proxy
    63  // kubernetes resource requests to the managed cluster.
    64  type ClusterGatewayProxy struct {
    65  }
    66  
    67  // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
    68  type ClusterGatewayProxyOptions struct {
    69  	metav1.TypeMeta
    70  
    71  	// Path is the target api path of the proxy request.
    72  	// e.g. "/healthz", "/api/v1"
    73  	Path string `json:"path"`
    74  
    75  	// Impersonate indicates whether to impersonate as the original
    76  	// user identity from the request context after proxying to the
    77  	// target cluster.
    78  	// Note that this will requires additional RBAC settings inside
    79  	// the target cluster for the impersonated users (i.e. the end-
    80  	// user using the proxy subresource.).
    81  	Impersonate bool `json:"impersonate"`
    82  }
    83  
    84  func (c *ClusterGatewayProxy) SubResourceName() string {
    85  	return "proxy"
    86  }
    87  
    88  func (c *ClusterGatewayProxy) New() runtime.Object {
    89  	return &ClusterGatewayProxyOptions{}
    90  }
    91  
    92  func (in *ClusterGatewayProxy) Destroy() {}
    93  
    94  func (c *ClusterGatewayProxy) Connect(ctx context.Context, id string, options runtime.Object, r registryrest.Responder) (http.Handler, error) {
    95  	ts := time.Now()
    96  
    97  	proxyOpts, ok := options.(*ClusterGatewayProxyOptions)
    98  	if !ok {
    99  		return nil, fmt.Errorf("invalid options object: %#v", options)
   100  	}
   101  
   102  	parentStorage, ok := contextutil.GetParentStorageGetter(ctx)
   103  	if !ok {
   104  		return nil, fmt.Errorf("no parent storage found")
   105  	}
   106  	parentObj, err := parentStorage.Get(ctx, id, &metav1.GetOptions{})
   107  	if err != nil {
   108  		return nil, fmt.Errorf("no such cluster %v", id)
   109  	}
   110  	clusterGateway := parentObj.(*ClusterGateway)
   111  
   112  	reqInfo, _ := request.RequestInfoFrom(ctx)
   113  	factory := request.RequestInfoFactory{
   114  		APIPrefixes:          sets.NewString("api", "apis"),
   115  		GrouplessAPIPrefixes: sets.NewString("api"),
   116  	}
   117  	proxyReqInfo, _ := factory.NewRequestInfo(&http.Request{
   118  		URL: &url.URL{
   119  			Path: proxyOpts.Path,
   120  		},
   121  		Method: strings.ToUpper(reqInfo.Verb),
   122  	})
   123  	proxyReqInfo.Verb = reqInfo.Verb
   124  
   125  	if config.AuthorizateProxySubpath {
   126  		user, _ := request.UserFrom(ctx)
   127  		var attr authorizer.Attributes
   128  		if proxyReqInfo.IsResourceRequest {
   129  			attr = authorizer.AttributesRecord{
   130  				User:        user,
   131  				APIGroup:    proxyReqInfo.APIGroup,
   132  				APIVersion:  proxyReqInfo.APIVersion,
   133  				Resource:    proxyReqInfo.Resource,
   134  				Subresource: proxyReqInfo.Subresource,
   135  				Namespace:   proxyReqInfo.Namespace,
   136  				Name:        proxyReqInfo.Name,
   137  				Verb:        proxyReqInfo.Verb,
   138  			}
   139  		} else {
   140  			path, _ := url.ParseRequestURI(proxyReqInfo.Path)
   141  			attr = authorizer.AttributesRecord{
   142  				User: user,
   143  				Path: path.Path,
   144  				Verb: proxyReqInfo.Verb,
   145  			}
   146  		}
   147  
   148  		decision, reason, err := loopback.GetAuthorizer().Authorize(ctx, attr)
   149  		if err != nil {
   150  			return nil, errors.Wrapf(err, "authorization failed due to %s", reason)
   151  		}
   152  		if decision != authorizer.DecisionAllow {
   153  			return nil, fmt.Errorf("proxying by user %v is forbidden authorization failed", user.GetName())
   154  		}
   155  	}
   156  
   157  	return &proxyHandler{
   158  		parentName:     id,
   159  		path:           proxyOpts.Path,
   160  		impersonate:    proxyOpts.Impersonate,
   161  		clusterGateway: clusterGateway,
   162  		responder:      r,
   163  		finishFunc: func(code int) {
   164  			metrics.RecordProxiedRequestsByResource(proxyReqInfo.Resource, proxyReqInfo.Verb, code)
   165  			metrics.RecordProxiedRequestsByCluster(id, code)
   166  			metrics.RecordProxiedRequestsDuration(proxyReqInfo.Resource, proxyReqInfo.Verb, id, code, time.Since(ts))
   167  		},
   168  	}, nil
   169  }
   170  
   171  func (c *ClusterGatewayProxy) NewConnectOptions() (runtime.Object, bool, string) {
   172  	return &ClusterGatewayProxyOptions{}, true, "path"
   173  }
   174  
   175  func (c *ClusterGatewayProxy) ConnectMethods() []string {
   176  	return proxyMethods
   177  }
   178  
   179  var _ resource.QueryParameterObject = &ClusterGatewayProxyOptions{}
   180  
   181  func (in *ClusterGatewayProxyOptions) ConvertFromUrlValues(values *url.Values) error {
   182  	in.Path = values.Get("path")
   183  	in.Impersonate = values.Get("impersonate") == "true"
   184  	return nil
   185  }
   186  
   187  var _ http.Handler = &proxyHandler{}
   188  
   189  type proxyHandler struct {
   190  	parentName     string
   191  	path           string
   192  	impersonate    bool
   193  	clusterGateway *ClusterGateway
   194  	responder      registryrest.Responder
   195  	finishFunc     func(code int)
   196  }
   197  
   198  var (
   199  	apiPrefix = "/apis/" + config.MetaApiGroupName + "/" + config.MetaApiVersionName + "/clustergateways/"
   200  	apiSuffix = "/proxy"
   201  )
   202  
   203  type proxyResponseWriter struct {
   204  	http.ResponseWriter
   205  	http.Hijacker
   206  	http.Flusher
   207  	statusCode int
   208  }
   209  
   210  func (in *proxyResponseWriter) WriteHeader(statusCode int) {
   211  	in.statusCode = statusCode
   212  	in.ResponseWriter.WriteHeader(statusCode)
   213  }
   214  
   215  func newProxyResponseWriter(_writer http.ResponseWriter) *proxyResponseWriter {
   216  	writer := &proxyResponseWriter{ResponseWriter: _writer, statusCode: http.StatusOK}
   217  	writer.Hijacker, _ = _writer.(http.Hijacker)
   218  	writer.Flusher, _ = _writer.(http.Flusher)
   219  	return writer
   220  }
   221  
   222  func (p *proxyHandler) ServeHTTP(_writer http.ResponseWriter, request *http.Request) {
   223  	writer := newProxyResponseWriter(_writer)
   224  	defer func() {
   225  		p.finishFunc(writer.statusCode)
   226  	}()
   227  	cluster := p.clusterGateway
   228  	if cluster.Spec.Access.Credential == nil {
   229  		responsewriters.InternalError(writer, request, fmt.Errorf("proxying cluster %s not support due to lacking credentials", cluster.Name))
   230  		return
   231  	}
   232  
   233  	// Go 1.19 removes the URL clone in WithContext method and therefore change
   234  	// to deep copy here
   235  	newReq := request.Clone(request.Context())
   236  	newReq.Header = utilnet.CloneHeader(request.Header)
   237  	newReq.URL.Path = p.path
   238  
   239  	urlAddr, err := GetEndpointURL(cluster)
   240  	if err != nil {
   241  		responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed parsing endpoint for cluster %s", cluster.Name))
   242  		return
   243  	}
   244  	host, _, _ := net.SplitHostPort(urlAddr.Host)
   245  	path := strings.TrimPrefix(request.URL.Path, apiPrefix+p.parentName+apiSuffix)
   246  	newReq.Host = host
   247  	newReq.URL.Path = gopath.Join(urlAddr.Path, path)
   248  	newReq.URL.RawQuery = unescapeQueryValues(request.URL.Query()).Encode()
   249  	newReq.RequestURI = newReq.URL.RequestURI()
   250  
   251  	cfg, err := NewConfigFromCluster(request.Context(), cluster)
   252  	if err != nil {
   253  		responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed creating cluster proxy client config %s", cluster.Name))
   254  		return
   255  	}
   256  	if p.impersonate || utilfeature.DefaultFeatureGate.Enabled(featuregates.ClientIdentityPenetration) {
   257  		cfg.Impersonate = p.getImpersonationConfig(request)
   258  	}
   259  	rt, err := restclient.TransportFor(cfg)
   260  	if err != nil {
   261  		responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed creating cluster proxy client %s", cluster.Name))
   262  		return
   263  	}
   264  	proxy := apiproxy.NewUpgradeAwareHandler(
   265  		&url.URL{
   266  			Scheme:   urlAddr.Scheme,
   267  			Path:     newReq.URL.Path,
   268  			Host:     urlAddr.Host,
   269  			RawQuery: request.URL.RawQuery,
   270  		},
   271  		rt,
   272  		false,
   273  		false,
   274  		nil)
   275  
   276  	const defaultFlushInterval = 200 * time.Millisecond
   277  	transportCfg, err := cfg.TransportConfig()
   278  	if err != nil {
   279  		responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed creating transport config %s", cluster.Name))
   280  		return
   281  	}
   282  	tlsConfig, err := transport.TLSConfigFor(transportCfg)
   283  	if err != nil {
   284  		responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed creating tls config %s", cluster.Name))
   285  		return
   286  	}
   287  	upgrader, err := transport.HTTPWrappersForConfig(transportCfg, apiproxy.MirrorRequest)
   288  	if err != nil {
   289  		responsewriters.InternalError(writer, request, errors.Wrapf(err, "failed creating upgrader client %s", cluster.Name))
   290  		return
   291  	}
   292  	upgrading := utilnet.SetOldTransportDefaults(&http.Transport{
   293  		TLSClientConfig: tlsConfig,
   294  		DialContext:     cfg.Dial,
   295  	})
   296  	proxy.UpgradeTransport = apiproxy.NewUpgradeRequestRoundTripper(
   297  		upgrading,
   298  		RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
   299  			newReq := utilnet.CloneRequest(req)
   300  			return upgrader.RoundTrip(newReq)
   301  		}))
   302  	proxy.Transport = rt
   303  	proxy.FlushInterval = defaultFlushInterval
   304  	proxy.Responder = ErrorResponderFunc(func(w http.ResponseWriter, req *http.Request, err error) {
   305  		p.responder.Error(err)
   306  	})
   307  	proxy.ServeHTTP(writer, newReq)
   308  }
   309  
   310  type noSuppressPanicError struct{}
   311  
   312  func (noSuppressPanicError) Write(p []byte) (n int, err error) {
   313  	// skip "suppressing panic for copyResponse error in test; copy error" error message
   314  	// that ends up in CI tests on each kube-apiserver termination as noise and
   315  	// everybody thinks this is fatal.
   316  	if strings.Contains(string(p), "suppressing panic") {
   317  		return len(p), nil
   318  	}
   319  	return os.Stderr.Write(p)
   320  }
   321  
   322  // +k8s:deepcopy-gen=false
   323  type RoundTripperFunc func(req *http.Request) (*http.Response, error)
   324  
   325  func (fn RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
   326  	return fn(req)
   327  }
   328  
   329  var _ apiproxy.ErrorResponder = ErrorResponderFunc(nil)
   330  
   331  // +k8s:deepcopy-gen=false
   332  type ErrorResponderFunc func(w http.ResponseWriter, req *http.Request, err error)
   333  
   334  func (e ErrorResponderFunc) Error(w http.ResponseWriter, req *http.Request, err error) {
   335  	e(w, req, err)
   336  }
   337  
   338  func (p *proxyHandler) getImpersonationConfig(req *http.Request) restclient.ImpersonationConfig {
   339  	user, _ := request.UserFrom(req.Context())
   340  	if p.clusterGateway.Spec.ProxyConfig != nil {
   341  		matched, ruleName, projected, err := ExchangeIdentity(&p.clusterGateway.Spec.ProxyConfig.Spec.ClientIdentityExchanger, user, p.parentName)
   342  		if err != nil {
   343  			klog.Errorf("exchange identity with cluster config error: %w", err)
   344  		}
   345  		if matched {
   346  			klog.Infof("identity exchanged with rule `%s` in the proxy config from cluster `%s`", ruleName, p.clusterGateway.Name)
   347  			return *projected
   348  		}
   349  	}
   350  	matched, ruleName, projected, err := ExchangeIdentity(&GlobalClusterGatewayProxyConfiguration.Spec.ClientIdentityExchanger, user, p.parentName)
   351  	if err != nil {
   352  		klog.Errorf("exchange identity with global config error: %w", err)
   353  	}
   354  	if matched {
   355  		klog.Infof("identity exchanged with rule `%s` in the proxy config from global config", ruleName)
   356  		return *projected
   357  	}
   358  	return restclient.ImpersonationConfig{
   359  		UserName: user.GetName(),
   360  		Groups:   user.GetGroups(),
   361  		Extra:    user.GetExtra(),
   362  	}
   363  }
   364  
   365  // NewClusterGatewayProxyRequestEscaper wrap the base http.Handler and escape
   366  // the dryRun parameter. Otherwise, the dryRun request will be blocked by
   367  // apiserver middlewares
   368  func NewClusterGatewayProxyRequestEscaper(delegate http.Handler) http.Handler {
   369  	return &clusterGatewayProxyRequestEscaper{delegate: delegate}
   370  }
   371  
   372  type clusterGatewayProxyRequestEscaper struct {
   373  	delegate http.Handler
   374  }
   375  
   376  var (
   377  	clusterGatewayProxyPathPattern = regexp.MustCompile(strings.Join([]string{
   378  		server.APIGroupPrefix,
   379  		config.MetaApiGroupName,
   380  		config.MetaApiVersionName,
   381  		"clustergateways",
   382  		"[a-z0-9]([-a-z0-9]*[a-z0-9])?",
   383  		"proxy"}, "/"))
   384  	clusterGatewayProxyQueryKeysToEscape = []string{"dryRun"}
   385  	clusterGatewayProxyEscaperPrefix     = "__"
   386  )
   387  
   388  func (in *clusterGatewayProxyRequestEscaper) ServeHTTP(w http.ResponseWriter, req *http.Request) {
   389  	if clusterGatewayProxyPathPattern.MatchString(req.URL.Path) {
   390  		newReq := req.Clone(req.Context())
   391  		q := req.URL.Query()
   392  		for _, k := range clusterGatewayProxyQueryKeysToEscape {
   393  			if q.Has(k) {
   394  				q.Set(clusterGatewayProxyEscaperPrefix+k, q.Get(k))
   395  				q.Del(k)
   396  			}
   397  		}
   398  		newReq.URL.RawQuery = q.Encode()
   399  		req = newReq
   400  	}
   401  	in.delegate.ServeHTTP(w, req)
   402  }
   403  
   404  func unescapeQueryValues(values url.Values) url.Values {
   405  	unescaped := url.Values{}
   406  	for k, vs := range values {
   407  		if strings.HasPrefix(k, clusterGatewayProxyEscaperPrefix) &&
   408  			slices.Contains(clusterGatewayProxyQueryKeysToEscape,
   409  				strings.TrimPrefix(k, clusterGatewayProxyEscaperPrefix)) {
   410  			k = strings.TrimPrefix(k, clusterGatewayProxyEscaperPrefix)
   411  		}
   412  		unescaped[k] = vs
   413  	}
   414  	return unescaped
   415  }