k8s.io/apiserver@v0.31.1/plugin/pkg/authorizer/webhook/webhook.go (about)

     1  /*
     2  Copyright 2016 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 webhook implements the authorizer.Authorizer interface using HTTP webhooks.
    18  package webhook
    19  
    20  import (
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"net/http"
    26  	"strconv"
    27  	"time"
    28  
    29  	authorizationv1 "k8s.io/api/authorization/v1"
    30  	authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
    31  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    32  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  	"k8s.io/apimachinery/pkg/runtime/schema"
    35  	"k8s.io/apimachinery/pkg/selection"
    36  	"k8s.io/apimachinery/pkg/util/cache"
    37  	utilnet "k8s.io/apimachinery/pkg/util/net"
    38  	"k8s.io/apimachinery/pkg/util/wait"
    39  	"k8s.io/apiserver/pkg/apis/apiserver"
    40  	apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation"
    41  	"k8s.io/apiserver/pkg/authentication/user"
    42  	"k8s.io/apiserver/pkg/authorization/authorizer"
    43  	authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
    44  	genericfeatures "k8s.io/apiserver/pkg/features"
    45  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    46  	"k8s.io/apiserver/pkg/util/webhook"
    47  	"k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
    48  	"k8s.io/client-go/kubernetes/scheme"
    49  	authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
    50  	"k8s.io/client-go/rest"
    51  	"k8s.io/klog/v2"
    52  )
    53  
    54  const (
    55  	// The maximum length of requester-controlled attributes to allow caching.
    56  	maxControlledAttrCacheSize = 10000
    57  )
    58  
    59  // DefaultRetryBackoff returns the default backoff parameters for webhook retry.
    60  func DefaultRetryBackoff() *wait.Backoff {
    61  	backoff := webhook.DefaultRetryBackoffWithInitialDelay(500 * time.Millisecond)
    62  	return &backoff
    63  }
    64  
    65  // Ensure Webhook implements the authorizer.Authorizer interface.
    66  var _ authorizer.Authorizer = (*WebhookAuthorizer)(nil)
    67  
    68  type subjectAccessReviewer interface {
    69  	Create(context.Context, *authorizationv1.SubjectAccessReview, metav1.CreateOptions) (*authorizationv1.SubjectAccessReview, int, error)
    70  }
    71  
    72  type WebhookAuthorizer struct {
    73  	subjectAccessReview subjectAccessReviewer
    74  	responseCache       *cache.LRUExpireCache
    75  	authorizedTTL       time.Duration
    76  	unauthorizedTTL     time.Duration
    77  	retryBackoff        wait.Backoff
    78  	decisionOnError     authorizer.Decision
    79  	metrics             metrics.AuthorizerMetrics
    80  	celMatcher          *authorizationcel.CELMatcher
    81  	name                string
    82  }
    83  
    84  // NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client
    85  func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1Interface, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, metrics metrics.AuthorizerMetrics) (*WebhookAuthorizer, error) {
    86  	return newWithBackoff(&subjectAccessReviewV1Client{subjectAccessReview.RESTClient()}, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, nil, metrics, "")
    87  }
    88  
    89  // New creates a new WebhookAuthorizer from the provided kubeconfig file.
    90  // The config's cluster field is used to refer to the remote service, user refers to the returned authorizer.
    91  //
    92  //	# clusters refers to the remote service.
    93  //	clusters:
    94  //	- name: name-of-remote-authz-service
    95  //	  cluster:
    96  //	    certificate-authority: /path/to/ca.pem      # CA for verifying the remote service.
    97  //	    server: https://authz.example.com/authorize # URL of remote service to query. Must use 'https'.
    98  //
    99  //	# users refers to the API server's webhook configuration.
   100  //	users:
   101  //	- name: name-of-api-server
   102  //	  user:
   103  //	    client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
   104  //	    client-key: /path/to/key.pem          # key matching the cert
   105  //
   106  // For additional HTTP configuration, refer to the kubeconfig documentation
   107  // https://kubernetes.io/docs/user-guide/kubeconfig-file/.
   108  func New(config *rest.Config, version string, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition, name string, metrics metrics.AuthorizerMetrics) (*WebhookAuthorizer, error) {
   109  	subjectAccessReview, err := subjectAccessReviewInterfaceFromConfig(config, version, retryBackoff)
   110  	if err != nil {
   111  		return nil, err
   112  	}
   113  	return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, matchConditions, metrics, name)
   114  }
   115  
   116  // newWithBackoff allows tests to skip the sleep.
   117  func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition, am metrics.AuthorizerMetrics, name string) (*WebhookAuthorizer, error) {
   118  	// compile all expressions once in validation and save the results to be used for eval later
   119  	cm, fieldErr := apiservervalidation.ValidateAndCompileMatchConditions(matchConditions)
   120  	if err := fieldErr.ToAggregate(); err != nil {
   121  		return nil, err
   122  	}
   123  	if cm != nil {
   124  		cm.AuthorizerType = "Webhook"
   125  		cm.AuthorizerName = name
   126  		cm.Metrics = am
   127  	}
   128  	return &WebhookAuthorizer{
   129  		subjectAccessReview: subjectAccessReview,
   130  		responseCache:       cache.NewLRUExpireCache(8192),
   131  		authorizedTTL:       authorizedTTL,
   132  		unauthorizedTTL:     unauthorizedTTL,
   133  		retryBackoff:        retryBackoff,
   134  		decisionOnError:     decisionOnError,
   135  		metrics:             am,
   136  		celMatcher:          cm,
   137  		name:                name,
   138  	}, nil
   139  }
   140  
   141  // Authorize makes a REST request to the remote service describing the attempted action as a JSON
   142  // serialized api.authorization.v1beta1.SubjectAccessReview object. An example request body is
   143  // provided below.
   144  //
   145  //	{
   146  //	  "apiVersion": "authorization.k8s.io/v1beta1",
   147  //	  "kind": "SubjectAccessReview",
   148  //	  "spec": {
   149  //	    "resourceAttributes": {
   150  //	      "namespace": "kittensandponies",
   151  //	      "verb": "GET",
   152  //	      "group": "group3",
   153  //	      "resource": "pods"
   154  //	    },
   155  //	    "user": "jane",
   156  //	    "group": [
   157  //	      "group1",
   158  //	      "group2"
   159  //	    ]
   160  //	  }
   161  //	}
   162  //
   163  // The remote service is expected to fill the SubjectAccessReviewStatus field to either allow or
   164  // disallow access. A permissive response would return:
   165  //
   166  //	{
   167  //	  "apiVersion": "authorization.k8s.io/v1beta1",
   168  //	  "kind": "SubjectAccessReview",
   169  //	  "status": {
   170  //	    "allowed": true
   171  //	  }
   172  //	}
   173  //
   174  // To disallow access, the remote service would return:
   175  //
   176  //	{
   177  //	  "apiVersion": "authorization.k8s.io/v1beta1",
   178  //	  "kind": "SubjectAccessReview",
   179  //	  "status": {
   180  //	    "allowed": false,
   181  //	    "reason": "user does not have read access to the namespace"
   182  //	  }
   183  //	}
   184  //
   185  // TODO(mikedanese): We should eventually support failing closed when we
   186  // encounter an error. We are failing open now to preserve backwards compatible
   187  // behavior.
   188  func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
   189  	r := &authorizationv1.SubjectAccessReview{}
   190  	if user := attr.GetUser(); user != nil {
   191  		r.Spec = authorizationv1.SubjectAccessReviewSpec{
   192  			User:   user.GetName(),
   193  			UID:    user.GetUID(),
   194  			Groups: user.GetGroups(),
   195  			Extra:  convertToSARExtra(user.GetExtra()),
   196  		}
   197  	}
   198  
   199  	if attr.IsResourceRequest() {
   200  		r.Spec.ResourceAttributes = resourceAttributesFrom(attr)
   201  	} else {
   202  		r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
   203  			Path: attr.GetPath(),
   204  			Verb: attr.GetVerb(),
   205  		}
   206  	}
   207  	// skipping match when feature is not enabled
   208  	if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthorizationConfiguration) {
   209  		// Process Match Conditions before calling the webhook
   210  		matches, err := w.match(ctx, r)
   211  		// If at least one matchCondition evaluates to an error (but none are FALSE):
   212  		// If failurePolicy=Deny, then the webhook rejects the request
   213  		// If failurePolicy=NoOpinion, then the error is ignored and the webhook is skipped
   214  		if err != nil {
   215  			return w.decisionOnError, "", err
   216  		}
   217  		// If at least one matchCondition successfully evaluates to FALSE,
   218  		// then the webhook is skipped.
   219  		if !matches {
   220  			return authorizer.DecisionNoOpinion, "", nil
   221  		}
   222  	}
   223  	// If all evaluated successfully and ALL matchConditions evaluate to TRUE,
   224  	// then the webhook is called.
   225  	key, err := json.Marshal(r.Spec)
   226  	if err != nil {
   227  		return w.decisionOnError, "", err
   228  	}
   229  	if entry, ok := w.responseCache.Get(string(key)); ok {
   230  		r.Status = entry.(authorizationv1.SubjectAccessReviewStatus)
   231  	} else {
   232  		var result *authorizationv1.SubjectAccessReview
   233  		var metricsResult string
   234  		// WithExponentialBackoff will return SAR create error (sarErr) if any.
   235  		if err := webhook.WithExponentialBackoff(ctx, w.retryBackoff, func() error {
   236  			var sarErr error
   237  			var statusCode int
   238  
   239  			start := time.Now()
   240  			result, statusCode, sarErr = w.subjectAccessReview.Create(ctx, r, metav1.CreateOptions{})
   241  			latency := time.Since(start)
   242  
   243  			switch {
   244  			case sarErr == nil:
   245  				metricsResult = "success"
   246  			case ctx.Err() != nil:
   247  				metricsResult = "canceled"
   248  			case utilnet.IsTimeout(sarErr) || errors.Is(sarErr, context.DeadlineExceeded) || apierrors.IsTimeout(sarErr) || statusCode == http.StatusGatewayTimeout:
   249  				metricsResult = "timeout"
   250  			default:
   251  				metricsResult = "error"
   252  			}
   253  			w.metrics.RecordWebhookEvaluation(ctx, w.name, metricsResult)
   254  			w.metrics.RecordWebhookDuration(ctx, w.name, metricsResult, latency.Seconds())
   255  
   256  			if statusCode != 0 {
   257  				w.metrics.RecordRequestTotal(ctx, strconv.Itoa(statusCode))
   258  				w.metrics.RecordRequestLatency(ctx, strconv.Itoa(statusCode), latency.Seconds())
   259  				return sarErr
   260  			}
   261  
   262  			if sarErr != nil {
   263  				w.metrics.RecordRequestTotal(ctx, "<error>")
   264  				w.metrics.RecordRequestLatency(ctx, "<error>", latency.Seconds())
   265  			}
   266  
   267  			return sarErr
   268  		}, webhook.DefaultShouldRetry); err != nil {
   269  			klog.Errorf("Failed to make webhook authorizer request: %v", err)
   270  
   271  			// we're returning NoOpinion, and the parent context has not timed out or been canceled
   272  			if w.decisionOnError == authorizer.DecisionNoOpinion && ctx.Err() == nil {
   273  				w.metrics.RecordWebhookFailOpen(ctx, w.name, metricsResult)
   274  			}
   275  
   276  			return w.decisionOnError, "", err
   277  		}
   278  
   279  		r.Status = result.Status
   280  		if shouldCache(attr) {
   281  			if r.Status.Allowed {
   282  				w.responseCache.Add(string(key), r.Status, w.authorizedTTL)
   283  			} else {
   284  				w.responseCache.Add(string(key), r.Status, w.unauthorizedTTL)
   285  			}
   286  		}
   287  	}
   288  	switch {
   289  	case r.Status.Denied && r.Status.Allowed:
   290  		return authorizer.DecisionDeny, r.Status.Reason, fmt.Errorf("webhook subject access review returned both allow and deny response")
   291  	case r.Status.Denied:
   292  		return authorizer.DecisionDeny, r.Status.Reason, nil
   293  	case r.Status.Allowed:
   294  		return authorizer.DecisionAllow, r.Status.Reason, nil
   295  	default:
   296  		return authorizer.DecisionNoOpinion, r.Status.Reason, nil
   297  	}
   298  
   299  }
   300  
   301  func resourceAttributesFrom(attr authorizer.Attributes) *authorizationv1.ResourceAttributes {
   302  	ret := &authorizationv1.ResourceAttributes{
   303  		Namespace:   attr.GetNamespace(),
   304  		Verb:        attr.GetVerb(),
   305  		Group:       attr.GetAPIGroup(),
   306  		Version:     attr.GetAPIVersion(),
   307  		Resource:    attr.GetResource(),
   308  		Subresource: attr.GetSubresource(),
   309  		Name:        attr.GetName(),
   310  	}
   311  
   312  	if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors) {
   313  		// If we are able to get any requirements while parsing selectors, use them, even if there's an error.
   314  		// This is because selectors only narrow, so if a subset of selector requirements are available, the request can be allowed.
   315  		if selectorRequirements, _ := fieldSelectorToAuthorizationAPI(attr); len(selectorRequirements) > 0 {
   316  			ret.FieldSelector = &authorizationv1.FieldSelectorAttributes{
   317  				Requirements: selectorRequirements,
   318  			}
   319  		}
   320  
   321  		if selectorRequirements, _ := labelSelectorToAuthorizationAPI(attr); len(selectorRequirements) > 0 {
   322  			ret.LabelSelector = &authorizationv1.LabelSelectorAttributes{
   323  				Requirements: selectorRequirements,
   324  			}
   325  		}
   326  	}
   327  
   328  	return ret
   329  }
   330  
   331  func fieldSelectorToAuthorizationAPI(attr authorizer.Attributes) ([]metav1.FieldSelectorRequirement, error) {
   332  	requirements, getFieldSelectorErr := attr.GetFieldSelector()
   333  	if len(requirements) == 0 {
   334  		return nil, getFieldSelectorErr
   335  	}
   336  
   337  	retRequirements := []metav1.FieldSelectorRequirement{}
   338  	for _, requirement := range requirements {
   339  		retRequirement := metav1.FieldSelectorRequirement{}
   340  		switch {
   341  		case requirement.Operator == selection.Equals || requirement.Operator == selection.DoubleEquals || requirement.Operator == selection.In:
   342  			retRequirement.Operator = metav1.FieldSelectorOpIn
   343  			retRequirement.Key = requirement.Field
   344  			retRequirement.Values = []string{requirement.Value}
   345  		case requirement.Operator == selection.NotEquals || requirement.Operator == selection.NotIn:
   346  			retRequirement.Operator = metav1.FieldSelectorOpNotIn
   347  			retRequirement.Key = requirement.Field
   348  			retRequirement.Values = []string{requirement.Value}
   349  		default:
   350  			// ignore this particular requirement. since requirements are AND'd, it is safe to ignore unknown requirements
   351  			// for authorization since the resulting check will only be as broad or broader than the intended.
   352  			continue
   353  		}
   354  		retRequirements = append(retRequirements, retRequirement)
   355  	}
   356  
   357  	if len(retRequirements) == 0 {
   358  		// this means that all requirements were dropped (likely due to unknown operators), so we are checking the broader
   359  		// unrestricted action.
   360  		return nil, getFieldSelectorErr
   361  	}
   362  	return retRequirements, getFieldSelectorErr
   363  }
   364  
   365  func labelSelectorToAuthorizationAPI(attr authorizer.Attributes) ([]metav1.LabelSelectorRequirement, error) {
   366  	requirements, getLabelSelectorErr := attr.GetLabelSelector()
   367  	if len(requirements) == 0 {
   368  		return nil, getLabelSelectorErr
   369  	}
   370  
   371  	retRequirements := []metav1.LabelSelectorRequirement{}
   372  	for _, requirement := range requirements {
   373  		retRequirement := metav1.LabelSelectorRequirement{
   374  			Key: requirement.Key(),
   375  		}
   376  		if values := requirement.ValuesUnsorted(); len(values) > 0 {
   377  			retRequirement.Values = values
   378  		}
   379  		switch requirement.Operator() {
   380  		case selection.Equals, selection.DoubleEquals, selection.In:
   381  			retRequirement.Operator = metav1.LabelSelectorOpIn
   382  		case selection.NotEquals, selection.NotIn:
   383  			retRequirement.Operator = metav1.LabelSelectorOpNotIn
   384  		case selection.Exists:
   385  			retRequirement.Operator = metav1.LabelSelectorOpExists
   386  		case selection.DoesNotExist:
   387  			retRequirement.Operator = metav1.LabelSelectorOpDoesNotExist
   388  		default:
   389  			// ignore this particular requirement. since requirements are AND'd, it is safe to ignore unknown requirements
   390  			// for authorization since the resulting check will only be as broad or broader than the intended.
   391  			continue
   392  		}
   393  		retRequirements = append(retRequirements, retRequirement)
   394  	}
   395  
   396  	if len(retRequirements) == 0 {
   397  		// this means that all requirements were dropped (likely due to unknown operators), so we are checking the broader
   398  		// unrestricted action.
   399  		return nil, getLabelSelectorErr
   400  	}
   401  	return retRequirements, getLabelSelectorErr
   402  }
   403  
   404  // TODO: need to finish the method to get the rules when using webhook mode
   405  func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
   406  	var (
   407  		resourceRules    []authorizer.ResourceRuleInfo
   408  		nonResourceRules []authorizer.NonResourceRuleInfo
   409  	)
   410  	incomplete := true
   411  	return resourceRules, nonResourceRules, incomplete, fmt.Errorf("webhook authorizer does not support user rule resolution")
   412  }
   413  
   414  // Match is used to evaluate the SubjectAccessReviewSpec against
   415  // the authorizer's matchConditions in the form of cel expressions
   416  // to return match or no match found, which then is used to
   417  // determine if the webhook should be skipped.
   418  func (w *WebhookAuthorizer) match(ctx context.Context, r *authorizationv1.SubjectAccessReview) (bool, error) {
   419  	// A nil celMatcher or zero saved CompilationResults matches all requests.
   420  	if w.celMatcher == nil || w.celMatcher.CompilationResults == nil {
   421  		return true, nil
   422  	}
   423  	return w.celMatcher.Eval(ctx, r)
   424  }
   425  
   426  func convertToSARExtra(extra map[string][]string) map[string]authorizationv1.ExtraValue {
   427  	if extra == nil {
   428  		return nil
   429  	}
   430  	ret := map[string]authorizationv1.ExtraValue{}
   431  	for k, v := range extra {
   432  		ret[k] = authorizationv1.ExtraValue(v)
   433  	}
   434  
   435  	return ret
   436  }
   437  
   438  // subjectAccessReviewInterfaceFromConfig builds a client from the specified kubeconfig file,
   439  // and returns a SubjectAccessReviewInterface that uses that client. Note that the client submits SubjectAccessReview
   440  // requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted.
   441  func subjectAccessReviewInterfaceFromConfig(config *rest.Config, version string, retryBackoff wait.Backoff) (subjectAccessReviewer, error) {
   442  	localScheme := runtime.NewScheme()
   443  	if err := scheme.AddToScheme(localScheme); err != nil {
   444  		return nil, err
   445  	}
   446  
   447  	switch version {
   448  	case authorizationv1.SchemeGroupVersion.Version:
   449  		groupVersions := []schema.GroupVersion{authorizationv1.SchemeGroupVersion}
   450  		if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
   451  			return nil, err
   452  		}
   453  		gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff)
   454  		if err != nil {
   455  			return nil, err
   456  		}
   457  		return &subjectAccessReviewV1ClientGW{gw.RestClient}, nil
   458  
   459  	case authorizationv1beta1.SchemeGroupVersion.Version:
   460  		groupVersions := []schema.GroupVersion{authorizationv1beta1.SchemeGroupVersion}
   461  		if err := localScheme.SetVersionPriority(groupVersions...); err != nil {
   462  			return nil, err
   463  		}
   464  		gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff)
   465  		if err != nil {
   466  			return nil, err
   467  		}
   468  		return &subjectAccessReviewV1beta1ClientGW{gw.RestClient}, nil
   469  
   470  	default:
   471  		return nil, fmt.Errorf(
   472  			"unsupported webhook authorizer version %q, supported versions are %q, %q",
   473  			version,
   474  			authorizationv1.SchemeGroupVersion.Version,
   475  			authorizationv1beta1.SchemeGroupVersion.Version,
   476  		)
   477  	}
   478  }
   479  
   480  type subjectAccessReviewV1Client struct {
   481  	client rest.Interface
   482  }
   483  
   484  func (t *subjectAccessReviewV1Client) Create(ctx context.Context, subjectAccessReview *authorizationv1.SubjectAccessReview, opts metav1.CreateOptions) (result *authorizationv1.SubjectAccessReview, statusCode int, err error) {
   485  	result = &authorizationv1.SubjectAccessReview{}
   486  
   487  	restResult := t.client.Post().
   488  		Resource("subjectaccessreviews").
   489  		VersionedParams(&opts, scheme.ParameterCodec).
   490  		Body(subjectAccessReview).
   491  		Do(ctx)
   492  
   493  	restResult.StatusCode(&statusCode)
   494  	err = restResult.Into(result)
   495  	return
   496  }
   497  
   498  // subjectAccessReviewV1ClientGW used by the generic webhook, doesn't specify GVR.
   499  type subjectAccessReviewV1ClientGW struct {
   500  	client rest.Interface
   501  }
   502  
   503  func (t *subjectAccessReviewV1ClientGW) Create(ctx context.Context, subjectAccessReview *authorizationv1.SubjectAccessReview, _ metav1.CreateOptions) (*authorizationv1.SubjectAccessReview, int, error) {
   504  	var statusCode int
   505  	result := &authorizationv1.SubjectAccessReview{}
   506  
   507  	restResult := t.client.Post().Body(subjectAccessReview).Do(ctx)
   508  
   509  	restResult.StatusCode(&statusCode)
   510  	err := restResult.Into(result)
   511  
   512  	return result, statusCode, err
   513  }
   514  
   515  // subjectAccessReviewV1beta1ClientGW used by the generic webhook, doesn't specify GVR.
   516  type subjectAccessReviewV1beta1ClientGW struct {
   517  	client rest.Interface
   518  }
   519  
   520  func (t *subjectAccessReviewV1beta1ClientGW) Create(ctx context.Context, subjectAccessReview *authorizationv1.SubjectAccessReview, _ metav1.CreateOptions) (*authorizationv1.SubjectAccessReview, int, error) {
   521  	var statusCode int
   522  	v1beta1Review := &authorizationv1beta1.SubjectAccessReview{Spec: v1SpecToV1beta1Spec(&subjectAccessReview.Spec)}
   523  	v1beta1Result := &authorizationv1beta1.SubjectAccessReview{}
   524  
   525  	restResult := t.client.Post().Body(v1beta1Review).Do(ctx)
   526  
   527  	restResult.StatusCode(&statusCode)
   528  	err := restResult.Into(v1beta1Result)
   529  	if err == nil {
   530  		subjectAccessReview.Status = v1beta1StatusToV1Status(&v1beta1Result.Status)
   531  	}
   532  	return subjectAccessReview, statusCode, err
   533  }
   534  
   535  // shouldCache determines whether it is safe to cache the given request attributes. If the
   536  // requester-controlled attributes are too large, this may be a DoS attempt, so we skip the cache.
   537  func shouldCache(attr authorizer.Attributes) bool {
   538  	controlledAttrSize := int64(len(attr.GetNamespace())) +
   539  		int64(len(attr.GetVerb())) +
   540  		int64(len(attr.GetAPIGroup())) +
   541  		int64(len(attr.GetAPIVersion())) +
   542  		int64(len(attr.GetResource())) +
   543  		int64(len(attr.GetSubresource())) +
   544  		int64(len(attr.GetName())) +
   545  		int64(len(attr.GetPath()))
   546  	return controlledAttrSize < maxControlledAttrCacheSize
   547  }
   548  
   549  func v1beta1StatusToV1Status(in *authorizationv1beta1.SubjectAccessReviewStatus) authorizationv1.SubjectAccessReviewStatus {
   550  	return authorizationv1.SubjectAccessReviewStatus{
   551  		Allowed:         in.Allowed,
   552  		Denied:          in.Denied,
   553  		Reason:          in.Reason,
   554  		EvaluationError: in.EvaluationError,
   555  	}
   556  }
   557  
   558  func v1SpecToV1beta1Spec(in *authorizationv1.SubjectAccessReviewSpec) authorizationv1beta1.SubjectAccessReviewSpec {
   559  	return authorizationv1beta1.SubjectAccessReviewSpec{
   560  		ResourceAttributes:    v1ResourceAttributesToV1beta1ResourceAttributes(in.ResourceAttributes),
   561  		NonResourceAttributes: v1NonResourceAttributesToV1beta1NonResourceAttributes(in.NonResourceAttributes),
   562  		User:                  in.User,
   563  		Groups:                in.Groups,
   564  		Extra:                 v1ExtraToV1beta1Extra(in.Extra),
   565  		UID:                   in.UID,
   566  	}
   567  }
   568  
   569  func v1ResourceAttributesToV1beta1ResourceAttributes(in *authorizationv1.ResourceAttributes) *authorizationv1beta1.ResourceAttributes {
   570  	if in == nil {
   571  		return nil
   572  	}
   573  	return &authorizationv1beta1.ResourceAttributes{
   574  		Namespace:     in.Namespace,
   575  		Verb:          in.Verb,
   576  		Group:         in.Group,
   577  		Version:       in.Version,
   578  		Resource:      in.Resource,
   579  		Subresource:   in.Subresource,
   580  		Name:          in.Name,
   581  		FieldSelector: in.FieldSelector,
   582  		LabelSelector: in.LabelSelector,
   583  	}
   584  }
   585  
   586  func v1NonResourceAttributesToV1beta1NonResourceAttributes(in *authorizationv1.NonResourceAttributes) *authorizationv1beta1.NonResourceAttributes {
   587  	if in == nil {
   588  		return nil
   589  	}
   590  	return &authorizationv1beta1.NonResourceAttributes{
   591  		Path: in.Path,
   592  		Verb: in.Verb,
   593  	}
   594  }
   595  
   596  func v1ExtraToV1beta1Extra(in map[string]authorizationv1.ExtraValue) map[string]authorizationv1beta1.ExtraValue {
   597  	if in == nil {
   598  		return nil
   599  	}
   600  	ret := make(map[string]authorizationv1beta1.ExtraValue, len(in))
   601  	for k, v := range in {
   602  		ret[k] = authorizationv1beta1.ExtraValue(v)
   603  	}
   604  	return ret
   605  }