Skip to content

Commit 7cfa57e

Browse files
Merge pull request #199 from softlayer/refreshv2
Added an observer for IAMToken refreshes
2 parents dc4dd3f + d06b8ed commit 7cfa57e

File tree

6 files changed

+146
-10
lines changed

6 files changed

+146
-10
lines changed

.secrets.baseline

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"files": "go.sum|^.secrets.baseline$",
44
"lines": null
55
},
6-
"generated_at": "2024-09-27T22:05:21Z",
6+
"generated_at": "2024-10-01T21:07:46Z",
77
"plugins_used": [
88
{
99
"name": "AWSKeyDetector"
@@ -242,7 +242,7 @@
242242
"hashed_secret": "6f667d3e9627f5549ffeb1055ff294c34430b837",
243243
"is_secret": false,
244244
"is_verified": false,
245-
"line_number": 197,
245+
"line_number": 201,
246246
"type": "Secret Keyword",
247247
"verified_result": null
248248
}

README.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,60 @@ func main() {
325325
}
326326
```
327327

328+
### IAM authentication
329+
330+
This library supports [IBM's IAM Authentication](https://cloud.ibm.com/docs/account?topic=account-iamoverview) (used by the `ibmcloud` cli for example). You will want to set the `IAMToken` and `IAMRefreshToken` properties on the session to make use of it.
331+
332+
333+
```go
334+
token := "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..."
335+
refreshToken := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb..."
336+
sess := &session.Session{
337+
Endpoint: "https://api.softlayer.com/rest/v3.1",
338+
Debug: true,
339+
Timeout: 90,
340+
IAMToken: token,
341+
IAMRefreshToken: refreshToken
342+
}
343+
```
344+
345+
You can bypass automatic IAM refresh by either not setting the `IAMRefreshToken` property, or by manually configuring the `TransportHandler`
346+
347+
```go
348+
token := "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..."
349+
handler := &session.RestTransport{}
350+
sess := &session.Session{
351+
Endpoint: "https://api.softlayer.com/rest/v3.1",
352+
Debug: true,
353+
Timeout: 90,
354+
IAMToken: token,
355+
TransportHandler handler,
356+
}
357+
```
358+
359+
If you want to be able to record the new tokens in a config file (or elsewhere), you can configure an `IAMUpdaters` Observer which will take in as arguments the new tokens, allowing you to save them somewhere for reuse.
360+
361+
```go
362+
type MyIamUpdater struct {
363+
debug bool
364+
}
365+
// This function is where you can configure the logic to save these new tokens somewhere
366+
func (iamupdater *MyIamUpdater) Update(token string, refresh string) {
367+
fmt.Printf("[DEBUG] New Token: %s\n", token)
368+
fmt.Printf("[DEBUG] New Refresh Token: %s\n", refresh)
369+
}
370+
updater := &MyIamUpdater{debug: false}
371+
token := "Bearer aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa..."
372+
refreshToken := "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb..."
373+
sess := &session.Session{
374+
Endpoint: "https://api.softlayer.com/rest/v3.1",
375+
IAMToken: token,
376+
IAMRefreshToken refreshToken,
377+
}
378+
sess.AddIAMUpdater(updater)
379+
```
380+
You can add multiple Updaters as well, they will be called in the order they are added. `MyIamUpdater.Update(token, refresh)` in this example will only be called when the token is actually refreshed.
381+
328382
## Running Examples
329383

330384
The [Examples](https://github.com/softlayer/softlayer-go/tree/master/examples) directory has a few rough examples scripts that can help you get started developing with this library.

session/iamupdater.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package session
2+
3+
//counterfeiter:generate . IAMUpdater
4+
type IAMUpdater interface {
5+
Update(token string, refresh string)
6+
}
7+
8+
type LogIamUpdater struct {
9+
debug bool
10+
}
11+
12+
func NewLogIamUpdater(debug bool) *LogIamUpdater {
13+
return &LogIamUpdater{
14+
debug: debug,
15+
}
16+
}
17+
18+
func (iamupdater *LogIamUpdater) Update(token string, refresh string) {
19+
if iamupdater.debug {
20+
Logger.Printf("[DEBUG] New Token: %s\n", token)
21+
Logger.Printf("[DEBUG] New Refresh Token: %s\n", refresh)
22+
}
23+
}

session/rest.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,6 @@ func (r *RestTransport) DoRequest(sess *Session, service string, method string,
5454

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

57-
//Check if this is a refreshable exception
58-
if err != nil && sess.IAMRefreshToken != "" && NeedsRefresh(err) {
59-
sess.RefreshToken()
60-
resp, code, err = sendHTTPRequest(sess, path, restMethod, parameters, options)
61-
}
62-
6357
if err != nil {
6458
//Preserve the original sl error
6559
if _, ok := err.(sl.Error); ok {

session/session.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ type Session struct {
127127
//IAMRefreshToken is the IAM refresh token secret that required to refresh IAM Token
128128
IAMRefreshToken string
129129

130+
// A list objects that implement the IAMUpdater interface.
131+
// When a IAMToken is refreshed, these are notified with the new token and new refresh token.
132+
IAMUpdaters []IAMUpdater
133+
130134
// AuthToken is the token secret for token-based authentication
131135
AuthToken string
132136

@@ -289,6 +293,11 @@ func (r *Session) DoRequest(service string, method string, args []interface{}, o
289293
}
290294

291295
err := r.TransportHandler.DoRequest(r, service, method, args, options, pResult)
296+
//Check if this is a refreshable exception and try 1 more time
297+
if err != nil && r.IAMRefreshToken != "" && NeedsRefresh(err) {
298+
r.RefreshToken()
299+
err = r.TransportHandler.DoRequest(r, service, method, args, options, pResult)
300+
}
292301
r.LastCall = CallToString(service, method, args, options)
293302
if err != nil {
294303
return err
@@ -345,8 +354,6 @@ func (r *Session) ResetUserAgent() {
345354

346355
// Refreshes an IAM authenticated session
347356
func (r *Session) RefreshToken() error {
348-
349-
Logger.Println("[DEBUG] Refreshing IAM Token")
350357
client := http.DefaultClient
351358
reqPayload := url.Values{}
352359
reqPayload.Add("grant_type", "refresh_token")
@@ -393,6 +400,10 @@ func (r *Session) RefreshToken() error {
393400

394401
r.IAMToken = fmt.Sprintf("%s %s", token.TokenType, token.AccessToken)
395402
r.IAMRefreshToken = token.RefreshToken
403+
// Mostly these are needed if we want to save these new tokens to a config file.
404+
for _, updater := range r.IAMUpdaters {
405+
updater.Update(r.IAMToken, r.IAMRefreshToken)
406+
}
396407
return nil
397408
}
398409

@@ -401,6 +412,12 @@ func (r *Session) String() string {
401412
return r.LastCall
402413
}
403414

415+
// Adds a new IAMUpdater instance to the session
416+
// Useful if you want to update a config file with the new Tokens
417+
func (r *Session) AddIAMUpdater(updater IAMUpdater) {
418+
r.IAMUpdaters = append(r.IAMUpdaters, updater)
419+
}
420+
404421
func envFallback(keyName string, value *string) {
405422
if *value == "" {
406423
*value = os.Getenv(keyName)
@@ -457,6 +474,7 @@ func isRetryable(err error) bool {
457474
return isTimeout(err) || hasRetryableCode(err)
458475
}
459476

477+
// Detects if the SL API returned a specific exception indicating the IAMToken is expired.
460478
func NeedsRefresh(err error) bool {
461479
if slError, ok := err.(sl.Error); ok {
462480
if slError.StatusCode == 500 && slError.Exception == "SoftLayer_Exception_Account_Authentication_AccessTokenValidation" {
@@ -475,6 +493,7 @@ func getDefaultUserAgent() string {
475493
return fmt.Sprintf("softlayer-go/%s %s ", sl.Version.String(), envAgent)
476494
}
477495

496+
// Formats an API call into a readable string
478497
func CallToString(service string, method string, args []interface{}, options *sl.Options) string {
479498
if options == nil {
480499
options = new(sl.Options)

session/session_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package session
22

33
import (
4+
"bytes"
45
"fmt"
56
"github.com/jarcoal/httpmock"
67
"github.com/softlayer/softlayer-go/datatypes"
78
"github.com/softlayer/softlayer-go/sl"
9+
"log"
810
"os"
911
"strings"
1012
"testing"
@@ -178,6 +180,50 @@ func TestRefreshToken(t *testing.T) {
178180
httpmock.Reset()
179181
}
180182

183+
// Tests refreshing a IAM token logging output and calling IAMUpdaters
184+
func TestRefreshTokenWithLog(t *testing.T) {
185+
// setup session and mock environment
186+
logBuf := bytes.Buffer{}
187+
Logger = log.New(&logBuf, "", log.LstdFlags)
188+
s = New()
189+
s.Endpoint = restEndpoint
190+
s.IAMToken = "Bearer TestToken"
191+
s.IAMRefreshToken = "TestTokenRefresh"
192+
//s.Debug = true
193+
updater := NewLogIamUpdater(true)
194+
s.AddIAMUpdater(updater)
195+
httpmock.Activate()
196+
defer httpmock.DeactivateAndReset()
197+
fmt.Printf("TestRefreshTokenWithLog [Happy Path]: ")
198+
199+
// Happy Path
200+
httpmock.RegisterResponder("POST", IBMCLOUDIAMENDPOINT,
201+
httpmock.NewStringResponder(200, `{"access_token": "NewToken123", "refresh_token":"NewRefreshToken123", "token_type":"Bearer"}`),
202+
)
203+
err := s.RefreshToken()
204+
if err != nil {
205+
t.Errorf("Testing Error: %v\n", err.Error())
206+
}
207+
208+
if s.IAMToken != "Bearer NewToken123" {
209+
t.Errorf("(IAMToken) %s != 'Bearer NewToken123', Refresh Failed.", s.IAMToken)
210+
}
211+
if s.IAMRefreshToken != "NewRefreshToken123" {
212+
t.Errorf("(IAMRefreshToken) %s != 'NewRefreshToken123', Refresh Failed.", s.IAMRefreshToken)
213+
}
214+
logOutput := strings.Split(logBuf.String(), "\n")
215+
if len(logOutput) < 2 {
216+
t.Errorf("Not enough log output detected.")
217+
}
218+
if !strings.HasSuffix(logOutput[0], "[DEBUG] New Token: Bearer NewToken123") {
219+
t.Errorf("%s is incorrect log output", logOutput[0])
220+
}
221+
if !strings.HasSuffix(logOutput[1], "[DEBUG] New Refresh Token: NewRefreshToken123") {
222+
t.Errorf("%s is incorrect log output", logOutput[1])
223+
}
224+
httpmock.Reset()
225+
}
226+
181227
func TestString(t *testing.T) {
182228
// setup session and mock environment
183229
s = New()

0 commit comments

Comments
 (0)