Skip to content

Commit

Permalink
Add session management : Reuse login session across multiple API requ…
Browse files Browse the repository at this point in the history
…ests (#117)

* Add cookie and token to enable session management

* Save the token exclusively in the event of successful response

* Optimize code handle 403 Forbidden status

* Optimize code to save token for each array

* Mask sensitive info from response

* Fix gosec issues

* Update copyright info
  • Loading branch information
AkshaySainiDell authored Oct 27, 2023
1 parent 4009fe7 commit dfba845
Showing 1 changed file with 75 additions and 14 deletions.
89 changes: 75 additions & 14 deletions api/api.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
*
* Copyright © 2020-2022 Dell Inc. or its subsidiaries. All Rights Reserved.
* Copyright © 2020-2023 Dell Inc. or its subsidiaries. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -26,7 +26,9 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"net/http/cookiejar"
"net/http/httputil"
"net/url"
"os"
Expand All @@ -35,14 +37,16 @@ import (
"regexp"
"strconv"
"strings"
"sync"
"time"
)

var (
debug = false
)
var debug = false

const paginationHeader = "content-range"
const (
paginationHeader = "content-range"
dellEmcToken = "DELL-EMC-TOKEN" // #nosec G101
)

// RequestConfig provide options for the request
type RequestConfig struct {
Expand Down Expand Up @@ -121,11 +125,14 @@ type ClientIMPL struct {
customHTTPHeaders http.Header
logger Logger
apiThrottle TimeoutSemaphoreInterface
loginMutex sync.Mutex
token string
}

// New creates and initialize API client
func New(apiURL string, username string,
password string, insecure bool, defaultTimeout, rateLimit uint64, requestIDKey string) (*ClientIMPL, error) {
password string, insecure bool, defaultTimeout, rateLimit uint64, requestIDKey string,
) (*ClientIMPL, error) {
debug, _ = strconv.ParseBool(os.Getenv("GOPOWERSTORE_DEBUG"))
if apiURL == "" || username == "" || password == "" {
return nil, errors.New("API ApiClient can't be initialized: " +
Expand All @@ -146,9 +153,19 @@ func New(apiURL string, username string,
client = &http.Client{}
}

// Set cookie jar to enable session management via auth_cookie
jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: nil})
if err != nil {
log.Printf("Failed to set cookie jar. error: %s", err)
log.Print("Session management is disabled.")
} else {
client.Jar = jar
log.Print("Session management is enabled.")
}

throttle := NewTimeoutSemaphore(int(defaultTimeout), int(rateLimit), &defaultLogger{})

return &ClientIMPL{
clientImpl := &ClientIMPL{
apiURL: apiURL,
insecure: insecure,
username: username,
Expand All @@ -158,7 +175,12 @@ func New(apiURL string, username string,
requestIDKey: requestIDKey,
logger: &defaultLogger{},
apiThrottle: throttle,
}, nil
}

// Create a login session after the client is initialized
clientImpl.login(context.Background()) // #nosec G104

return clientImpl, nil
}

const errorSeverity = "Error"
Expand Down Expand Up @@ -191,8 +213,10 @@ func buildError(r *http.Response) *ErrorMsg {
s := buf.String()
errMsg = fmt.Sprintf("%s: %s", errMsg, s)
}
return &ErrorMsg{StatusCode: r.StatusCode, Severity: errorSeverity,
Message: errMsg}
return &ErrorMsg{
StatusCode: r.StatusCode, Severity: errorSeverity,
Message: errMsg,
}
}
firstErrMsg := (*apiErrorMsg.Messages)[0]
firstErrMsg.StatusCode = r.StatusCode
Expand All @@ -219,8 +243,8 @@ func (c *ClientIMPL) SetLogger(logger Logger) {
func (c *ClientIMPL) Query(
ctx context.Context,
cfg RequestConfigRenderer,
resp interface{}) (RespMeta, error) {

resp interface{},
) (RespMeta, error) {
config := cfg.RenderRequestConfig()
meta := RespMeta{}
var cancelFuncPtr *func()
Expand Down Expand Up @@ -262,16 +286,48 @@ func (c *ClientIMPL) Query(
case resp == nil:
return meta, nil
case r.StatusCode >= 200 && r.StatusCode < 300:
// Save DELL-EMC-TOKEN if it was a successful response.
token := r.Header.Get(dellEmcToken)
if len(token) != 0 {
c.token = token
}

c.updatePaginationInfoInMeta(&meta, r)
err = json.NewDecoder(r.Body).Decode(resp)
if err == io.EOF {
return meta, nil
}
return meta, err
case r.StatusCode == http.StatusForbidden:
loginResp, err := c.login(ctx)
// Invalid credentials - No need to retry if response of login api was 401 Unauthorized.
if err != nil || loginResp.Status == http.StatusUnauthorized {
return meta, buildError(r)
}

// login successful - resend the failed request
return c.Query(ctx, cfg, resp)
default:
return meta, buildError(r)
}
}

func (c *ClientIMPL) login(ctx context.Context) (RespMeta, error) {
c.loginMutex.Lock()
defer c.loginMutex.Unlock()

type loginDetails []struct {
ID string `json:"id"`
}
var login loginDetails

resp, err := c.Query(ctx,
RequestConfig{
Method: "GET",
Endpoint: "login_session",
}, &login)

return resp, err
}

func addMetaData(req *http.Request, body interface{}) {
Expand Down Expand Up @@ -303,7 +359,8 @@ func (c *ClientIMPL) QueryParamsWithFields(fp FieldProvider) QueryParamsEncoder
}

func (c *ClientIMPL) prepareRequestURL(endpoint, id string, action string,
queryParams QueryParamsEncoder) (string, error) {
queryParams QueryParamsEncoder,
) (string, error) {
requestURL, err := url.Parse(c.apiURL)
if err != nil {
return "", err
Expand All @@ -325,7 +382,8 @@ func (c *ClientIMPL) prepareRequestURL(endpoint, id string, action string,
}

func (c *ClientIMPL) prepareRequest(ctx context.Context, method, requestURL, traceMsg string,
body interface{}) (*http.Request, error) {
body interface{},
) (*http.Request, error) {
var req *http.Request
var err error
if body != nil && !(reflect.ValueOf(body).Kind() == reflect.Ptr && reflect.ValueOf(body).IsNil()) {
Expand All @@ -345,6 +403,9 @@ func (c *ClientIMPL) prepareRequest(ctx context.Context, method, requestURL, tra
}
req = req.WithContext(ctx)
req.SetBasicAuth(c.username, c.password)
if len(c.token) != 0 {
req.Header.Add(dellEmcToken, c.token)
}
for key, values := range c.customHTTPHeaders {
for _, elem := range values {
req.Header.Add(key, elem)
Expand Down

0 comments on commit dfba845

Please sign in to comment.