k8s.io/client-go@v0.22.2/transport/token_source.go (about)

     1  /*
     2  Copyright 2018 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 transport
    18  
    19  import (
    20  	"fmt"
    21  	"io/ioutil"
    22  	"net/http"
    23  	"strings"
    24  	"sync"
    25  	"time"
    26  
    27  	"golang.org/x/oauth2"
    28  
    29  	"k8s.io/klog/v2"
    30  )
    31  
    32  // TokenSourceWrapTransport returns a WrapTransport that injects bearer tokens
    33  // authentication from an oauth2.TokenSource.
    34  func TokenSourceWrapTransport(ts oauth2.TokenSource) func(http.RoundTripper) http.RoundTripper {
    35  	return func(rt http.RoundTripper) http.RoundTripper {
    36  		return &tokenSourceTransport{
    37  			base: rt,
    38  			ort: &oauth2.Transport{
    39  				Source: ts,
    40  				Base:   rt,
    41  			},
    42  		}
    43  	}
    44  }
    45  
    46  type ResettableTokenSource interface {
    47  	oauth2.TokenSource
    48  	ResetTokenOlderThan(time.Time)
    49  }
    50  
    51  // ResettableTokenSourceWrapTransport returns a WrapTransport that injects bearer tokens
    52  // authentication from an ResettableTokenSource.
    53  func ResettableTokenSourceWrapTransport(ts ResettableTokenSource) func(http.RoundTripper) http.RoundTripper {
    54  	return func(rt http.RoundTripper) http.RoundTripper {
    55  		return &tokenSourceTransport{
    56  			base: rt,
    57  			ort: &oauth2.Transport{
    58  				Source: ts,
    59  				Base:   rt,
    60  			},
    61  			src: ts,
    62  		}
    63  	}
    64  }
    65  
    66  // NewCachedFileTokenSource returns a resettable token source which reads a
    67  // token from a file at a specified path and periodically reloads it.
    68  func NewCachedFileTokenSource(path string) *cachingTokenSource {
    69  	return &cachingTokenSource{
    70  		now:    time.Now,
    71  		leeway: 10 * time.Second,
    72  		base: &fileTokenSource{
    73  			path: path,
    74  			// This period was picked because it is half of the duration between when the kubelet
    75  			// refreshes a projected service account token and when the original token expires.
    76  			// Default token lifetime is 10 minutes, and the kubelet starts refreshing at 80% of lifetime.
    77  			// This should induce re-reading at a frequency that works with the token volume source.
    78  			period: time.Minute,
    79  		},
    80  	}
    81  }
    82  
    83  // NewCachedTokenSource returns resettable token source with caching. It reads
    84  // a token from a designed TokenSource if not in cache or expired.
    85  func NewCachedTokenSource(ts oauth2.TokenSource) *cachingTokenSource {
    86  	return &cachingTokenSource{
    87  		now:  time.Now,
    88  		base: ts,
    89  	}
    90  }
    91  
    92  type tokenSourceTransport struct {
    93  	base http.RoundTripper
    94  	ort  http.RoundTripper
    95  	src  ResettableTokenSource
    96  }
    97  
    98  func (tst *tokenSourceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    99  	// This is to allow --token to override other bearer token providers.
   100  	if req.Header.Get("Authorization") != "" {
   101  		return tst.base.RoundTrip(req)
   102  	}
   103  	// record time before RoundTrip to make sure newly acquired Unauthorized
   104  	// token would not be reset. Another request from user is required to reset
   105  	// and proceed.
   106  	start := time.Now()
   107  	resp, err := tst.ort.RoundTrip(req)
   108  	if err == nil && resp != nil && resp.StatusCode == 401 && tst.src != nil {
   109  		tst.src.ResetTokenOlderThan(start)
   110  	}
   111  	return resp, err
   112  }
   113  
   114  func (tst *tokenSourceTransport) CancelRequest(req *http.Request) {
   115  	if req.Header.Get("Authorization") != "" {
   116  		tryCancelRequest(tst.base, req)
   117  		return
   118  	}
   119  	tryCancelRequest(tst.ort, req)
   120  }
   121  
   122  type fileTokenSource struct {
   123  	path   string
   124  	period time.Duration
   125  }
   126  
   127  var _ = oauth2.TokenSource(&fileTokenSource{})
   128  
   129  func (ts *fileTokenSource) Token() (*oauth2.Token, error) {
   130  	tokb, err := ioutil.ReadFile(ts.path)
   131  	if err != nil {
   132  		return nil, fmt.Errorf("failed to read token file %q: %v", ts.path, err)
   133  	}
   134  	tok := strings.TrimSpace(string(tokb))
   135  	if len(tok) == 0 {
   136  		return nil, fmt.Errorf("read empty token from file %q", ts.path)
   137  	}
   138  
   139  	return &oauth2.Token{
   140  		AccessToken: tok,
   141  		Expiry:      time.Now().Add(ts.period),
   142  	}, nil
   143  }
   144  
   145  type cachingTokenSource struct {
   146  	base   oauth2.TokenSource
   147  	leeway time.Duration
   148  
   149  	sync.RWMutex
   150  	tok *oauth2.Token
   151  	t   time.Time
   152  
   153  	// for testing
   154  	now func() time.Time
   155  }
   156  
   157  func (ts *cachingTokenSource) Token() (*oauth2.Token, error) {
   158  	now := ts.now()
   159  	// fast path
   160  	ts.RLock()
   161  	tok := ts.tok
   162  	ts.RUnlock()
   163  
   164  	if tok != nil && tok.Expiry.Add(-1*ts.leeway).After(now) {
   165  		return tok, nil
   166  	}
   167  
   168  	// slow path
   169  	ts.Lock()
   170  	defer ts.Unlock()
   171  	if tok := ts.tok; tok != nil && tok.Expiry.Add(-1*ts.leeway).After(now) {
   172  		return tok, nil
   173  	}
   174  
   175  	tok, err := ts.base.Token()
   176  	if err != nil {
   177  		if ts.tok == nil {
   178  			return nil, err
   179  		}
   180  		klog.Errorf("Unable to rotate token: %v", err)
   181  		return ts.tok, nil
   182  	}
   183  
   184  	ts.t = ts.now()
   185  	ts.tok = tok
   186  	return tok, nil
   187  }
   188  
   189  func (ts *cachingTokenSource) ResetTokenOlderThan(t time.Time) {
   190  	ts.Lock()
   191  	defer ts.Unlock()
   192  	if ts.t.Before(t) {
   193  		ts.tok = nil
   194  		ts.t = time.Time{}
   195  	}
   196  }