diff --git a/core/config.go b/core/config.go index efdf986..a1e2578 100644 --- a/core/config.go +++ b/core/config.go @@ -34,6 +34,7 @@ type InfoInterface interface { GetName() string GetCode() string GetLogoPath() string + GetSecret() string } // Config struct. @@ -195,6 +196,11 @@ func (t Info) GetLogoPath() string { return t.LogoPath } +// GetSecret returns secret. +func (t Info) GetSecret() string { + return t.Secret +} + // IsSSLVerificationEnabled returns SSL verification flag (default is true). func (h *HTTPClientConfig) IsSSLVerificationEnabled() bool { if h.SSLVerification == nil { diff --git a/core/one_step_connection.go b/core/one_step_connection.go new file mode 100644 index 0000000..1c073e7 --- /dev/null +++ b/core/one_step_connection.go @@ -0,0 +1,50 @@ +package core + +import ( + "encoding/json" + "net/http" + + "github.com/gin-gonic/gin" + retailcrm "github.com/retailcrm/api-client-go/v2" +) + +// ConnectionConfig returns middleware for the one-step connection configuration route. +func ConnectionConfig(registerURL string, scopes []string) gin.HandlerFunc { + return func(c *gin.Context) { + c.AbortWithStatusJSON(http.StatusOK, retailcrm.ConnectionConfigResponse{ + SuccessfulResponse: retailcrm.SuccessfulResponse{Success: true}, + Scopes: scopes, + RegisterURL: registerURL, + }) + } +} + +// VerifyConnectRequest will verify ConnectRequest and place it into the "request" context field. +func VerifyConnectRequest(secret string) gin.HandlerFunc { + return func(c *gin.Context) { + connectReqData := c.PostForm("register") + if connectReqData == "" { + c.AbortWithStatusJSON(http.StatusOK, retailcrm.ErrorResponse{ErrorMessage: "No data provided"}) + return + } + + var r retailcrm.ConnectRequest + err := json.Unmarshal([]byte(connectReqData), &r) + if err != nil { + c.AbortWithStatusJSON(http.StatusOK, retailcrm.ErrorResponse{ErrorMessage: "Invalid JSON provided"}) + return + } + + if !r.Verify(secret) { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{}) + return + } + + c.Set("request", r) + } +} + +// MustGetConnectRequest will extract retailcrm.ConnectRequest from the request context. +func MustGetConnectRequest(c *gin.Context) retailcrm.ConnectRequest { + return c.MustGet("request").(retailcrm.ConnectRequest) +} diff --git a/core/one_step_connection_test.go b/core/one_step_connection_test.go new file mode 100644 index 0000000..70b7af7 --- /dev/null +++ b/core/one_step_connection_test.go @@ -0,0 +1,166 @@ +package core + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/gin-gonic/gin" + retailcrm "github.com/retailcrm/api-client-go/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMustGetConnectRequest_Success(t *testing.T) { + assert.Equal(t, retailcrm.ConnectRequest{}, MustGetConnectRequest(&gin.Context{ + Keys: map[string]interface{}{ + "request": retailcrm.ConnectRequest{}, + }, + })) +} + +func TestMustGetConnectRequest_Failure(t *testing.T) { + assert.Panics(t, func() { + MustGetConnectRequest(&gin.Context{ + Keys: map[string]interface{}{}, + }) + }) + assert.Panics(t, func() { + MustGetConnectRequest(&gin.Context{}) + }) + assert.Panics(t, func() { + MustGetConnectRequest(nil) + }) +} + +func TestConnectionConfig(t *testing.T) { + scopes := []string{ + "integration_read", + "integration_write", + } + registerURL := "https://example.com" + gin.SetMode(gin.ReleaseMode) + g := gin.New() + g.GET("/", ConnectionConfig(registerURL, scopes)) + + req, err := http.NewRequest(http.MethodGet, "/", nil) + require.NoError(t, err) + + rr := httptest.NewRecorder() + g.ServeHTTP(rr, req) + + var cc retailcrm.ConnectionConfigResponse + require.NoError(t, json.NewDecoder(rr.Body).Decode(&cc)) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, retailcrm.ConnectionConfigResponse{ + SuccessfulResponse: retailcrm.SuccessfulResponse{ + Success: true, + }, + Scopes: scopes, + RegisterURL: registerURL, + }, cc) +} + +func TestVerifyConnectRequest_NoData(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + g := gin.New() + g.POST("/", VerifyConnectRequest("secret")) + + req, err := http.NewRequest(http.MethodPost, "/", nil) + require.NoError(t, err) + + rr := httptest.NewRecorder() + g.ServeHTTP(rr, req) + + var resp retailcrm.ErrorResponse + require.NoError(t, json.NewDecoder(rr.Body).Decode(&resp)) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, retailcrm.ErrorResponse{ErrorMessage: "No data provided"}, resp) +} + +func TestVerifyConnectRequest_InvalidJSON(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + g := gin.New() + g.POST("/", VerifyConnectRequest("secret")) + + req, err := http.NewRequest( + http.MethodPost, "/", strings.NewReader(url.Values{"register": {"invalid json"}}.Encode())) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + rr := httptest.NewRecorder() + g.ServeHTTP(rr, req) + + var resp retailcrm.ErrorResponse + require.NoError(t, json.NewDecoder(rr.Body).Decode(&resp)) + + assert.Equal(t, http.StatusOK, rr.Code) + assert.Equal(t, retailcrm.ErrorResponse{ErrorMessage: "Invalid JSON provided"}, resp) +} + +func TestVerifyConnectRequest_InvalidToken(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + g := gin.New() + g.POST("/", VerifyConnectRequest("secret")) + + data, err := json.Marshal(retailcrm.ConnectRequest{ + Token: "token", + APIKey: "key", + URL: "https://example.com", + }) + require.NoError(t, err) + + req, err := http.NewRequest( + http.MethodPost, "/", strings.NewReader(url.Values{"register": {string(data)}}.Encode())) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + rr := httptest.NewRecorder() + g.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusUnauthorized, rr.Code) + assert.Equal(t, "{}", rr.Body.String()) +} + +func TestVerifyConnectRequest_OK(t *testing.T) { + gin.SetMode(gin.ReleaseMode) + g := gin.New() + g.POST("/", VerifyConnectRequest("secret"), func(c *gin.Context) { + _ = MustGetConnectRequest(c) + c.AbortWithStatus(http.StatusCreated) + }) + + data, err := json.Marshal(retailcrm.ConnectRequest{ + Token: createConnectToken("key", "secret"), + APIKey: "key", + URL: "https://example.com", + }) + require.NoError(t, err) + + req, err := http.NewRequest( + http.MethodPost, "/", strings.NewReader(url.Values{"register": {string(data)}}.Encode())) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + rr := httptest.NewRecorder() + g.ServeHTTP(rr, req) + + assert.Equal(t, http.StatusCreated, rr.Code) +} + +func createConnectToken(apiKey, secret string) string { + mac := hmac.New(sha256.New, []byte(secret)) + if _, err := mac.Write([]byte(apiKey)); err != nil { + panic(err) + } + return hex.EncodeToString(mac.Sum(nil)) +} diff --git a/go.mod b/go.mod index e593180..4b51bb6 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/pkg/errors v0.9.1 - github.com/retailcrm/api-client-go/v2 v2.0.1 + github.com/retailcrm/api-client-go/v2 v2.0.3 github.com/retailcrm/mg-transport-api-client-go v1.1.32 github.com/stretchr/testify v1.7.0 github.com/ugorji/go v1.2.6 // indirect diff --git a/go.sum b/go.sum index 34eb572..42274f1 100644 --- a/go.sum +++ b/go.sum @@ -251,6 +251,10 @@ github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/retailcrm/api-client-go/v2 v2.0.1 h1:wyM0F1VTSJPO8PVEXB0u7s6ZEs0xRCnUu7YdQ1E4UZ8= github.com/retailcrm/api-client-go/v2 v2.0.1/go.mod h1:1yTZl9+gd3+/k0kAJe7sYvC+mL4fqMwIwtnSgSWZlkQ= +github.com/retailcrm/api-client-go/v2 v2.0.2 h1:oFQycGqwcvfgW2JrbeWmPjxH7Wmh9j762c4FRxCDGNs= +github.com/retailcrm/api-client-go/v2 v2.0.2/go.mod h1:1yTZl9+gd3+/k0kAJe7sYvC+mL4fqMwIwtnSgSWZlkQ= +github.com/retailcrm/api-client-go/v2 v2.0.3 h1:7oKwOZgRLM7eEJUvFNhzfnyIJVomy80ffOEBdYpQRIs= +github.com/retailcrm/api-client-go/v2 v2.0.3/go.mod h1:1yTZl9+gd3+/k0kAJe7sYvC+mL4fqMwIwtnSgSWZlkQ= github.com/retailcrm/mg-transport-api-client-go v1.1.32 h1:IBPltSoD5q2PPZJbNC/prK5F9rEVPXVx/ZzDpi7HKhs= github.com/retailcrm/mg-transport-api-client-go v1.1.32/go.mod h1:AWV6BueE28/6SCoyfKURTo4lF0oXYoOKmHTzehd5vAI= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=