diff --git a/bluemix/authentication/iam/iam.go b/bluemix/authentication/iam/iam.go index 5cc290da..246f61cb 100644 --- a/bluemix/authentication/iam/iam.go +++ b/bluemix/authentication/iam/iam.go @@ -186,6 +186,7 @@ func SetPhoneAuthToken(token string) authentication.TokenOption { type Token struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` + SessionID string `json:"session_id"` TokenType string `json:"token_type"` Scope string `json:"scope"` Expiry time.Time `json:"expiration"` @@ -226,6 +227,7 @@ type Endpoint struct { type Interface interface { GetEndpoint() (*Endpoint, error) + RefreshSession(sessionId string) error GetToken(req *authentication.TokenRequest) (*Token, error) InitiateIMSPhoneFactor(req *authentication.TokenRequest) (authToken string, err error) } @@ -233,6 +235,7 @@ type Interface interface { type Config struct { IAMEndpoint string TokenEndpoint string // Optional. Default value is /identity/token + SessionEndpoint string // Optional. Default value is /v1/sessions ClientID string ClientSecret string UAAClientID string @@ -246,10 +249,18 @@ func (c Config) tokenEndpoint() string { return c.IAMEndpoint + "/identity/token" } +func (c Config) sessionEndpoint() string { + if c.SessionEndpoint != "" { + return c.SessionEndpoint + } + return c.IAMEndpoint + "/v1/sessions" +} + func DefaultConfig(iamEndpoint string) Config { return Config{ IAMEndpoint: iamEndpoint, TokenEndpoint: iamEndpoint + "/identity/token", + SessionEndpoint: iamEndpoint + "/v1/sessions", ClientID: defaultClientID, ClientSecret: defaultClientSecret, UAAClientID: defaultUAAClientID, @@ -307,6 +318,24 @@ func (c *client) GetToken(tokenReq *authentication.TokenRequest) (*Token, error) return &ret, nil } +// RefreshSession maintains the session state. Useful for async workloads +// @param sessionID string - the session ID +func (c *client) RefreshSession(sessionID string) error { + // If no session ID is provided there is no need to refresh + if sessionID == "" { + return nil + } + url := fmt.Sprintf("%s/%s/state", c.config.sessionEndpoint(), sessionID) + r := rest.PatchRequest(url). + Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", c.config.ClientID, c.config.ClientSecret)))) + + if err := c.doRequest(r, nil); err != nil { + return err + } + + return nil +} + func (c *client) InitiateIMSPhoneFactor(tokenReq *authentication.TokenRequest) (authToken string, err error) { v := make(url.Values) tokenReq.SetValue(v) diff --git a/bluemix/authentication/iam/iam_test.go b/bluemix/authentication/iam/iam_test.go index 2a57206c..9e6e00cc 100644 --- a/bluemix/authentication/iam/iam_test.go +++ b/bluemix/authentication/iam/iam_test.go @@ -13,6 +13,7 @@ import ( ) const ( + crAuthTestSessionId string = "C-6376c629-9808-4447-8751-65a2d9414fx" crAuthMockIAMProfileName string = "iam-user-123" crAuthMockIAMProfileID string = "iam-id-123" 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) { func TestGetTokenOneFromServerApiErrorWithProfileNameAndID(t *testing.T) { errorCases := []struct { - errorCode string + errorCode string errorMessage string - }{ { - errorCode: InvalidTokenErrorCode, + errorCode: InvalidTokenErrorCode, errorMessage: "invalid token", }, { - errorCode: RefreshTokenExpiryErrorCode, + errorCode: RefreshTokenExpiryErrorCode, errorMessage: "refresh token expired", }, { - errorCode: ExternalAuthenticationErrorCode, + errorCode: ExternalAuthenticationErrorCode, errorMessage: "External authentication failed", }, { - errorCode: SessionInactiveErrorCode, + errorCode: SessionInactiveErrorCode, errorMessage: "sdf", }, } - for _, errorCase := range errorCases { errorJson := fmt.Sprintf(`{"errorCode": "%s", "errorMessage": "%s", "errorDetails": "", "requirements": {"code": "", "error": ""}}`, errorCase.errorCode, errorCase.errorMessage) server := startMockIAMServerForCRExchange(t, 1, http.StatusUnauthorized, errorJson) @@ -296,13 +295,20 @@ func TestGetTokenOneFromServerApiErrorWithProfileNameAndID(t *testing.T) { IAMToken, err := mockClient.GetToken(tokenReq) assert.NotNil(t, err) assert.Nil(t, IAMToken) - assert.Contains(t, err.Error(),errorCase.errorMessage) - - + assert.Contains(t, err.Error(), errorCase.errorMessage) } +} +func TestRefreshSession(t *testing.T) { + server := startMockIAMServerForCRExchange(t, 1, http.StatusAccepted, "") + defer server.Close() + mockIAMEndpoint := server.URL + mockConfig := DefaultConfig(mockIAMEndpoint) + mockClient := NewClient(mockConfig, rest.NewClient()) + err := mockClient.RefreshSession(crAuthTestSessionId) + assert.Nil(t, err) } // startMockIAMServerForCRExchange will start a mock server endpoint that supports both the @@ -350,7 +356,7 @@ func startMockIAMServerForCRExchange(t *testing.T, call int, statusCode int, err if errorJson == "" { mockErrorJson = "Sorry, bad request!" } - fmt.Fprint(res, mockErrorJson) + fmt.Fprint(res, mockErrorJson) case http.StatusUnauthorized: if errorJson == "" { @@ -358,6 +364,12 @@ func startMockIAMServerForCRExchange(t *testing.T, call int, statusCode int, err } fmt.Fprint(res, mockErrorJson) } + } else if operationPath == fmt.Sprintf("/v1/sessions/%s/state", crAuthTestSessionId) { + username, password, ok := req.BasicAuth() + assert.True(t, ok) + assert.Equal(t, defaultClientID, username) + assert.Equal(t, defaultClientSecret, password) + res.WriteHeader(statusCode) } else { assert.Fail(t, "unknown operation path: "+operationPath) } diff --git a/bluemix/configuration/core_config/bx_config.go b/bluemix/configuration/core_config/bx_config.go index b23930df..40fc52e4 100644 --- a/bluemix/configuration/core_config/bx_config.go +++ b/bluemix/configuration/core_config/bx_config.go @@ -50,6 +50,7 @@ type BXConfigData struct { SSLDisabled bool Locale string MessageOfTheDayTime int64 + LastSessionUpdateTime int64 Trace string ColorEnabled string HTTPTimeout int @@ -727,6 +728,20 @@ func (c *bxConfig) SetMessageOfTheDayTime() { }) } +func (c *bxConfig) SetLastSessionUpdateTime() { + c.write(func() { + c.data.LastSessionUpdateTime = time.Now().Unix() + }) +} + +func (c *bxConfig) LastSessionUpdateTime() (session int64) { + c.read(func() { + session = c.data.LastSessionUpdateTime + }) + + return +} + func (c *bxConfig) ClearSession() { c.write(func() { c.data.IAMToken = "" diff --git a/bluemix/configuration/core_config/bx_config_test.go b/bluemix/configuration/core_config/bx_config_test.go index e28ba335..3d329074 100644 --- a/bluemix/configuration/core_config/bx_config_test.go +++ b/bluemix/configuration/core_config/bx_config_test.go @@ -454,6 +454,21 @@ func TestMOD(t *testing.T) { t.Cleanup(cleanupConfigFiles) } +func TestLastUpdateSessionTime(t *testing.T) { + + config := prepareConfigForCLI(`{}`, t) + + // check initial state + assert.Empty(t, config.LastSessionUpdateTime()) + + // Set last session update time and check that the timestamp is set + config.SetLastSessionUpdateTime() + + // Best effort to check session time was just updated (delta ~1min) + assert.WithinDuration(t, time.Now(), time.Unix(config.LastSessionUpdateTime(), 0), 60*time.Second) + +} + func checkUsageStats(enabled bool, timeStampExist bool, config core_config.Repository, t *testing.T) { assert.Equal(t, config.UsageStatsEnabled(), enabled) assert.Equal(t, config.UsageStatsEnabledLastUpdate().IsZero(), !timeStampExist) diff --git a/bluemix/configuration/core_config/repository.go b/bluemix/configuration/core_config/repository.go index 910520ab..7465cf05 100644 --- a/bluemix/configuration/core_config/repository.go +++ b/bluemix/configuration/core_config/repository.go @@ -122,6 +122,9 @@ type Repository interface { CheckMessageOfTheDay() bool SetMessageOfTheDayTime() + + SetLastSessionUpdateTime() + LastSessionUpdateTime() (session int64) } // Deprecated @@ -266,6 +269,8 @@ func (c repository) RefreshIAMToken() (string, error) { c.SetIAMRefreshToken(token.RefreshToken) } + c.SetLastSessionUpdateTime() + return ret, nil } @@ -355,6 +360,14 @@ func (c repository) ClearSession() { c.cfConfig.ClearSession() } +func (c repository) LastSessionUpdateTime() (session int64) { + return c.bxConfig.LastSessionUpdateTime() +} + +func (c repository) SetLastSessionUpdateTime() { + c.bxConfig.SetLastSessionUpdateTime() +} + func NewCoreConfig(errHandler func(error)) ReadWriter { // config_helpers.MigrateFromOldConfig() // error ignored return NewCoreConfigFromPath(config_helpers.CFConfigFilePath(), config_helpers.ConfigFilePath(), errHandler) diff --git a/bluemix/version.go b/bluemix/version.go index c0ee85ce..b2df7e3a 100644 --- a/bluemix/version.go +++ b/bluemix/version.go @@ -3,7 +3,7 @@ package bluemix import "fmt" // Version is the SDK version -var Version = VersionType{Major: 0, Minor: 11, Build: 0} +var Version = VersionType{Major: 0, Minor: 12, Build: 0} // VersionType describe version info type VersionType struct { diff --git a/docs/plugin_developer_guide.md b/docs/plugin_developer_guide.md index a21d8e6e..fc816d0e 100644 --- a/docs/plugin_developer_guide.md +++ b/docs/plugin_developer_guide.md @@ -963,15 +963,18 @@ accessToken := token.AccessToken newRefreshToken := token.RefreshToken // optional, set access token and refresh token back to config - config.SetAccessToken(accessToken) config.SetRefreshToken(newRefreshToken) + +// optional, maintain session for long running workloads +request = iam.RefreshSessionRequest(token) +client.RefreshSession(token) ``` ### 5.3 VPC Compute Resource Identity Authentication #### 5.3.1 Get the IAM Access Token -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. +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. 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. 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: