diff --git a/src/app/model.go b/src/app/model.go index ce29f04..67c6e3f 100644 --- a/src/app/model.go +++ b/src/app/model.go @@ -36,7 +36,11 @@ type Service struct { AuthorizedServices map[string]AuthorizedService `toml:"AuthorizedServices"` APIBasePort string EnableJWTAuth bool // Enable JWT authentication for service-to-service communication - FIX FIXConfig + // ServiceAPIKey is the shared secret that authenticates qbymarouter (and any + // other internal service) when posting to /qfixdpl/v1/quotes and + // /qfixdpl/v1/messages. Must match the DPLAPIKey configured on the caller. + ServiceAPIKey string + FIX FIXConfig } type FIXConfig struct { diff --git a/src/client/api/rest/controller.go b/src/client/api/rest/controller.go index 47fbf12..1a3642f 100644 --- a/src/client/api/rest/controller.go +++ b/src/client/api/rest/controller.go @@ -348,7 +348,7 @@ func (cont *Controller) AllMessages(ctx *gin.Context) { return } - if req.APIKey != "1234" { + if !cont.checkServiceAPIKey(req.APIKey) { ctx.JSON(http.StatusUnauthorized, HTTPError{Error: "Not allowed to perform this request"}) return } @@ -356,6 +356,13 @@ func (cont *Controller) AllMessages(ctx *gin.Context) { ctx.JSON(http.StatusOK, cont.tradeProvider.GetAllMessages(req.In, req.Out)) } +// checkServiceAPIKey returns true when the provided key matches the configured +// service-to-service shared secret. Empty configured key is always rejected +// to avoid open authentication when misconfigured. +func (cont *Controller) checkServiceAPIKey(key string) bool { + return cont.config.ServiceAPIKey != "" && key == cont.config.ServiceAPIKey +} + // GetPendingQuoteRequests godoc // @Summary List pending QuoteRequests // @Description Returns all QuoteRequests received from TW that have not been quoted yet by the dealer @@ -382,12 +389,19 @@ func (cont *Controller) GetPendingQuoteRequests(ctx *gin.Context) { // @Failure 500 {object} HTTPError // @Router /qfixdpl/v1/quotes [post] func (cont *Controller) SendQuote(ctx *gin.Context) { + setHeaders(ctx, cont.config) + var req SendQuoteRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.JSON(http.StatusBadRequest, HTTPError{Error: err.Error()}) return } + if !cont.checkServiceAPIKey(req.APIKey) { + ctx.JSON(http.StatusUnauthorized, HTTPError{Error: "Not allowed to perform this request"}) + return + } + price, err := decimal.NewFromString(req.Price) if err != nil { ctx.JSON(http.StatusBadRequest, HTTPError{Error: "invalid price: " + err.Error()}) diff --git a/src/client/api/rest/model.go b/src/client/api/rest/model.go index f136d39..2e30da4 100644 --- a/src/client/api/rest/model.go +++ b/src/client/api/rest/model.go @@ -18,6 +18,7 @@ type Session struct { } type SendQuoteRequest struct { + APIKey string `json:"APIKey" binding:"required"` QuoteReqID string `json:"QuoteReqID" binding:"required"` Price string `json:"Price" binding:"required" example:"99.6"` } diff --git a/src/client/api/rest/routes.go b/src/client/api/rest/routes.go index 5003591..cf517ae 100644 --- a/src/client/api/rest/routes.go +++ b/src/client/api/rest/routes.go @@ -24,10 +24,11 @@ func SetRoutes(api *API) { qfixdpl.GET("/trades", cont.GetTrades) qfixdpl.GET("/trades/:quoteReqID/logs", cont.GetLogs) qfixdpl.GET("/quote-requests", cont.GetPendingQuoteRequests) - qfixdpl.POST("/quotes", cont.SendQuote) - msgs := v1.Group("/") - msgs.POST("/messages", cont.AllMessages) + // services group: API-key auth via body, no session cookie required. + services := v1.Group("/") + services.POST("/messages", cont.AllMessages) + services.POST("/quotes", cont.SendQuote) backoffice := qfixdpl.Group("/backoffice") backoffice.Use(cont.BackOfficeUser) diff --git a/src/client/api/rest/server.go b/src/client/api/rest/server.go index 7e36bf1..3abb7da 100644 --- a/src/client/api/rest/server.go +++ b/src/client/api/rest/server.go @@ -39,6 +39,10 @@ type Config struct { AuthorizedServices map[string]app.AuthorizedService `toml:"AuthorizedServices"` Port string EnableJWTAuth bool + // ServiceAPIKey authenticates internal services (qbymarouter, etc.) calling + // /qfixdpl/v1/quotes and /qfixdpl/v1/messages. Compared against the APIKey + // field in the request body. + ServiceAPIKey string } func New(userData app.UserDataProvider, storeInstance *store.Store, tradeProvider TradeProvider, config Config, notify domain.Notifier) *API { diff --git a/src/cmd/service/service.go b/src/cmd/service/service.go index b2e79be..1586e07 100644 --- a/src/cmd/service/service.go +++ b/src/cmd/service/service.go @@ -51,6 +51,7 @@ func Runner(cfg app.Config) error { External: cfg.External, AuthorizedServices: cfg.AuthorizedServices, EnableJWTAuth: cfg.EnableJWTAuth, + ServiceAPIKey: cfg.ServiceAPIKey, } api := rest.New(userData, appStore, fixManager, apiConfig, notify)