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 }