diff --git a/README.md b/README.md index 628298e..3e926f8 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Name | Description `GOISILON_PASSWORD` | the password `GOISILON_INSECURE` | whether to skip SSL validation `GOISILON_VOLUMEPATH` | which base path to use when looking for volume directories +`GOISILON_VOLUMEPATH_PERMISSIONS` | permissions for new volume directory ### Initialize a new client with options The following example demonstrates how to explicitly specify options when diff --git a/api/api.go b/api/api.go index fe25713..abaa3bc 100755 --- a/api/api.go +++ b/api/api.go @@ -21,6 +21,7 @@ import ( "crypto/tls" "crypto/x509" "errors" + "fmt" "io" "net/http" "net/url" @@ -41,6 +42,11 @@ const ( headerValContentTypeBinaryOctetStream = "binary/octet-stream" headerKeyContentLength = "Content-Length" defaultVolumesPath = "/ifs/volumes" + defaultVolumesPathPermissions = "0777" + headerISISessToken = "Cookie" + headerISICSRFToken = "X-CSRF-Token" + headerISIReferer = "Referer" + isiSessCsrfToken = "Set-Cookie" ) var ( @@ -107,18 +113,50 @@ type Client interface { // VolumePath returns the path to a volume with the provided name. VolumePath(name string) string + + // SetAuthToken sets the Auth token/Cookie for the HTTP client + SetAuthToken(token string) + + // SetCSRFToken sets the Auth token for the HTTP client + SetCSRFToken(csrf string) + + // SetReferer sets the Referer header + SetReferer(referer string) + + // GetAuthToken gets the Auth token/Cookie for the HTTP client + GetAuthToken() string + + // GetCSRFToken gets the CSRF token for the HTTP client + GetCSRFToken() string + + // GetReferer gets the Referer header + GetReferer() string } type client struct { - http *http.Client - hostname string - username string - groupname string - password string - volumePath string - apiVersion uint8 - apiMinorVersion uint8 - verboseLogging VerboseType + http *http.Client + hostname string + username string + groupname string + password string + volumePath string + volumePathPermissions string + apiVersion uint8 + apiMinorVersion uint8 + verboseLogging VerboseType + sessionCredentials session +} + +type session struct { + sessionCookies string + sessionCSRF string + referer string +} + +type setupConnection struct { + Services []string `json:"services"` + Username string `json:"username"` + Password string `json:"password"` } type VerboseType uint @@ -155,6 +193,9 @@ type ClientOptions struct { // stored. VolumesPath string + // VolumesPathPermissions is the directory permissions for VolumesPath + VolumesPathPermissions string + // Timeout specifies a time limit for requests made by this client. Timeout time.Duration } @@ -171,12 +212,13 @@ func New( } c := &client{ - hostname: hostname, - username: username, - groupname: groupname, - password: password, - volumePath: defaultVolumesPath, - verboseLogging: VerboseType(verboseLogging), + hostname: hostname, + username: username, + groupname: groupname, + password: password, + volumePath: defaultVolumesPath, + volumePathPermissions: defaultVolumesPathPermissions, + verboseLogging: VerboseType(verboseLogging), } c.http = &http.Client{} @@ -186,6 +228,10 @@ func New( c.volumePath = opts.VolumesPath } + if opts.VolumesPathPermissions != "" { + c.volumePathPermissions = opts.VolumesPathPermissions + } + if opts.Timeout != 0 { c.http.Timeout = opts.Timeout } @@ -212,6 +258,7 @@ func New( } } + c.authenticate(ctx, username, password, hostname) resp := &apiVerResponse{} if err := c.Get(ctx, "/platform/latest", "", nil, nil, resp); err != nil && !strings.HasPrefix(err.Error(), "json: ") { @@ -252,7 +299,7 @@ func (c *client) Get( params OrderedValues, headers map[string]string, resp interface{}) error { - return c.DoWithHeaders( + return c.executeWithRetryAuthenticate( ctx, http.MethodGet, path, id, params, headers, nil, resp) } @@ -262,7 +309,7 @@ func (c *client) Post( params OrderedValues, headers map[string]string, body, resp interface{}) error { - return c.DoWithHeaders( + return c.executeWithRetryAuthenticate( ctx, http.MethodPost, path, id, params, headers, body, resp) } @@ -272,7 +319,7 @@ func (c *client) Put( params OrderedValues, headers map[string]string, body, resp interface{}) error { - return c.DoWithHeaders( + return c.executeWithRetryAuthenticate( ctx, http.MethodPut, path, id, params, headers, body, resp) } @@ -282,7 +329,7 @@ func (c *client) Delete( params OrderedValues, headers map[string]string, resp interface{}) error { - return c.DoWithHeaders( + return c.executeWithRetryAuthenticate( ctx, http.MethodDelete, path, id, params, headers, nil, resp) } @@ -292,7 +339,7 @@ func (c *client) Do( params OrderedValues, body, resp interface{}) error { - return c.DoWithHeaders(ctx, method, path, id, params, nil, body, resp) + return c.executeWithRetryAuthenticate(ctx, method, path, id, params, nil, body, resp) } func beginsWithSlash(s string) bool { @@ -443,8 +490,11 @@ func (c *client) DoAndGetResponseBody( } } - // set the username and password - req.SetBasicAuth(c.username, c.password) + if c.GetAuthToken() != "" { + req.Header.Set(headerISISessToken, c.GetAuthToken()) + req.Header.Set(headerISIReferer, c.GetReferer()) + req.Header.Set(headerISICSRFToken, c.GetCSRFToken()) + } var ( isDebugLog bool @@ -492,6 +542,30 @@ func (err *JSONError) Error() string { return err.Err[0].Message } +func (c *client) SetAuthToken(cookie string) { + c.sessionCredentials.sessionCookies = cookie +} + +func (c *client) SetCSRFToken(csrf string) { + c.sessionCredentials.sessionCSRF = csrf +} + +func (c *client) SetReferer(referer string) { + c.sessionCredentials.referer = referer +} + +func (c *client) GetAuthToken() string { + return c.sessionCredentials.sessionCookies +} + +func (c *client) GetCSRFToken() string { + return c.sessionCredentials.sessionCSRF +} + +func (c *client) GetReferer() string { + return c.sessionCredentials.referer +} + func parseJSONError(r *http.Response) error { jsonError := &JSONError{} if err := json.NewDecoder(r.Body).Decode(jsonError); err != nil { @@ -505,3 +579,93 @@ func parseJSONError(r *http.Response) error { return jsonError } + +// Authenticate make a REST API call [/session/1/session] to PowerScale to authenticate the given credentials. +// The response contains the session Cookie, X-CSRF-Token and the client uses it for further communication. +func (c *client) authenticate(ctx context.Context, username string, password string, endpoint string) error { + headers := make(map[string]string, 1) + headers[headerKeyContentType] = headerValContentTypeJSON + var data = &setupConnection{Services: []string{"platform", "namespace"}, Username: username, Password: password} + resp, _, err := c.DoAndGetResponseBody(ctx, http.MethodPost, "/session/1/session", "", nil, headers, data) + if err != nil { + return errors.New(fmt.Sprintf("Authentication error: %v", err)) + } + + if resp != nil { + log.Debug(ctx, "Authentication response code: %d", resp.StatusCode) + defer resp.Body.Close() + + switch { + case resp.StatusCode == 201: + { + log.Debug(ctx, "Authentication successful") + } + case resp.StatusCode == 401: + { + log.Debug(ctx, "Response Code %v", resp) + return errors.New(fmt.Sprintf("Authentication failed. Unable to login to PowerScale. Verify username and password.")) + } + default: + return errors.New(fmt.Sprintf("Authenticate error. Response:")) + } + + headerRes := strings.Join(resp.Header.Values(isiSessCsrfToken), " ") + + startIndex, endIndex, matchStrLen := FetchValueIndexForKey(headerRes, "isisessid=", ";") + if startIndex < 0 || endIndex < 0 { + return errors.New(fmt.Sprintf("Session ID not retrieved")) + } else { + c.SetAuthToken(headerRes[startIndex : startIndex+matchStrLen+endIndex]) + } + + startIndex, endIndex, matchStrLen = FetchValueIndexForKey(headerRes, "isicsrf=", ";") + if startIndex < 0 || endIndex < 0 { + log.Warn(ctx, "Anti-CSRF Token not retrieved") + } else { + c.SetCSRFToken(headerRes[startIndex+matchStrLen : startIndex+matchStrLen+endIndex]) + } + + c.SetReferer(endpoint) + } else { + log.Error(ctx, "Authenticate error: Nil response received") + } + return nil +} + +// executeWithRetryAuthenticate re-authenticates when session credentials become invalid due to time-out or requests exceed. +// it retries the same operation after performing authentication. +func (c *client) executeWithRetryAuthenticate(ctx context.Context, method, uri string, id string, params OrderedValues, headers map[string]string, body, resp interface{}) error { + err := c.DoWithHeaders(ctx, method, uri, id, params, headers, body, resp) + if err == nil { + log.Debug(ctx, "Execution successful on Method: %v, URI: %v", method, uri) + return nil + } + // check if we need to Re-authenticate + if e, ok := err.(*JSONError); ok { + log.Debug(ctx, "Error in response. Method:%s URI:%s Error: %v JSON Error: %+v", method, uri, err, e) + if e.StatusCode == 401 { + log.Debug(ctx, "need to re-authenticate") + // Authenticate then try again + if err := c.authenticate(ctx, c.username, c.password, c.hostname); err != nil { + return fmt.Errorf("authentication failure due to: %v", err) + } + return c.DoWithHeaders(ctx, method, uri, id, params, headers, body, resp) + } + } else { + log.Error(ctx, "Error is not a type of \"*JSONError\". Error:", err) + } + + return err +} + +func FetchValueIndexForKey(l string, match string, sep string) (int, int, int) { + + if strings.Contains(l, match) { + if i := strings.Index(l, match); i != -1 { + if j := strings.Index(l[i+len(match):], sep); j != -1 { + return i, j, len(match) + } + } + } + return -1, -1, len(match) +} diff --git a/api/api_logging.go b/api/api_logging.go index 2831907..d5d7ddb 100644 --- a/api/api_logging.go +++ b/api/api_logging.go @@ -19,14 +19,12 @@ import ( "bufio" "bytes" "context" - "encoding/base64" "fmt" + log "github.com/akutz/gournal" "io" "net/http" "net/http/httputil" "strings" - - log "github.com/akutz/gournal" ) func isBinOctetBody(h http.Header) bool { @@ -46,7 +44,7 @@ func logRequest(ctx context.Context, w io.Writer, req *http.Request, verbose Ver default: //full logging, i.e. print full request message content buf, _ := httputil.DumpRequest(req, !isBinOctetBody(req.Header)) - decodedBuf := decodeAuthorization(buf) + decodedBuf := encryptPassword(buf) WriteIndented(w, decodedBuf) fmt.Fprintln(w) } @@ -112,20 +110,17 @@ func WriteIndented(w io.Writer, b []byte) error { return WriteIndentedN(w, b, 4) } -func decodeAuthorization(buf []byte) []byte { +func encryptPassword(buf []byte) []byte { sc := bufio.NewScanner(bytes.NewReader(buf)) ou := &bytes.Buffer{} var l string for sc.Scan() { l = sc.Text() - if strings.Contains(l, "Authorization") { - base64str := strings.Split(l, " ")[2] - decoded, _ := base64.StdEncoding.DecodeString(base64str) - decodedName := strings.Split(string(decoded), ":")[0] - //l = "Authorization: Decoded Username: " + decodedName - //l = "Authorization: " + decodedName + " [decoded username]" - l = "Authorization: " + decodedName + ":******" + match := `"password":"` + if strings.Contains(l, match) { + startIndex, endIndex, matchStrLen := FetchValueIndexForKey(l, match, `"`) + l = l[:startIndex+matchStrLen] + "****" + l[startIndex+matchStrLen+endIndex:] } fmt.Fprintln(ou, l) } diff --git a/api/v1/api_v1_volumes.go b/api/v1/api_v1_volumes.go index b52b0a2..cb11126 100644 --- a/api/v1/api_v1_volumes.go +++ b/api/v1/api_v1_volumes.go @@ -62,16 +62,16 @@ func CreateIsiVolume( func CreateIsiVolumeWithIsiPath( ctx context.Context, client api.Client, - isiPath, name string) (resp *getIsiVolumesResp, err error) { - return CreateIsiVolumeWithACLAndIsiPath(ctx, client, isiPath, name, defaultACL) + isiPath, name, isiVolumePathPermissions string) (resp *getIsiVolumesResp, err error) { + return CreateIsiVolumeWithACLAndIsiPath(ctx, client, isiPath, name, isiVolumePathPermissions) } // CreateIsiVolumeWithIsiPathMetaData makes a new volume with isiPath on the cluster func CreateIsiVolumeWithIsiPathMetaData( ctx context.Context, client api.Client, - isiPath, name string, metadata map[string]string) (resp *getIsiVolumesResp, err error) { - return CreateIsiVolumeWithACLAndIsiPathMetaData(ctx, client, isiPath, name, defaultACL, metadata) + isiPath, name, isiVolumePathPermissions string, metadata map[string]string) (resp *getIsiVolumesResp, err error) { + return CreateIsiVolumeWithACLAndIsiPathMetaData(ctx, client, isiPath, name, isiVolumePathPermissions, metadata) } // CreateIsiVolumeWithACL makes a new volume on the cluster with the specified permissions diff --git a/api/v5/api_v5_quotas.go b/api/v5/api_v5_quotas.go index b906276..122bf49 100644 --- a/api/v5/api_v5_quotas.go +++ b/api/v5/api_v5_quotas.go @@ -20,8 +20,8 @@ import ( "errors" "fmt" + log "github.com/akutz/gournal" "github.com/dell/goisilon/api" - log "github.com/sirupsen/logrus" ) const ( @@ -77,7 +77,7 @@ func getIsiQuotaLicenseStatus( return "", errors.New("SmartQuotas license status is empty") } - log.Debugf("SmartQuotas license status retrieved : '%s'", lic.STATUS) + log.Debug(ctx, "SmartQuotas license status retrieved : '%s'", lic.STATUS) if !isQuotaLicenseStatusValid(lic.STATUS) { return "", fmt.Errorf("unknown SmartQuotas license status '%s'", lic.STATUS) @@ -94,7 +94,7 @@ func IsQuotaLicenseActivated(ctx context.Context, if err != nil { - log.Errorf("error encountered when retrieving SmartQuotas license info, cannot determine whether SmartQuotas is activated. error : '%v'", err) + log.Error(ctx, "error encountered when retrieving SmartQuotas license info, cannot determine whether SmartQuotas is activated. error : '%v'", err) return false, nil } diff --git a/client.go b/client.go index cd3907d..ef9f70c 100644 --- a/client.go +++ b/client.go @@ -42,7 +42,9 @@ func NewClient(ctx context.Context) (*Client, error) { os.Getenv("GOISILON_USERNAME"), os.Getenv("GOISILON_GROUP"), os.Getenv("GOISILON_PASSWORD"), - os.Getenv("GOISILON_VOLUMEPATH")) + os.Getenv("GOISILON_VOLUMEPATH"), + os.Getenv("GOISILON_VOLUMEPATH_PERMISSIONS")) + } // NewClientWithArgs returns a new Isilon client struct initialized from the supplied arguments. @@ -50,16 +52,17 @@ func NewClientWithArgs( ctx context.Context, endpoint string, insecure bool, verboseLogging uint, - user, group, pass, volumesPath string) (*Client, error) { + user, group, pass, volumesPath string, volumesPathPermissions string) (*Client, error) { timeout, _ := time.ParseDuration(os.Getenv("GOISILON_TIMEOUT")) client, err := api.New( ctx, endpoint, user, pass, group, verboseLogging, &api.ClientOptions{ - Insecure: insecure, - VolumesPath: volumesPath, - Timeout: timeout, + Insecure: insecure, + VolumesPath: volumesPath, + VolumesPathPermissions: volumesPathPermissions, + Timeout: timeout, }) if err != nil { diff --git a/env.sh b/env.sh index 32ce7bb..e0bd27b 100644 --- a/env.sh +++ b/env.sh @@ -8,3 +8,4 @@ GOISILON_GROUP="" GOISILON_PASSWORD="admin" GOISILON_INSECURE="true" GOISILON_VOLUMEPATH="/ifs/data/csi" +GOISILON_VOLUMEPATH_PERMISSIONS="0777" diff --git a/go.mod b/go.mod index 742a711..f685f34 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,7 @@ module github.com/dell/goisilon +go 1.16 + require ( github.com/akutz/gournal v0.5.0 github.com/sirupsen/logrus v1.4.2 diff --git a/go.sum b/go.sum index fe2743a..2e9463a 100644 --- a/go.sum +++ b/go.sum @@ -14,7 +14,9 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/goisilon_test.go b/goisilon_test.go index 5dd991a..89a7278 100644 --- a/goisilon_test.go +++ b/goisilon_test.go @@ -73,7 +73,8 @@ func TestMain(m *testing.M) { os.Getenv("GOISILON_USERNAME"), "", os.Getenv("GOISILON_PASSWORD"), - os.Getenv("GOISILON_VOLUMEPATH")) + os.Getenv("GOISILON_VOLUMEPATH"), + os.Getenv("GOISILON_VOLUMEPATH_PERMISSIONS")) if err != nil { log.WithError(err).Panic(defaultCtx, "error creating test client") diff --git a/volume.go b/volume.go index e86d455..a2f8f57 100644 --- a/volume.go +++ b/volume.go @@ -25,7 +25,6 @@ import ( log "github.com/akutz/gournal" apiv1 "github.com/dell/goisilon/api/v1" apiv2 "github.com/dell/goisilon/api/v2" - logger "github.com/sirupsen/logrus" ) // Volume represents an Isilon Volume (namespace API). @@ -78,9 +77,9 @@ func (c *Client) IsVolumeExistent( err := apiv1.GetIsiVolumeWithoutMetadata(ctx, c.API, name) if err == nil { - logger.Debugf("the query of volume (id '%s', name '%s') did not return an error, regard the volume as existent.", id, name) + log.Debug(ctx, "the query of volume (id '%s', name '%s') did not return an error, regard the volume as existent.", id, name) } else { - logger.Debugf("the query of volume (id '%s', name '%s') returned an error, regard the volume as non-existent. error : '%v'", id, name, err) + log.Debug(ctx, "the query of volume (id '%s', name '%s') returned an error, regard the volume as non-existent. error : '%v'", id, name, err) } return err == nil @@ -97,9 +96,9 @@ func (c *Client) IsVolumeExistentWithIsiPath( err := apiv1.GetIsiVolumeWithoutMetadataWithIsiPath(ctx, c.API, isiPath, name) if err == nil { - logger.Debugf("the query of volume (id '%s', name '%s') did not return an error, regard the volume as existent.", id, name) + log.Debug(ctx, "the query of volume (id '%s', name '%s') did not return an error, regard the volume as existent.", id, name) } else { - logger.Debugf("the query of volume (id '%s', name '%s') returned an error, regard the volume as non-existent. error : '%v'", id, name, err) + log.Debug(ctx, "the query of volume (id '%s', name '%s') returned an error, regard the volume as non-existent. error : '%v'", id, name, err) } return err == nil @@ -134,8 +133,8 @@ func (c *Client) CreateVolume( // CreateVolumeWithIsipath creates a volume with isiPath func (c *Client) CreateVolumeWithIsipath( - ctx context.Context, isiPath, name string) (Volume, error) { - _, err := apiv1.CreateIsiVolumeWithIsiPath(ctx, c.API, isiPath, name) + ctx context.Context, isiPath, name, isiVolumePathPermissions string) (Volume, error) { + _, err := apiv1.CreateIsiVolumeWithIsiPath(ctx, c.API, isiPath, name, isiVolumePathPermissions) if err != nil { return nil, err } @@ -146,8 +145,8 @@ func (c *Client) CreateVolumeWithIsipath( // CreateVolumeWithIsipathMetaData creates a volume with isiPath func (c *Client) CreateVolumeWithIsipathMetaData( - ctx context.Context, isiPath, name string, metadata map[string]string) (Volume, error) { - _, err := apiv1.CreateIsiVolumeWithIsiPathMetaData(ctx, c.API, isiPath, name, metadata) + ctx context.Context, isiPath, name, isiVolumePathPermissions string, metadata map[string]string) (Volume, error) { + _, err := apiv1.CreateIsiVolumeWithIsiPathMetaData(ctx, c.API, isiPath, name, isiVolumePathPermissions, metadata) if err != nil { return nil, err } diff --git a/volume_test.go b/volume_test.go index c809983..1f2d2a6 100644 --- a/volume_test.go +++ b/volume_test.go @@ -125,6 +125,7 @@ func TestVolumeGetCreate(*testing.T) { func TestVolumeGetCreateMetaData(*testing.T) { volumeName := "test_get_create_volume_name" isiPath := "/ifs/data/csi" + isiVolumePathPermissions := "077" testHeader := map[string]string{ "x-csi-pv-name": "pv-name", @@ -138,7 +139,7 @@ func TestVolumeGetCreateMetaData(*testing.T) { } // Add the test volume - testVolume, err := client.CreateVolumeWithIsipathMetaData(defaultCtx, isiPath, volumeName, testHeader) + testVolume, err := client.CreateVolumeWithIsipathMetaData(defaultCtx, isiPath, volumeName, isiVolumePathPermissions, testHeader) if err != nil { panic(err) }