k8s.io/apiserver@v0.31.1/plugin/pkg/authenticator/token/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 authenticator.Token interface using HTTP webhooks. 18 package webhook 19 20 import ( 21 "context" 22 "errors" 23 "fmt" 24 "strconv" 25 "time" 26 27 authenticationv1 "k8s.io/api/authentication/v1" 28 authenticationv1beta1 "k8s.io/api/authentication/v1beta1" 29 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 "k8s.io/apimachinery/pkg/util/wait" 33 "k8s.io/apiserver/pkg/authentication/authenticator" 34 "k8s.io/apiserver/pkg/authentication/user" 35 "k8s.io/apiserver/pkg/util/webhook" 36 "k8s.io/client-go/kubernetes/scheme" 37 authenticationv1client "k8s.io/client-go/kubernetes/typed/authentication/v1" 38 "k8s.io/client-go/rest" 39 "k8s.io/klog/v2" 40 ) 41 42 // DefaultRetryBackoff returns the default backoff parameters for webhook retry. 43 func DefaultRetryBackoff() *wait.Backoff { 44 backoff := webhook.DefaultRetryBackoffWithInitialDelay(500 * time.Millisecond) 45 return &backoff 46 } 47 48 // Ensure WebhookTokenAuthenticator implements the authenticator.Token interface. 49 var _ authenticator.Token = (*WebhookTokenAuthenticator)(nil) 50 51 type tokenReviewer interface { 52 Create(ctx context.Context, review *authenticationv1.TokenReview, _ metav1.CreateOptions) (*authenticationv1.TokenReview, int, error) 53 } 54 55 type WebhookTokenAuthenticator struct { 56 tokenReview tokenReviewer 57 retryBackoff wait.Backoff 58 implicitAuds authenticator.Audiences 59 requestTimeout time.Duration 60 metrics AuthenticatorMetrics 61 } 62 63 // NewFromInterface creates a webhook authenticator using the given tokenReview 64 // client. It is recommend to wrap this authenticator with the token cache 65 // authenticator implemented in 66 // k8s.io/apiserver/pkg/authentication/token/cache. 67 func NewFromInterface(tokenReview authenticationv1client.AuthenticationV1Interface, implicitAuds authenticator.Audiences, retryBackoff wait.Backoff, requestTimeout time.Duration, metrics AuthenticatorMetrics) (*WebhookTokenAuthenticator, error) { 68 tokenReviewClient := &tokenReviewV1Client{tokenReview.RESTClient()} 69 return newWithBackoff(tokenReviewClient, retryBackoff, implicitAuds, requestTimeout, metrics) 70 } 71 72 // New creates a new WebhookTokenAuthenticator from the provided rest 73 // config. It is recommend to wrap this authenticator with the token cache 74 // authenticator implemented in 75 // k8s.io/apiserver/pkg/authentication/token/cache. 76 func New(config *rest.Config, version string, implicitAuds authenticator.Audiences, retryBackoff wait.Backoff) (*WebhookTokenAuthenticator, error) { 77 tokenReview, err := tokenReviewInterfaceFromConfig(config, version, retryBackoff) 78 if err != nil { 79 return nil, err 80 } 81 return newWithBackoff(tokenReview, retryBackoff, implicitAuds, time.Duration(0), AuthenticatorMetrics{ 82 RecordRequestTotal: noopMetrics{}.RequestTotal, 83 RecordRequestLatency: noopMetrics{}.RequestLatency, 84 }) 85 } 86 87 // newWithBackoff allows tests to skip the sleep. 88 func newWithBackoff(tokenReview tokenReviewer, retryBackoff wait.Backoff, implicitAuds authenticator.Audiences, requestTimeout time.Duration, metrics AuthenticatorMetrics) (*WebhookTokenAuthenticator, error) { 89 return &WebhookTokenAuthenticator{ 90 tokenReview, 91 retryBackoff, 92 implicitAuds, 93 requestTimeout, 94 metrics, 95 }, nil 96 } 97 98 // AuthenticateToken implements the authenticator.Token interface. 99 func (w *WebhookTokenAuthenticator) AuthenticateToken(ctx context.Context, token string) (*authenticator.Response, bool, error) { 100 // We take implicit audiences of the API server at WebhookTokenAuthenticator 101 // construction time. The outline of how we validate audience here is: 102 // 103 // * if the ctx is not audience limited, don't do any audience validation. 104 // * if ctx is audience-limited, add the audiences to the tokenreview spec 105 // * if the tokenreview returns with audiences in the status that intersect 106 // with the audiences in the ctx, copy into the response and return success 107 // * if the tokenreview returns without an audience in the status, ensure 108 // the ctx audiences intersect with the implicit audiences, and set the 109 // intersection in the response. 110 // * otherwise return unauthenticated. 111 wantAuds, checkAuds := authenticator.AudiencesFrom(ctx) 112 r := &authenticationv1.TokenReview{ 113 Spec: authenticationv1.TokenReviewSpec{ 114 Token: token, 115 Audiences: wantAuds, 116 }, 117 } 118 var ( 119 result *authenticationv1.TokenReview 120 auds authenticator.Audiences 121 cancel context.CancelFunc 122 ) 123 124 // set a hard timeout if it was defined 125 // if the child has a shorter deadline then it will expire first, 126 // otherwise if the parent has a shorter deadline then the parent will expire and it will be propagate to the child 127 if w.requestTimeout > 0 { 128 ctx, cancel = context.WithTimeout(ctx, w.requestTimeout) 129 defer cancel() 130 } 131 132 // WithExponentialBackoff will return tokenreview create error (tokenReviewErr) if any. 133 if err := webhook.WithExponentialBackoff(ctx, w.retryBackoff, func() error { 134 var tokenReviewErr error 135 var statusCode int 136 137 start := time.Now() 138 result, statusCode, tokenReviewErr = w.tokenReview.Create(ctx, r, metav1.CreateOptions{}) 139 latency := time.Since(start) 140 141 if statusCode != 0 { 142 w.metrics.RecordRequestTotal(ctx, strconv.Itoa(statusCode)) 143 w.metrics.RecordRequestLatency(ctx, strconv.Itoa(statusCode), latency.Seconds()) 144 return tokenReviewErr 145 } 146 147 if tokenReviewErr != nil { 148 w.metrics.RecordRequestTotal(ctx, "<error>") 149 w.metrics.RecordRequestLatency(ctx, "<error>", latency.Seconds()) 150 } 151 return tokenReviewErr 152 }, webhook.DefaultShouldRetry); err != nil { 153 // An error here indicates bad configuration or an outage. Log for debugging. 154 klog.Errorf("Failed to make webhook authenticator request: %v", err) 155 return nil, false, err 156 } 157 158 if checkAuds { 159 gotAuds := w.implicitAuds 160 if len(result.Status.Audiences) > 0 { 161 gotAuds = result.Status.Audiences 162 } 163 auds = wantAuds.Intersect(gotAuds) 164 if len(auds) == 0 { 165 return nil, false, nil 166 } 167 } 168 169 r.Status = result.Status 170 if !r.Status.Authenticated { 171 var err error 172 if len(r.Status.Error) != 0 { 173 err = errors.New(r.Status.Error) 174 } 175 return nil, false, err 176 } 177 178 var extra map[string][]string 179 if r.Status.User.Extra != nil { 180 extra = map[string][]string{} 181 for k, v := range r.Status.User.Extra { 182 extra[k] = v 183 } 184 } 185 186 return &authenticator.Response{ 187 User: &user.DefaultInfo{ 188 Name: r.Status.User.Username, 189 UID: r.Status.User.UID, 190 Groups: r.Status.User.Groups, 191 Extra: extra, 192 }, 193 Audiences: auds, 194 }, true, nil 195 } 196 197 // tokenReviewInterfaceFromConfig builds a client from the specified kubeconfig file, 198 // and returns a TokenReviewInterface that uses that client. Note that the client submits TokenReview 199 // requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted. 200 func tokenReviewInterfaceFromConfig(config *rest.Config, version string, retryBackoff wait.Backoff) (tokenReviewer, error) { 201 localScheme := runtime.NewScheme() 202 if err := scheme.AddToScheme(localScheme); err != nil { 203 return nil, err 204 } 205 206 switch version { 207 case authenticationv1.SchemeGroupVersion.Version: 208 groupVersions := []schema.GroupVersion{authenticationv1.SchemeGroupVersion} 209 if err := localScheme.SetVersionPriority(groupVersions...); err != nil { 210 return nil, err 211 } 212 gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff) 213 if err != nil { 214 return nil, err 215 } 216 return &tokenReviewV1ClientGW{gw.RestClient}, nil 217 218 case authenticationv1beta1.SchemeGroupVersion.Version: 219 groupVersions := []schema.GroupVersion{authenticationv1beta1.SchemeGroupVersion} 220 if err := localScheme.SetVersionPriority(groupVersions...); err != nil { 221 return nil, err 222 } 223 gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, config, groupVersions, retryBackoff) 224 if err != nil { 225 return nil, err 226 } 227 return &tokenReviewV1beta1ClientGW{gw.RestClient}, nil 228 229 default: 230 return nil, fmt.Errorf( 231 "unsupported authentication webhook version %q, supported versions are %q, %q", 232 version, 233 authenticationv1.SchemeGroupVersion.Version, 234 authenticationv1beta1.SchemeGroupVersion.Version, 235 ) 236 } 237 238 } 239 240 type tokenReviewV1Client struct { 241 client rest.Interface 242 } 243 244 // Create takes the representation of a tokenReview and creates it. Returns the server's representation of the tokenReview, HTTP status code and an error, if there is any. 245 func (c *tokenReviewV1Client) Create(ctx context.Context, tokenReview *authenticationv1.TokenReview, opts metav1.CreateOptions) (result *authenticationv1.TokenReview, statusCode int, err error) { 246 result = &authenticationv1.TokenReview{} 247 248 restResult := c.client.Post(). 249 Resource("tokenreviews"). 250 VersionedParams(&opts, scheme.ParameterCodec). 251 Body(tokenReview). 252 Do(ctx) 253 254 restResult.StatusCode(&statusCode) 255 err = restResult.Into(result) 256 return 257 } 258 259 // tokenReviewV1ClientGW used by the generic webhook, doesn't specify GVR. 260 type tokenReviewV1ClientGW struct { 261 client rest.Interface 262 } 263 264 // Create takes the representation of a tokenReview and creates it. Returns the server's representation of the tokenReview, HTTP status code and an error, if there is any. 265 func (c *tokenReviewV1ClientGW) Create(ctx context.Context, tokenReview *authenticationv1.TokenReview, opts metav1.CreateOptions) (result *authenticationv1.TokenReview, statusCode int, err error) { 266 result = &authenticationv1.TokenReview{} 267 268 restResult := c.client.Post(). 269 Body(tokenReview). 270 Do(ctx) 271 272 restResult.StatusCode(&statusCode) 273 err = restResult.Into(result) 274 return 275 } 276 277 // tokenReviewV1beta1ClientGW used by the generic webhook, doesn't specify GVR. 278 type tokenReviewV1beta1ClientGW struct { 279 client rest.Interface 280 } 281 282 func (t *tokenReviewV1beta1ClientGW) Create(ctx context.Context, review *authenticationv1.TokenReview, _ metav1.CreateOptions) (*authenticationv1.TokenReview, int, error) { 283 var statusCode int 284 v1beta1Review := &authenticationv1beta1.TokenReview{Spec: v1SpecToV1beta1Spec(&review.Spec)} 285 v1beta1Result := &authenticationv1beta1.TokenReview{} 286 287 restResult := t.client.Post().Body(v1beta1Review).Do(ctx) 288 restResult.StatusCode(&statusCode) 289 err := restResult.Into(v1beta1Result) 290 if err != nil { 291 return nil, statusCode, err 292 } 293 review.Status = v1beta1StatusToV1Status(&v1beta1Result.Status) 294 return review, statusCode, nil 295 } 296 297 func v1SpecToV1beta1Spec(in *authenticationv1.TokenReviewSpec) authenticationv1beta1.TokenReviewSpec { 298 return authenticationv1beta1.TokenReviewSpec{ 299 Token: in.Token, 300 Audiences: in.Audiences, 301 } 302 } 303 304 func v1beta1StatusToV1Status(in *authenticationv1beta1.TokenReviewStatus) authenticationv1.TokenReviewStatus { 305 return authenticationv1.TokenReviewStatus{ 306 Authenticated: in.Authenticated, 307 User: v1beta1UserToV1User(in.User), 308 Audiences: in.Audiences, 309 Error: in.Error, 310 } 311 } 312 313 func v1beta1UserToV1User(u authenticationv1beta1.UserInfo) authenticationv1.UserInfo { 314 var extra map[string]authenticationv1.ExtraValue 315 if u.Extra != nil { 316 extra = make(map[string]authenticationv1.ExtraValue, len(u.Extra)) 317 for k, v := range u.Extra { 318 extra[k] = authenticationv1.ExtraValue(v) 319 } 320 } 321 return authenticationv1.UserInfo{ 322 Username: u.Username, 323 UID: u.UID, 324 Groups: u.Groups, 325 Extra: extra, 326 } 327 }