Skip to content

Commit c1f20ce

Browse files
committed
feat(auth): Added LastSessionUpdateTime config (IBM-Cloud#335)
* feat(auth): Added LastSessionUpdateTime config * docs: Added how to refresh session in guide * style(guide): Fixed newline consistency * chore(auth): Fix golang doc comment method * test: Added better test case for last update session time
1 parent 259a03a commit c1f20ce

File tree

6 files changed

+100
-13
lines changed

6 files changed

+100
-13
lines changed

bluemix/authentication/iam/iam.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ func SetPhoneAuthToken(token string) authentication.TokenOption {
186186
type Token struct {
187187
AccessToken string `json:"access_token"`
188188
RefreshToken string `json:"refresh_token"`
189+
SessionID string `json:"session_id"`
189190
TokenType string `json:"token_type"`
190191
Scope string `json:"scope"`
191192
Expiry time.Time `json:"expiration"`
@@ -226,13 +227,15 @@ type Endpoint struct {
226227

227228
type Interface interface {
228229
GetEndpoint() (*Endpoint, error)
230+
RefreshSession(sessionId string) error
229231
GetToken(req *authentication.TokenRequest) (*Token, error)
230232
InitiateIMSPhoneFactor(req *authentication.TokenRequest) (authToken string, err error)
231233
}
232234

233235
type Config struct {
234236
IAMEndpoint string
235237
TokenEndpoint string // Optional. Default value is <IAMEndpoint>/identity/token
238+
SessionEndpoint string // Optional. Default value is <IAMEndpoint>/v1/sessions
236239
ClientID string
237240
ClientSecret string
238241
UAAClientID string
@@ -246,10 +249,18 @@ func (c Config) tokenEndpoint() string {
246249
return c.IAMEndpoint + "/identity/token"
247250
}
248251

252+
func (c Config) sessionEndpoint() string {
253+
if c.SessionEndpoint != "" {
254+
return c.SessionEndpoint
255+
}
256+
return c.IAMEndpoint + "/v1/sessions"
257+
}
258+
249259
func DefaultConfig(iamEndpoint string) Config {
250260
return Config{
251261
IAMEndpoint: iamEndpoint,
252262
TokenEndpoint: iamEndpoint + "/identity/token",
263+
SessionEndpoint: iamEndpoint + "/v1/sessions",
253264
ClientID: defaultClientID,
254265
ClientSecret: defaultClientSecret,
255266
UAAClientID: defaultUAAClientID,
@@ -307,6 +318,24 @@ func (c *client) GetToken(tokenReq *authentication.TokenRequest) (*Token, error)
307318
return &ret, nil
308319
}
309320

321+
// RefreshSession maintains the session state. Useful for async workloads
322+
// @param sessionID string - the session ID
323+
func (c *client) RefreshSession(sessionID string) error {
324+
// If no session ID is provided there is no need to refresh
325+
if sessionID == "" {
326+
return nil
327+
}
328+
url := fmt.Sprintf("%s/%s/state", c.config.sessionEndpoint(), sessionID)
329+
r := rest.PatchRequest(url).
330+
Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", c.config.ClientID, c.config.ClientSecret))))
331+
332+
if err := c.doRequest(r, nil); err != nil {
333+
return err
334+
}
335+
336+
return nil
337+
}
338+
310339
func (c *client) InitiateIMSPhoneFactor(tokenReq *authentication.TokenRequest) (authToken string, err error) {
311340
v := make(url.Values)
312341
tokenReq.SetValue(v)

bluemix/authentication/iam/iam_test.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
)
1414

1515
const (
16+
crAuthTestSessionId string = "C-6376c629-9808-4447-8751-65a2d9414fx"
1617
crAuthMockIAMProfileName string = "iam-user-123"
1718
crAuthMockIAMProfileID string = "iam-id-123"
1819
crAuthMockIAMProfileCRN string = "crn:v1:bluemix:public:iam-identity::a/123456::profile:Profile-9fd84246-7df4-4667-94e4-8ecde51d5ac5"
@@ -258,29 +259,27 @@ func TestGetTokenOneFromServerFailureWithProfileNameAndIDAndCRN(t *testing.T) {
258259

259260
func TestGetTokenOneFromServerApiErrorWithProfileNameAndID(t *testing.T) {
260261
errorCases := []struct {
261-
errorCode string
262+
errorCode string
262263
errorMessage string
263-
264264
}{
265265
{
266-
errorCode: InvalidTokenErrorCode,
266+
errorCode: InvalidTokenErrorCode,
267267
errorMessage: "invalid token",
268268
},
269269
{
270-
errorCode: RefreshTokenExpiryErrorCode,
270+
errorCode: RefreshTokenExpiryErrorCode,
271271
errorMessage: "refresh token expired",
272272
},
273273
{
274-
errorCode: ExternalAuthenticationErrorCode,
274+
errorCode: ExternalAuthenticationErrorCode,
275275
errorMessage: "External authentication failed",
276276
},
277277
{
278-
errorCode: SessionInactiveErrorCode,
278+
errorCode: SessionInactiveErrorCode,
279279
errorMessage: "sdf",
280280
},
281281
}
282282

283-
284283
for _, errorCase := range errorCases {
285284
errorJson := fmt.Sprintf(`{"errorCode": "%s", "errorMessage": "%s", "errorDetails": "", "requirements": {"code": "", "error": ""}}`, errorCase.errorCode, errorCase.errorMessage)
286285
server := startMockIAMServerForCRExchange(t, 1, http.StatusUnauthorized, errorJson)
@@ -296,13 +295,20 @@ func TestGetTokenOneFromServerApiErrorWithProfileNameAndID(t *testing.T) {
296295
IAMToken, err := mockClient.GetToken(tokenReq)
297296
assert.NotNil(t, err)
298297
assert.Nil(t, IAMToken)
299-
assert.Contains(t, err.Error(),errorCase.errorMessage)
300-
301-
298+
assert.Contains(t, err.Error(), errorCase.errorMessage)
302299
}
300+
}
303301

302+
func TestRefreshSession(t *testing.T) {
303+
server := startMockIAMServerForCRExchange(t, 1, http.StatusAccepted, "")
304+
defer server.Close()
304305

306+
mockIAMEndpoint := server.URL
307+
mockConfig := DefaultConfig(mockIAMEndpoint)
308+
mockClient := NewClient(mockConfig, rest.NewClient())
309+
err := mockClient.RefreshSession(crAuthTestSessionId)
305310

311+
assert.Nil(t, err)
306312
}
307313

308314
// startMockIAMServerForCRExchange will start a mock server endpoint that supports both the
@@ -350,14 +356,20 @@ func startMockIAMServerForCRExchange(t *testing.T, call int, statusCode int, err
350356
if errorJson == "" {
351357
mockErrorJson = "Sorry, bad request!"
352358
}
353-
fmt.Fprint(res, mockErrorJson)
359+
fmt.Fprint(res, mockErrorJson)
354360

355361
case http.StatusUnauthorized:
356362
if errorJson == "" {
357363
mockErrorJson = "Sorry, you are not authorized!"
358364
}
359365
fmt.Fprint(res, mockErrorJson)
360366
}
367+
} else if operationPath == fmt.Sprintf("/v1/sessions/%s/state", crAuthTestSessionId) {
368+
username, password, ok := req.BasicAuth()
369+
assert.True(t, ok)
370+
assert.Equal(t, defaultClientID, username)
371+
assert.Equal(t, defaultClientSecret, password)
372+
res.WriteHeader(statusCode)
361373
} else {
362374
assert.Fail(t, "unknown operation path: "+operationPath)
363375
}

bluemix/configuration/core_config/bx_config.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ type BXConfigData struct {
5050
SSLDisabled bool
5151
Locale string
5252
MessageOfTheDayTime int64
53+
LastSessionUpdateTime int64
5354
Trace string
5455
ColorEnabled string
5556
HTTPTimeout int
@@ -727,6 +728,20 @@ func (c *bxConfig) SetMessageOfTheDayTime() {
727728
})
728729
}
729730

731+
func (c *bxConfig) SetLastSessionUpdateTime() {
732+
c.write(func() {
733+
c.data.LastSessionUpdateTime = time.Now().Unix()
734+
})
735+
}
736+
737+
func (c *bxConfig) LastSessionUpdateTime() (session int64) {
738+
c.read(func() {
739+
session = c.data.LastSessionUpdateTime
740+
})
741+
742+
return
743+
}
744+
730745
func (c *bxConfig) ClearSession() {
731746
c.write(func() {
732747
c.data.IAMToken = ""

bluemix/configuration/core_config/bx_config_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,21 @@ func TestMOD(t *testing.T) {
454454
t.Cleanup(cleanupConfigFiles)
455455
}
456456

457+
func TestLastUpdateSessionTime(t *testing.T) {
458+
459+
config := prepareConfigForCLI(`{}`, t)
460+
461+
// check initial state
462+
assert.Empty(t, config.LastSessionUpdateTime())
463+
464+
// Set last session update time and check that the timestamp is set
465+
config.SetLastSessionUpdateTime()
466+
467+
// Best effort to check session time was just updated (delta ~1min)
468+
assert.WithinDuration(t, time.Now(), time.Unix(config.LastSessionUpdateTime(), 0), 60*time.Second)
469+
470+
}
471+
457472
func checkUsageStats(enabled bool, timeStampExist bool, config core_config.Repository, t *testing.T) {
458473
assert.Equal(t, config.UsageStatsEnabled(), enabled)
459474
assert.Equal(t, config.UsageStatsEnabledLastUpdate().IsZero(), !timeStampExist)

bluemix/configuration/core_config/repository.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,9 @@ type Repository interface {
122122

123123
CheckMessageOfTheDay() bool
124124
SetMessageOfTheDayTime()
125+
126+
SetLastSessionUpdateTime()
127+
LastSessionUpdateTime() (session int64)
125128
}
126129

127130
// Deprecated
@@ -266,6 +269,8 @@ func (c repository) RefreshIAMToken() (string, error) {
266269
c.SetIAMRefreshToken(token.RefreshToken)
267270
}
268271

272+
c.SetLastSessionUpdateTime()
273+
269274
return ret, nil
270275
}
271276

@@ -355,6 +360,14 @@ func (c repository) ClearSession() {
355360
c.cfConfig.ClearSession()
356361
}
357362

363+
func (c repository) LastSessionUpdateTime() (session int64) {
364+
return c.bxConfig.LastSessionUpdateTime()
365+
}
366+
367+
func (c repository) SetLastSessionUpdateTime() {
368+
c.bxConfig.SetLastSessionUpdateTime()
369+
}
370+
358371
func NewCoreConfig(errHandler func(error)) ReadWriter {
359372
// config_helpers.MigrateFromOldConfig() // error ignored
360373
return NewCoreConfigFromPath(config_helpers.CFConfigFilePath(), config_helpers.ConfigFilePath(), errHandler)

docs/plugin_developer_guide.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -963,15 +963,18 @@ accessToken := token.AccessToken
963963
newRefreshToken := token.RefreshToken
964964

965965
// optional, set access token and refresh token back to config
966-
967966
config.SetAccessToken(accessToken)
968967
config.SetRefreshToken(newRefreshToken)
968+
969+
// optional, maintain session for long running workloads
970+
request = iam.RefreshSessionRequest(token)
971+
client.RefreshSession(token)
969972
```
970973

971974
### 5.3 VPC Compute Resource Identity Authentication
972975

973976
#### 5.3.1 Get the IAM Access Token
974-
The IBM CLoud CLI supports logging in as a VPC compute resource identity. The CLI will fetch a VPC instance identity token and exchange it for an IAM access token when logging in as a VPC compute resource identity. This access token is stored in configuration once a user successfully logs into the CLI.
977+
The IBM Cloud CLI supports logging in as a VPC compute resource identity. The CLI will fetch a VPC instance identity token and exchange it for an IAM access token when logging in as a VPC compute resource identity. This access token is stored in configuration once a user successfully logs into the CLI.
975978

976979
Plug-ins can invoke `plugin.PluginContext.IsLoggedInAsCRI()` and `plugin.PluginContext.CRIType()` in the CLI SDK to detect whether the user has logged in as a VPC compute resource identity.
977980
You can get the IAM access token resulting from the user logging in as a VPC compute resource identity from the IBM CLoud SDK as follows:

0 commit comments

Comments
 (0)