Skip to content

Commit

Permalink
Merge pull request #195 from softlayer/terraform
Browse files Browse the repository at this point in the history
Adds ability to refresh auth token
  • Loading branch information
allmightyspiff authored Sep 25, 2024
2 parents 2ecaf6f + eb64e7d commit 129de40
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 63 deletions.
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"files": "go.sum|^.secrets.baseline$",
"lines": null
},
"generated_at": "2024-06-06T22:18:14Z",
"generated_at": "2024-09-24T20:48:27Z",
"plugins_used": [
{
"name": "AWSKeyDetector"
Expand Down Expand Up @@ -242,7 +242,7 @@
"hashed_secret": "6f667d3e9627f5549ffeb1055ff294c34430b837",
"is_secret": false,
"is_verified": false,
"line_number": 171,
"line_number": 193,
"type": "Secret Keyword",
"verified_result": null
}
Expand Down
55 changes: 55 additions & 0 deletions examples/cmd/iam_demo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cmd

import (
"fmt"
"github.com/spf13/cobra"
"time"

"github.com/softlayer/softlayer-go/services"
"github.com/softlayer/softlayer-go/session"
)

func init() {
rootCmd.AddCommand(iamDemoCmd)
}

var iamDemoCmd = &cobra.Command{
Use: "iam-demo",
Short: "Will make 1 API call per minute and refresh API key when needed.",
RunE: func(cmd *cobra.Command, args []string) error {
return RunIamCmd(cmd, args)
},
}

func RunIamCmd(cmd *cobra.Command, args []string) error {
objectMask := "mask[id,companyName]"

// Sets up the session with authentication headers.
sess := &session.Session{
Endpoint: session.DefaultEndpoint,
IAMToken: "Bearer TOKEN",
IAMRefreshToken: "REFRESH TOKEN",
Debug: true,
}

// creates a reference to the service object (SoftLayer_Account)
service := services.GetAccountService(sess)

// Sets the mask, filter, result limit, and then makes the API call SoftLayer_Account::getHardware()

for {
account, err := service.Mask(objectMask).GetObject()
if err != nil {
fmt.Printf("======= ERROR ======")
return err
}
fmt.Printf("AccountId: %v, CompanyName: %v\n", *account.Id, *account.CompanyName)
fmt.Printf("Refreshing Token for no reason...\n")
sess.RefreshToken()
fmt.Printf("%s\n", sess.IAMToken)
fmt.Printf("Sleeping for 60s.......\n")
time.Sleep(60 * time.Second)
}

return nil
}
18 changes: 1 addition & 17 deletions tests/specialcase_test.go → generator/specialcase_test.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,4 @@
/**
* Copyright 2016 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 tests
package generator_test

import (
"github.com/softlayer/softlayer-go/datatypes"
Expand Down
14 changes: 8 additions & 6 deletions session/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ func (r *RestTransport) DoRequest(sess *Session, service string, method string,

path := buildPath(service, method, options)

resp, code, err := sendHTTPRequest(
sess,
path,
restMethod,
parameters,
options)
resp, code, err := sendHTTPRequest(sess, path, restMethod, parameters, options)

//Check if this is a refreshable exception
if err != nil && sess.IAMRefreshToken != "" && NeedsRefresh(err) {
sess.RefreshToken()
resp, code, err = sendHTTPRequest(sess, path, restMethod, parameters, options)
}

if err != nil {
//Preserve the original sl error
Expand Down Expand Up @@ -226,6 +227,7 @@ func makeHTTPRequest(
} else {
url = url + session.Endpoint
}
// fmt.Printf("Calling %s/%s", strings.TrimRight(url, "/"), path)
url = fmt.Sprintf("%s/%s", strings.TrimRight(url, "/"), path)
req, err := http.NewRequest(requestType, url, bytes.NewBuffer(requestBody))
if err != nil {
Expand Down
65 changes: 61 additions & 4 deletions session/rest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,14 @@ package session

import (
"errors"
"testing"

"fmt"
"net/http"
"reflect"
"testing"

"github.com/jarcoal/httpmock"
"github.com/softlayer/softlayer-go/datatypes"
"github.com/softlayer/softlayer-go/sl"
"github.com/softlayer/softlayer-go/tests"
)

var s *Session
Expand Down Expand Up @@ -208,7 +207,7 @@ var testcases = []testcase{
sl.String("https://example.com"),
},
options: sl.Options{Id: sl.Int(12345)},
responder: tests.NewEchoResponder(200),
responder: httpmock.NewStringResponder(200, `{"parameters":["https://example.com"]}`),
expected: `{"parameters":["https://example.com"]}`,
expectError: nil,
},
Expand Down Expand Up @@ -320,6 +319,64 @@ func TestRest(t *testing.T) {
}
}

// Tests refreshing a IAM token if it is expired.
func TestRestReauth(t *testing.T) {
// setup session and mock environment
s = New()
s.Endpoint = restEndpoint
s.IAMToken = "Bearer TestToken"
s.IAMRefreshToken = "TestTokenRefresh"
//s.Debug = true
httpmock.Activate()
defer httpmock.DeactivateAndReset()
fmt.Printf("Test [Rest Reauthentication]: ")
slOptions := &sl.Options{}
slResults := &datatypes.Account{}

httpmock.RegisterResponder("GET", fmt.Sprintf("%s/SoftLayer_Account.json", restEndpoint),
func(req *http.Request) (*http.Response, error) {
if s.IAMToken == "Bearer TestToken" {
return httpmock.NewStringResponse(
500,
`{"code":"SoftLayer_Exception_Account_Authentication_AccessTokenValidation", "error": "Expired Token"}`,
), nil
} else {
return httpmock.NewStringResponse(
200,
`{"id":1234,"companyName":"test"}`,
), nil
}
})
httpmock.RegisterResponder("POST", IBMCLOUDIAMENDPOINT,
httpmock.NewStringResponder(200, `{"access_token": "NewToken123", "refresh_token":"NewRefreshToken123", "token_type":"Bearer"}`),
)
err := s.DoRequest("SoftLayer_Account", "getObject", nil, slOptions, slResults)
if err != nil {
t.Errorf("Testing Error: %v\n", err.Error())
}

if s.IAMToken != "Bearer NewToken123" {
t.Errorf("(IAMToken) %s != 'Bearer NewToken123', Refresh Failed.", s.IAMToken)
}
if s.IAMRefreshToken != "NewRefreshToken123" {
t.Errorf("(IAMRefreshToken) %s != 'NewRefreshToken123', Refresh Failed.", s.IAMRefreshToken)
}
if httpmock.GetTotalCallCount() != 3 {
t.Errorf("Call Count = %d, expected 3", httpmock.GetTotalCallCount())
}
callInfo := httpmock.GetCallCountInfo()
fmt.Printf("%v\n", callInfo)
iamUrl := "POST https://iam.cloud.ibm.com/identity/token"
slUrl := "GET https://api.softlayer.com/rest/v3/SoftLayer_Account.json"
if callInfo[iamUrl] != 1 {
t.Errorf("%s called %d times, expected 1", iamUrl, callInfo[iamUrl])
}
if callInfo[slUrl] != 2 {
t.Errorf("%s called %d times, expected 1", slUrl, callInfo[slUrl])
}
teardown()
}

func setup(tc testcase) {
httpmock.RegisterResponder(
httpMethod(tc.method, tc.args),
Expand Down
84 changes: 84 additions & 0 deletions session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ package session

import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
"net/http"
"net/url"
"os"
"os/user"
"strings"
Expand All @@ -44,6 +48,21 @@ func init() {
// DefaultEndpoint is the default endpoint for API calls, when no override is provided.
const DefaultEndpoint = "https://api.softlayer.com/rest/v3.1"

const IBMCLOUDIAMENDPOINT = "https://iam.cloud.ibm.com/identity/token"

// IAMTokenResponse ...
type IAMTokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
}

// IAMErrorMessage -
type IAMErrorMessage struct {
ErrorMessage string `json:"errormessage"`
ErrorCode string `json:"errorcode"`
}

var retryableErrorCodes = []string{"SoftLayer_Exception_WebService_RateLimitExceeded"}

// TransportHandler interface for the protocol-specific handling of API requests.
Expand Down Expand Up @@ -105,6 +124,9 @@ type Session struct {
//IAMToken is the IAM token secret that included IMS account for token-based authentication
IAMToken string

//IAMRefreshToken is the IAM refresh token secret that required to refresh IAM Token
IAMRefreshToken string

// AuthToken is the token secret for token-based authentication
AuthToken string

Expand Down Expand Up @@ -316,6 +338,59 @@ func (r *Session) ResetUserAgent() {
r.userAgent = getDefaultUserAgent()
}

// Refreshes an IAM authenticated session
func (r *Session) RefreshToken() error {

Logger.Println("[DEBUG] Refreshing IAM Token")
client := http.DefaultClient
reqPayload := url.Values{}
reqPayload.Add("grant_type", "refresh_token")
reqPayload.Add("refresh_token", r.IAMRefreshToken)

req, err := http.NewRequest("POST", IBMCLOUDIAMENDPOINT, strings.NewReader(reqPayload.Encode()))

if err != nil {
return err
}
req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("bx:bx")))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Accept", "application/json")
var token IAMTokenResponse
var eresp IAMErrorMessage

resp, err := client.Do(req)
if err != nil {
return err
}

defer resp.Body.Close()

responseBody, err := ioutil.ReadAll(resp.Body)

if err != nil {
return err
}

if resp != nil && resp.StatusCode != 200 {
err = json.Unmarshal(responseBody, &eresp)
if err != nil {
return err
}
if eresp.ErrorCode != "" {
return sl.Error{Exception: eresp.ErrorCode, Message: eresp.ErrorMessage}
}
}

err = json.Unmarshal(responseBody, &token)
if err != nil {
return err
}

r.IAMToken = fmt.Sprintf("%s %s", token.TokenType, token.AccessToken)
r.IAMRefreshToken = token.RefreshToken
return nil
}

func envFallback(keyName string, value *string) {
if *value == "" {
*value = os.Getenv(keyName)
Expand Down Expand Up @@ -372,6 +447,15 @@ func isRetryable(err error) bool {
return isTimeout(err) || hasRetryableCode(err)
}

func NeedsRefresh(err error) bool {
if slError, ok := err.(sl.Error); ok {
if slError.StatusCode == 500 && slError.Exception == "SoftLayer_Exception_Account_Authentication_AccessTokenValidation" {
return true
}
}
return false
}

// Set ENV Variable SL_USERAGENT to append that to the useragent string
func getDefaultUserAgent() string {
envAgent := os.Getenv("SL_USERAGENT")
Expand Down
Loading

0 comments on commit 129de40

Please sign in to comment.