One-step connection middlewares (#34)

This commit is contained in:
Pavel 2021-11-18 10:43:30 +03:00 committed by GitHub
parent 65f0ecfa6a
commit b0d5488f5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 227 additions and 1 deletions

View File

@ -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 {

View File

@ -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)
}

View File

@ -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))
}

2
go.mod
View File

@ -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

4
go.sum
View File

@ -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=