From 128228a73fa9fb0d600ec43e492e1bf829ef327a Mon Sep 17 00:00:00 2001 From: hkantare Date: Tue, 6 Jul 2021 11:42:49 +0530 Subject: [PATCH 1/9] Add support for refresh token in softlayer-go --- session/rest.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++ session/session.go | 3 ++ 2 files changed, 83 insertions(+) diff --git a/session/rest.go b/session/rest.go index 9680dba..10c53ff 100644 --- a/session/rest.go +++ b/session/rest.go @@ -18,6 +18,7 @@ package session import ( "bytes" + "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -35,6 +36,21 @@ import ( type RestTransport struct{} +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"` +} + // DoRequest - Implementation of the TransportHandler interface for handling // calls to the REST endpoint. func (r *RestTransport) DoRequest(sess *Session, service string, method string, args []interface{}, options *sl.Options, pResult interface{}) error { @@ -189,6 +205,22 @@ func tryHTTPRequest( resp, code, err := makeHTTPRequest(sess, path, requestType, requestBody, options) if err != nil { + if code == 500 && (sess.IAMToken != "" && sess.IAMRefreshToken != "") { + authErr := refreshToken(sess) + if authErr == nil { + if retries--; retries > 0 { + jitter := time.Duration(rand.Int63n(int64(wait))) + wait = wait + jitter/2 + time.Sleep(wait) + return tryHTTPRequest( + retries, wait, sess, path, requestType, requestBody, options) + } + } + if authErr != nil { + return resp, code, fmt.Errorf("Unable to refresh auth token: {{%v}}", authErr) + } + + } if !isRetryable(err) { return resp, code, err } @@ -320,3 +352,51 @@ func findResponseError(code int, resp []byte) error { } return nil } + +func refreshToken(sess *Session) error { + + client := http.DefaultClient + reqPayload := url.Values{} + reqPayload.Add("grant_type", "refresh_token") + reqPayload.Add("refresh_token", sess.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 + } + sess.IAMToken = fmt.Sprintf("%s %s", token.TokenType, token.AccessToken) + sess.IAMRefreshToken = token.RefreshToken + return nil +} diff --git a/session/session.go b/session/session.go index fc4c0cc..9384920 100644 --- a/session/session.go +++ b/session/session.go @@ -104,6 +104,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 From cd1029a836ad1c7b24e625f673f5575e4989dcf5 Mon Sep 17 00:00:00 2001 From: hkantare Date: Wed, 7 Jul 2021 10:45:08 +0530 Subject: [PATCH 2/9] Add ClusterIdentifier and HostId attributes for terraform --- datatypes/container.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/datatypes/container.go b/datatypes/container.go index bb66f5b..c0f9563 100644 --- a/datatypes/container.go +++ b/datatypes/container.go @@ -3623,6 +3623,11 @@ type Container_Product_Item_Discount_Program struct { type Container_Product_Order struct { Entity + // Used to identify which items on an order belong in the same cluster. + ClusterIdentifier *string `json:"clusterIdentifier,omitempty" xmlrpc:"clusterIdentifier,omitempty"` + + HostId *int `json:"hostId,omitempty" xmlrpc:"hostId,omitempty"` + // Flag for identifying an order for Big Data Deployment. BigDataOrderFlag *bool `json:"bigDataOrderFlag,omitempty" xmlrpc:"bigDataOrderFlag,omitempty"` From b9b949beff641cb5cc1f1625c4c1b912a852615d Mon Sep 17 00:00:00 2001 From: hkantare Date: Mon, 4 Oct 2021 14:21:20 +0530 Subject: [PATCH 3/9] Add ReservedCapacityId attributes for terraform --- datatypes/container.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/datatypes/container.go b/datatypes/container.go index c0f9563..bbad321 100644 --- a/datatypes/container.go +++ b/datatypes/container.go @@ -3781,6 +3781,9 @@ type Container_Product_Order struct { // If a regional group is provided and VLANs are specified (within the hardware or virtualGuests properties), we will use the datacenter where the VLANs are located. If no VLANs are specified, we will use the preferred datacenter on the regional group object. RegionalGroup *string `json:"regionalGroup,omitempty" xmlrpc:"regionalGroup,omitempty"` + // Identifier of [[SoftLayer_Virtual_ReservedCapacityGroup]] to order + ReservedCapacityId *int `json:"reservedCapacityId,omitempty" xmlrpc:"reservedCapacityId,omitempty"` + // An optional resource group identifier specifying the resource group to attach the order to ResourceGroupId *int `json:"resourceGroupId,omitempty" xmlrpc:"resourceGroupId,omitempty"` From 4ad3d01356cd3f045713b6713f4c7925aafd2add Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 19 Sep 2024 16:34:19 -0500 Subject: [PATCH 4/9] Reverted datatypes/container updates --- datatypes/container.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/datatypes/container.go b/datatypes/container.go index 1e8f36f..575a6c0 100644 --- a/datatypes/container.go +++ b/datatypes/container.go @@ -3340,11 +3340,6 @@ type Container_Product_Item_Discount_Program struct { type Container_Product_Order struct { Entity - // Used to identify which items on an order belong in the same cluster. - ClusterIdentifier *string `json:"clusterIdentifier,omitempty" xmlrpc:"clusterIdentifier,omitempty"` - - HostId *int `json:"hostId,omitempty" xmlrpc:"hostId,omitempty"` - // Flag for identifying an order for Big Data Deployment. BigDataOrderFlag *bool `json:"bigDataOrderFlag,omitempty" xmlrpc:"bigDataOrderFlag,omitempty"` @@ -3499,9 +3494,6 @@ type Container_Product_Order struct { // If a regional group is provided and VLANs are specified (within the hardware or virtualGuests properties), we will use the datacenter where the VLANs are located. If no VLANs are specified, we will use the preferred datacenter on the regional group object. RegionalGroup *string `json:"regionalGroup,omitempty" xmlrpc:"regionalGroup,omitempty"` - // Identifier of [[SoftLayer_Virtual_ReservedCapacityGroup]] to order - ReservedCapacityId *int `json:"reservedCapacityId,omitempty" xmlrpc:"reservedCapacityId,omitempty"` - // An optional resource group identifier specifying the resource group to attach the order to ResourceGroupId *int `json:"resourceGroupId,omitempty" xmlrpc:"resourceGroupId,omitempty"` From 6eff079dbe3aaf89d2db72933323141ce2581928 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Thu, 19 Sep 2024 17:00:41 -0500 Subject: [PATCH 5/9] Ran gofmt session/rest.go --- session/rest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/session/rest.go b/session/rest.go index 6c232ed..9db5aeb 100644 --- a/session/rest.go +++ b/session/rest.go @@ -38,7 +38,7 @@ type RestTransport struct{} const IBMCLOUDIAMENDPOINT = "https://iam.cloud.ibm.com/identity/token" -//IAMTokenResponse ... +// IAMTokenResponse ... type IAMTokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` From 303b98d571b3265e2864742e43f8b86d6e6d973e Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Fri, 20 Sep 2024 14:18:43 -0500 Subject: [PATCH 6/9] Moved refreshSession to the Session struct, added ability for rest transport to automatically refresh token if needed --- .secrets.baseline | 4 +- examples/cmd/iam_demo.go | 56 +++++++++++++++++++++++++ session/rest.go | 90 ++++------------------------------------ session/session.go | 79 +++++++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 85 deletions(-) create mode 100644 examples/cmd/iam_demo.go diff --git a/.secrets.baseline b/.secrets.baseline index 358a8d6..7b880d9 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "go.sum|^.secrets.baseline$", "lines": null }, - "generated_at": "2024-06-06T22:18:14Z", + "generated_at": "2024-09-20T19:18:35Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -242,7 +242,7 @@ "hashed_secret": "6f667d3e9627f5549ffeb1055ff294c34430b837", "is_secret": false, "is_verified": false, - "line_number": 171, + "line_number": 194, "type": "Secret Keyword", "verified_result": null } diff --git a/examples/cmd/iam_demo.go b/examples/cmd/iam_demo.go new file mode 100644 index 0000000..319308c --- /dev/null +++ b/examples/cmd/iam_demo.go @@ -0,0 +1,56 @@ +package cmd + +import ( + "fmt" + "time" + "github.com/spf13/cobra" + + "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 +} diff --git a/session/rest.go b/session/rest.go index 9db5aeb..1088cc1 100644 --- a/session/rest.go +++ b/session/rest.go @@ -18,7 +18,6 @@ package session import ( "bytes" - "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -36,20 +35,6 @@ import ( type RestTransport struct{} -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"` -} // DoRequest - Implementation of the TransportHandler interface for handling // calls to the REST endpoint. @@ -68,12 +53,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 @@ -205,22 +191,6 @@ func tryHTTPRequest( resp, code, err := makeHTTPRequest(sess, path, requestType, requestBody, options) if err != nil { - if code == 500 && (sess.IAMToken != "" && sess.IAMRefreshToken != "") { - authErr := refreshToken(sess) - if authErr == nil { - if retries--; retries > 0 { - jitter := time.Duration(rand.Int63n(int64(wait))) - wait = wait + jitter/2 - time.Sleep(wait) - return tryHTTPRequest( - retries, wait, sess, path, requestType, requestBody, options) - } - } - if authErr != nil { - return resp, code, fmt.Errorf("Unable to refresh auth token: {{%v}}", authErr) - } - - } if !isRetryable(err) { return resp, code, err } @@ -379,50 +349,4 @@ func findResponseError(code int, resp []byte) error { return nil } -func refreshToken(sess *Session) error { - - client := http.DefaultClient - reqPayload := url.Values{} - reqPayload.Add("grant_type", "refresh_token") - reqPayload.Add("refresh_token", sess.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 - } - sess.IAMToken = fmt.Sprintf("%s %s", token.TokenType, token.AccessToken) - sess.IAMRefreshToken = token.RefreshToken - return nil -} diff --git a/session/session.go b/session/session.go index a7d82ab..6addf9d 100644 --- a/session/session.go +++ b/session/session.go @@ -24,10 +24,15 @@ import ( "math/rand" "net" "net/http" + "net/url" "os" "os/user" "strings" "time" + "encoding/base64" + "encoding/json" + "io/ioutil" + "github.com/softlayer/softlayer-go/config" "github.com/softlayer/softlayer-go/sl" @@ -44,6 +49,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. @@ -319,6 +339,56 @@ 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) @@ -375,6 +445,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") From 570631b67fbf8a1609cfabaeda9a2c32d1abadd5 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 24 Sep 2024 16:12:49 -0500 Subject: [PATCH 7/9] More unit tests --- .secrets.baseline | 4 +- examples/cmd/iam_demo.go | 9 ++--- session/rest.go | 4 +- session/rest_test.go | 62 ++++++++++++++++++++++++++++++- session/session.go | 10 +++-- session/session_test.go | 80 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 153 insertions(+), 16 deletions(-) diff --git a/.secrets.baseline b/.secrets.baseline index 7b880d9..8e106fb 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "go.sum|^.secrets.baseline$", "lines": null }, - "generated_at": "2024-09-20T19:18:35Z", + "generated_at": "2024-09-24T20:48:27Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -242,7 +242,7 @@ "hashed_secret": "6f667d3e9627f5549ffeb1055ff294c34430b837", "is_secret": false, "is_verified": false, - "line_number": 194, + "line_number": 193, "type": "Secret Keyword", "verified_result": null } diff --git a/examples/cmd/iam_demo.go b/examples/cmd/iam_demo.go index 319308c..ab01786 100644 --- a/examples/cmd/iam_demo.go +++ b/examples/cmd/iam_demo.go @@ -2,8 +2,8 @@ package cmd import ( "fmt" - "time" "github.com/spf13/cobra" + "time" "github.com/softlayer/softlayer-go/services" "github.com/softlayer/softlayer-go/session" @@ -26,10 +26,10 @@ func RunIamCmd(cmd *cobra.Command, args []string) error { // Sets up the session with authentication headers. sess := &session.Session{ - Endpoint: session.DefaultEndpoint, - IAMToken: "Bearer TOKEN", + Endpoint: session.DefaultEndpoint, + IAMToken: "Bearer TOKEN", IAMRefreshToken: "REFRESH TOKEN", - Debug: true, + Debug: true, } // creates a reference to the service object (SoftLayer_Account) @@ -37,7 +37,6 @@ func RunIamCmd(cmd *cobra.Command, args []string) error { // 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 { diff --git a/session/rest.go b/session/rest.go index 1088cc1..cc4e388 100644 --- a/session/rest.go +++ b/session/rest.go @@ -35,7 +35,6 @@ import ( type RestTransport struct{} - // DoRequest - Implementation of the TransportHandler interface for handling // calls to the REST endpoint. func (r *RestTransport) DoRequest(sess *Session, service string, method string, args []interface{}, options *sl.Options, pResult interface{}) error { @@ -228,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 { @@ -348,5 +348,3 @@ func findResponseError(code int, resp []byte) error { } return nil } - - diff --git a/session/rest_test.go b/session/rest_test.go index 64cf053..3d312f4 100644 --- a/session/rest_test.go +++ b/session/rest_test.go @@ -18,10 +18,10 @@ package session import ( "errors" - "testing" - "fmt" + "net/http" "reflect" + "testing" "github.com/jarcoal/httpmock" "github.com/softlayer/softlayer-go/datatypes" @@ -320,6 +320,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), diff --git a/session/session.go b/session/session.go index 6addf9d..593f1c2 100644 --- a/session/session.go +++ b/session/session.go @@ -19,7 +19,10 @@ package session import ( "context" + "encoding/base64" + "encoding/json" "fmt" + "io/ioutil" "log" "math/rand" "net" @@ -29,10 +32,6 @@ import ( "os/user" "strings" "time" - "encoding/base64" - "encoding/json" - "io/ioutil" - "github.com/softlayer/softlayer-go/config" "github.com/softlayer/softlayer-go/sl" @@ -349,6 +348,7 @@ func (r *Session) RefreshToken() error { reqPayload.Add("refresh_token", r.IAMRefreshToken) req, err := http.NewRequest("POST", IBMCLOUDIAMENDPOINT, strings.NewReader(reqPayload.Encode())) + if err != nil { return err } @@ -366,6 +366,7 @@ func (r *Session) RefreshToken() error { defer resp.Body.Close() responseBody, err := ioutil.ReadAll(resp.Body) + if err != nil { return err } @@ -384,6 +385,7 @@ func (r *Session) RefreshToken() error { if err != nil { return err } + r.IAMToken = fmt.Sprintf("%s %s", token.TokenType, token.AccessToken) r.IAMRefreshToken = token.RefreshToken return nil diff --git a/session/session_test.go b/session/session_test.go index d18cd48..d79ab8a 100644 --- a/session/session_test.go +++ b/session/session_test.go @@ -1,6 +1,9 @@ package session import ( + "fmt" + "github.com/jarcoal/httpmock" + "github.com/softlayer/softlayer-go/sl" "os" "strings" "testing" @@ -96,3 +99,80 @@ func TestSetRetries(t *testing.T) { t.Errorf("Session.Retries (%v) != newVariable (%v)", s.Retries, newVariable) } } + +func TestNeedsRefresh(t *testing.T) { + testError := sl.Error{ + StatusCode: 500, + Exception: "SoftLayer_Exception_Account_Authentication_AccessTokenValidation", + } + if !NeedsRefresh(testError) { + t.Errorf("NeedsRefresh() failed to detect refresh error") + } + testError = sl.Error{ + StatusCode: 500, + Exception: "SoftLayer_Exception_Public", + } + if NeedsRefresh(testError) { + t.Errorf("NeedsRefresh() failed to properly error check") + } +} + +// Tests refreshing a IAM token if it is expired. +func TestRefreshToken(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("TestRefreshToken [Happy Path]: ") + expectedError := "" + // Happy Path + httpmock.RegisterResponder("POST", IBMCLOUDIAMENDPOINT, + httpmock.NewStringResponder(200, `{"access_token": "NewToken123", "refresh_token":"NewRefreshToken123", "token_type":"Bearer"}`), + ) + err := s.RefreshToken() + 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) + } + httpmock.Reset() + + // Error returned from IAM API + fmt.Printf("TestRefreshToken [API error]: ") + httpmock.RegisterResponder("POST", IBMCLOUDIAMENDPOINT, + httpmock.NewStringResponder(400, `{"errormessage": "Some Error", "errorcode":"400"}`), + ) + err = s.RefreshToken() + if err == nil { + t.Errorf("Expected an error, none returned\n") + } + expectedError = "400: Some Error " + if err.Error() != expectedError { + t.Errorf("Expected |%s| == %s", err.Error(), expectedError) + } + httpmock.Reset() + + // Junk returned from IAM API + fmt.Printf("TestRefreshToken [Bad Response]: ") + httpmock.RegisterResponder("POST", IBMCLOUDIAMENDPOINT, + httpmock.NewStringResponder(200, ""), + ) + err = s.RefreshToken() + if err == nil { + t.Errorf("Expected an error, none returned\n") + } + expectedError = "unexpected end of JSON input" + if err.Error() != expectedError { + t.Errorf("Expected %s == %s", err.Error(), expectedError) + } + httpmock.Reset() +} From 9cfe31a24ccb80408f62be53eeb1502f8d6aa937 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 24 Sep 2024 16:34:15 -0500 Subject: [PATCH 8/9] Removed some old unit tests --- generator/generate_test.go | 1 + {tests => generator}/specialcase_test.go | 18 +------------ session/rest_test.go | 3 +-- tests/util.go | 34 ------------------------ 4 files changed, 3 insertions(+), 53 deletions(-) rename {tests => generator}/specialcase_test.go (79%) delete mode 100644 tests/util.go diff --git a/generator/generate_test.go b/generator/generate_test.go index c05885b..f272579 100644 --- a/generator/generate_test.go +++ b/generator/generate_test.go @@ -42,3 +42,4 @@ var _ = Describe("Generate Tests", func() { Expect(os.RemoveAll(outDir)).To(Succeed()) }) }) + diff --git a/tests/specialcase_test.go b/generator/specialcase_test.go similarity index 79% rename from tests/specialcase_test.go rename to generator/specialcase_test.go index 65e0da1..1ffd1a5 100644 --- a/tests/specialcase_test.go +++ b/generator/specialcase_test.go @@ -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" diff --git a/session/rest_test.go b/session/rest_test.go index 3d312f4..ef9761a 100644 --- a/session/rest_test.go +++ b/session/rest_test.go @@ -26,7 +26,6 @@ import ( "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 @@ -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, }, diff --git a/tests/util.go b/tests/util.go deleted file mode 100644 index 6d33a99..0000000 --- a/tests/util.go +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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 - -import ( - "net/http" - "strconv" -) - -// Returns a responder whose response body is the request body -func NewEchoResponder(status int) func(*http.Request) (*http.Response, error) { - return func(req *http.Request) (*http.Response, error) { - return &http.Response{ - Status: strconv.Itoa(status), - StatusCode: status, - Body: req.Body, - Header: http.Header{}, - }, nil - } -} From eb64e7dfe6df9cfb4678d1ae8ab441c81d4db0a2 Mon Sep 17 00:00:00 2001 From: Christopher Gallo Date: Tue, 24 Sep 2024 16:35:33 -0500 Subject: [PATCH 9/9] Ran go fmt generator/generate_test.go --- generator/generate_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/generator/generate_test.go b/generator/generate_test.go index f272579..c05885b 100644 --- a/generator/generate_test.go +++ b/generator/generate_test.go @@ -42,4 +42,3 @@ var _ = Describe("Generate Tests", func() { Expect(os.RemoveAll(outDir)).To(Succeed()) }) }) -