ceph-csi/vendor/github.com/IBM/keyprotect-go-client/iam/iam.go

253 lines
6.8 KiB
Go
Raw Normal View History

// Copyright 2019 IBM Corp.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package iam
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"sync"
"time"
rhttp "github.com/hashicorp/go-retryablehttp"
)
// IAMTokenURL is the global endpoint URL for the IAM token service
const IAMTokenURL = "https://iam.cloud.ibm.com/oidc/token"
var (
// RetryWaitMax is the maximum time to wait between HTTP retries
RetryWaitMax = 30 * time.Second
// RetryMax is the max number of attempts to retry for failed HTTP requests
RetryMax = 4
)
type TokenSource interface {
Token() (*Token, error)
}
// CredentialFromAPIKey returns an IAMTokenSource that requests access tokens
// from the default token endpoint using an IAM API Key as the authentication mechanism
func CredentialFromAPIKey(apiKey string) *IAMTokenSource {
return &IAMTokenSource{
TokenURL: IAMTokenURL,
APIKey: apiKey,
}
}
// Token represents an IAM credential used to authorize requests to another service.
type Token struct {
AccessToken string
RefreshToken string
TokenType string
Expiry time.Time
}
func (t *Token) Valid() bool {
if t == nil || t.AccessToken == "" {
return false
}
if t.Expiry.Before(time.Now()) {
return false
}
return true
}
// jsonToken is for deserializing the token from the response body
type jsonToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int32 `json:"expires_in"`
}
// getExpireTime uses local time and the ExpiresIn offset to calculate an
// expiration time based off our local clock, which is more accurate for
// us to determine when it expires relative to our client.
// we also pad the time a bit, because long running requests can fail
// mid-request if we send a soon-to-expire token along
func (jt jsonToken) getExpireTime() time.Time {
// set the expiration time for 1 min less than the
// actual time to prevent timeout errors
return time.Now().Add(time.Duration(jt.ExpiresIn-60) * time.Second)
}
// IAMTokenSource is used to retrieve access tokens from the IAM token service.
// Most will probably want to use CredentialFromAPIKey to build an IAMTokenSource type,
// but it can also be created directly if one wishes to override the default IAM
// endpoint by setting TokenURL
type IAMTokenSource struct {
TokenURL string
APIKey string
mu sync.Mutex
t *Token
}
// Token requests an access token from IAM using the IAMTokenSource config.
func (ts *IAMTokenSource) Token() (*Token, error) {
ts.mu.Lock()
defer ts.mu.Unlock()
if ts.t.Valid() {
return ts.t, nil
}
if ts.APIKey == "" {
return nil, errors.New("iam: APIKey is empty")
}
v := url.Values{}
v.Set("grant_type", "urn:ibm:params:oauth:grant-type:apikey")
v.Set("apikey", ts.APIKey)
reqBody := []byte(v.Encode())
u, err := url.Parse(ts.TokenURL)
if err != nil {
return nil, err
}
// NewRequest will calculate Content-Length if we pass it a bytes.Buffer
// instead of a io.Reader type
bodyBuf := bytes.NewBuffer(reqBody)
request, err := rhttp.NewRequest("POST", u.String(), bodyBuf)
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
request.Header.Set("Accept", "application/json")
// use hashicorp retryable client with max wait time and attempts from module vars
client := rhttp.NewClient()
client.Logger = nil
client.RetryWaitMax = RetryWaitMax
client.RetryMax = RetryMax
client.ErrorHandler = rhttp.PassthroughErrorHandler
// need to use the go http DefaultTransport for tests to override with stubs (gock HTTP stubbing)
client.HTTPClient = &http.Client{
Timeout: time.Duration(60) * time.Second,
}
// this is the DefaultRetryPolicy but with retry on 429s as well
client.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
// do not retry on context.Canceled or context.DeadlineExceeded
if ctx.Err() != nil {
return false, ctx.Err()
}
if err != nil {
return true, err
}
// retry on connection error (code == 0), all 500s except 501, and 429s
if resp.StatusCode == 0 || (resp.StatusCode >= 500 && resp.StatusCode != 501) || resp.StatusCode == 429 {
return true, nil
}
return false, nil
}
resp, err := client.Do(request)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(resp.Body); err != nil {
return nil, err
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var iamErr Error
if err = json.Unmarshal(buf.Bytes(), &iamErr); err != nil {
return nil, err
}
iamErr.HTTPResponse = resp
return nil, iamErr
}
var jToken jsonToken
if err = json.Unmarshal(buf.Bytes(), &jToken); err != nil {
return nil, err
}
token := &Token{
AccessToken: jToken.AccessToken,
RefreshToken: jToken.RefreshToken,
TokenType: jToken.TokenType,
Expiry: jToken.getExpireTime(),
}
ts.t = token
return token, nil
}
// Error is a type to hold error information that the IAM services sends back
// when a request cannot be completed. ErrorCode, ErrorMessage, and Context.RequestID
// are probably the most useful fields. IAM will most likely ask you for the RequestID
// if you ask for support.
//
// Also of note is that the http.Response object is included in HTTPResponse for
// error handling at the higher application levels.
type Error struct {
ErrorCode string `json:"errorCode"`
ErrorMessage string `json:"errorMessage"`
Context *iamRequestContext `json:"context"`
HTTPResponse *http.Response
}
type iamRequestContext struct {
ClientIP string `json:"clientIp"`
ClusterName string `json:"clusterName"`
Host string `json:"host"`
InstanceID string `json:"instanceId"`
RequestID string `json:"requestId"`
RequestType string `json:"requestType"`
ElapsedTime string `json:"elapsedTime"`
StartTime string `json:"startTime"`
EndTime string `json:"endTime"`
ThreadID string `json:"threadId"`
URL string `json:"url"`
UserAgent string `json:"userAgent"`
Locale string `json:"locale"`
}
func (ie Error) Error() string {
reqId := ""
if ie.Context != nil {
reqId = ie.Context.RequestID
}
statusCode := 0
if ie.HTTPResponse != nil {
statusCode = ie.HTTPResponse.StatusCode
}
return fmt.Sprintf("iam.Error: HTTP %d requestId='%s' message='%s %s'",
statusCode, reqId, ie.ErrorCode, ie.ErrorMessage)
}