mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-11-14 10:20:20 +00:00
253 lines
6.8 KiB
Go
253 lines
6.8 KiB
Go
|
// 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)
|
||
|
}
|