Skip to content

Commit

Permalink
Merge pull request #339 from IBM-Cloud/feat/session-refresh
Browse files Browse the repository at this point in the history
feat: Cherry pick session refresh feature
  • Loading branch information
steveclay authored Sep 1, 2022
2 parents 259a03a + bc98601 commit 7d34e51
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 14 deletions.
29 changes: 29 additions & 0 deletions bluemix/authentication/iam/iam.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -226,13 +227,15 @@ 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)
}

type Config struct {
IAMEndpoint string
TokenEndpoint string // Optional. Default value is <IAMEndpoint>/identity/token
SessionEndpoint string // Optional. Default value is <IAMEndpoint>/v1/sessions
ClientID string
ClientSecret string
UAAClientID string
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
34 changes: 23 additions & 11 deletions bluemix/authentication/iam/iam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -350,14 +356,20 @@ 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 == "" {
mockErrorJson = "Sorry, you are not authorized!"
}
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)
}
Expand Down
15 changes: 15 additions & 0 deletions bluemix/configuration/core_config/bx_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type BXConfigData struct {
SSLDisabled bool
Locale string
MessageOfTheDayTime int64
LastSessionUpdateTime int64
Trace string
ColorEnabled string
HTTPTimeout int
Expand Down Expand Up @@ -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 = ""
Expand Down
15 changes: 15 additions & 0 deletions bluemix/configuration/core_config/bx_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions bluemix/configuration/core_config/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ type Repository interface {

CheckMessageOfTheDay() bool
SetMessageOfTheDayTime()

SetLastSessionUpdateTime()
LastSessionUpdateTime() (session int64)
}

// Deprecated
Expand Down Expand Up @@ -266,6 +269,8 @@ func (c repository) RefreshIAMToken() (string, error) {
c.SetIAMRefreshToken(token.RefreshToken)
}

c.SetLastSessionUpdateTime()

return ret, nil
}

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion bluemix/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions docs/plugin_developer_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 7d34e51

Please sign in to comment.