Skip to content

Commit

Permalink
validate Header changes (#38)
Browse files Browse the repository at this point in the history
* validate Header changes

Signed-off-by: schakrad <[email protected]>

* test cases for server.go

Signed-off-by: schakrad <[email protected]>

* testcases

Signed-off-by: schakrad <[email protected]>

* changes

Signed-off-by: schakrad <[email protected]>

---------

Signed-off-by: schakrad <[email protected]>
  • Loading branch information
schakrad authored Jul 20, 2023
1 parent f2e63ab commit 5d714b0
Show file tree
Hide file tree
Showing 2 changed files with 329 additions and 1 deletion.
109 changes: 108 additions & 1 deletion server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import (
"context"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"go.uber.org/zap"
"io/ioutil"
"log"
"net/http"
"os"
"strings"

"github.com/gin-gonic/gin"
"go.uber.org/zap"

tls2 "github.com/argoproj-labs/argocd-metric-ext-server/shared/tls"
)
Expand All @@ -34,6 +36,35 @@ type MetricsProvider interface {
getType() string
}

func validateHeader(header http.Header, headerName string) error {
val, ok := header[headerName]
if !ok {
errMsg := headerName + " header not sent"
return errors.New(errMsg)
}
if len(val) != 1 {
errMsg := "Multiple values for " + headerName + " header sent. Only one is allowed"
return errors.New(errMsg)
}
return nil
}

func validateQueryParam(queryParam string, queryParamName string) error {
if len(queryParam) == 0 {
errMsg := queryParamName + " query param not sent"
return errors.New(errMsg)
}
return nil
}

func validatePathParam(pathParam string, pathParamName string) error {
if len(pathParam) == 0 {
errMsg := pathParamName + " path param not sent"
return errors.New(errMsg)
}
return nil
}

func NewO11yServer(logger *zap.SugaredLogger, port int, enableTLS bool) O11yServer {
return O11yServer{
logger: logger,
Expand All @@ -44,6 +75,7 @@ func NewO11yServer(logger *zap.SugaredLogger, port int, enableTLS bool) O11yServ
func (ms *O11yServer) Run(ctx context.Context) {

err := ms.readConfig()

if err != nil {
panic(err)
}
Expand Down Expand Up @@ -72,6 +104,7 @@ func (ms *O11yServer) Run(ctx context.Context) {
c.String(http.StatusOK, "healthy")
})
handler.GET("/api/applications/:application/groupkinds/:groupkind/rows/:row/graphs/:graph", ms.queryMetrics)

handler.GET("/api/applications/:application/groupkinds/:groupkind/dashboards", ms.dashboardConfig)

address := fmt.Sprintf(":%d", ms.port)
Expand Down Expand Up @@ -110,10 +143,84 @@ func (ms *O11yServer) runWithTLS(address string, handler *gin.Engine) {
}

func (ms *O11yServer) queryMetrics(ctx *gin.Context) {
headers := ctx.Request.Header

if err := validateHeader(headers, "Argocd-Application-Name"); err != nil {
ms.logger.Warn(err)
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
val := headers["Argocd-Application-Name"]
applicationNameHeader := strings.Split(val[0], ":")[1]

if err := validateHeader(headers, "Argocd-Project-Name"); err != nil {
ms.logger.Warn(err)
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
temp := headers["Argocd-Project-Name"]
projectHeader := temp[0]

applicationNameQueryParam := ctx.Query("application_name")

if err := validateQueryParam(applicationNameQueryParam, "application_name"); err != nil {
ms.logger.Warn(err)
ctx.JSON(400, gin.H{"error": err.Error()})
return
}

projectQueryParam := ctx.Query("project")

if err := validateQueryParam(projectQueryParam, "project"); err != nil {
ms.logger.Warn(err)
ctx.JSON(400, gin.H{"error": err.Error()})
return
}

if applicationNameHeader != applicationNameQueryParam {
msg := "Application name mismatch. Value from the header is different from the url."
err := errors.New(msg)
ms.logger.Warn(msg)
ctx.JSON(400, gin.H{"error": err.Error()})
return
}

if projectHeader != projectQueryParam {
msg := "Project mismatch. Value from the header is different from the url."
err := errors.New(msg)
ms.logger.Warn(msg)
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
ms.provider.execute(ctx)
}

func (ms *O11yServer) dashboardConfig(ctx *gin.Context) {
headers := ctx.Request.Header

if err := validateHeader(headers, "Argocd-Application-Name"); err != nil {
ms.logger.Warn(err)
ctx.JSON(400, gin.H{"error": err.Error()})
return
}

val := headers["Argocd-Application-Name"]
applicationNameHeader := strings.Split(val[0], ":")[1]

applicationNamePathParam := ctx.Param("application")

if err := validatePathParam(applicationNamePathParam, "application"); err != nil {
ms.logger.Warn(err)
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
if applicationNameHeader != applicationNamePathParam {
msg := "Application name mismatch. Value from the header is different from the url."
err := errors.New(msg)
ms.logger.Warn(msg)
ctx.JSON(400, gin.H{"error": err.Error()})
return
}
ms.provider.getDashboard(ctx)
}

Expand Down
221 changes: 221 additions & 0 deletions server/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package server

import (
"errors"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/argoproj-labs/argocd-metric-ext-server/shared/logging"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)

type MockO11yServer struct {
}

func (ms MockO11yServer) init() error {
return errors.New("")
}
func (ms MockO11yServer) execute(ctx *gin.Context) {

}

func (ms MockO11yServer) getDashboard(ctx *gin.Context) {

}
func (ms MockO11yServer) getType() string {
return "test"
}

func TestQueryMetrics(t *testing.T) {
invalidTests := []struct {
testName string
header map[string][]string
params map[string]string
queryParams map[string]string
expectedResult int
}{
{testName: "Header Argocd-Application-Name not sent",
header: map[string][]string{"Argocd-Project-Name": []string{"test"}},
params: map[string]string{},
queryParams: map[string]string{"application_name": "test", "project": "default"},
expectedResult: 400,
},
{testName: "Header Argocd-Application-Name multiple values sent",
header: map[string][]string{"Argocd-Application-Name": []string{"argo:test", "argo:test"}, "Argocd-Project-Name": []string{"default"}},
params: map[string]string{},
queryParams: map[string]string{"application_name": "test", "project": "default"},
expectedResult: 400,
},
{testName: "QueryParam application_name not sent",
header: map[string][]string{"Argocd-Application-Name": []string{"argo:test"}, "Argocd-Project-Name": []string{"default"}},
params: map[string]string{},
queryParams: map[string]string{"project": "default"},
expectedResult: 400,
},
{testName: "QueryParam project not sent",
header: map[string][]string{"Argocd-Application-Name": []string{"argo:test"}, "Argocd-Project-Name": []string{"default"}},
params: map[string]string{},
queryParams: map[string]string{"application_name": "test"},
expectedResult: 400,
},
{testName: "Application name mismatch. Value from the header is different from the query param application_name.",
header: map[string][]string{"Argocd-Application-Name": []string{"argo:test1"}, "Argocd-Project-Name": []string{"default"}},
params: map[string]string{},
queryParams: map[string]string{"application_name": "test", "project": "default"},
expectedResult: 400,
},
{testName: "Project name mismatch. Value from the header is different from the query param project.",
header: map[string][]string{"Argocd-Application-Name": []string{"argo:test"}, "Argocd-Project-Name": []string{"defaults"}},
params: map[string]string{},
queryParams: map[string]string{"application_name": "test", "project": "default"},
expectedResult: 400,
},
}
//test invalid scenarios
for _, invalidTest := range invalidTests {
invalidTest := invalidTest
t.Run(invalidTest.testName, func(t *testing.T) {
w := httptest.NewRecorder()
ctx, ms := createContextAndNewO11yServer(w)
MockJsonGet(ctx, invalidTest.header, invalidTest.params, invalidTest.queryParams)
ms.queryMetrics(ctx)
assert.EqualValues(t, invalidTest.expectedResult, w.Code)
})
}

validTests := []struct {
testName string
header map[string][]string
params map[string]string
queryParams map[string]string
expectedResult int
}{
{testName: "All the headers and query params sent and are matched successfully",
header: map[string][]string{"Argocd-Application-Name": []string{"argo:test"}, "Argocd-Project-Name": []string{"default"}},
params: map[string]string{},
queryParams: map[string]string{"application_name": "test", "project": "default"},
expectedResult: 200,
},
}
for _, validTest := range validTests {
validTest := validTest
t.Run(validTest.testName, func(t *testing.T) {
w := httptest.NewRecorder()
ctx, ms := createContextAndNewO11yServer(w)
MockJsonGet(ctx, validTest.header, validTest.params, validTest.queryParams)
ms.queryMetrics(ctx)
assert.EqualValues(t, validTest.expectedResult, w.Code)
})
}
}

func createContextAndNewO11yServer(w *httptest.ResponseRecorder) (ctx *gin.Context, ms O11yServer) {
var port int
var enableTLS bool
logger := logging.NewLogger().Named("metric-sever")
ms = NewO11yServer(logger, port, enableTLS)
var temp MetricsProvider = MockO11yServer{}
ms.provider = temp
ctx = GetTestGinContext(w)
return ctx, ms
}

func TestDashboardConfig(t *testing.T) {
invalidTests := []struct {
testName string
header map[string][]string
params map[string]string
queryParams map[string]string
expectedResult int
}{
{testName: "Header Argocd-Application-Name not sent",
header: map[string][]string{"Argocd-Project-Name": []string{"test"}},
params: map[string]string{"application": "test"},
queryParams: map[string]string{},
expectedResult: 400,
},
{testName: "Header Argocd-Application-Name multiple values sent",
header: map[string][]string{"Argocd-Application-Name": []string{"argo:test", "argo:test"}},
params: map[string]string{"application": "test"},
queryParams: map[string]string{},
expectedResult: 400,
},
{testName: "PathParam application not sent",
header: map[string][]string{"Argocd-Application-Name": []string{"argo:test"}},
params: map[string]string{},
queryParams: map[string]string{},
expectedResult: 400,
},
{testName: "Application name mismatch. Value from the header is different from the path param application.",
header: map[string][]string{"Argocd-Application-Name": []string{"argo:test1"}},
params: map[string]string{"application": "test"},
queryParams: map[string]string{},
expectedResult: 400,
},
}
//test invalid scenarios
for _, invalidTest := range invalidTests {
invalidTest := invalidTest
t.Run(invalidTest.testName, func(t *testing.T) {
w := httptest.NewRecorder()
ctx, ms := createContextAndNewO11yServer(w)
MockJsonGet(ctx, invalidTest.header, invalidTest.params, invalidTest.queryParams)
ms.dashboardConfig(ctx)
assert.EqualValues(t, invalidTest.expectedResult, w.Code)
})
}

validTests := []struct {
testName string
header map[string][]string
params map[string]string
queryParams map[string]string
expectedResult int
}{
{testName: "All Headers and path params are sent and are matched successfully",
header: map[string][]string{"Argocd-Application-Name": []string{"argo:test"}},
params: map[string]string{"application": "test"},
queryParams: map[string]string{},
expectedResult: 200,
},
}
for _, validTest := range validTests {
validTest := validTest
t.Run(validTest.testName, func(t *testing.T) {
w := httptest.NewRecorder()
ctx, ms := createContextAndNewO11yServer(w)
MockJsonGet(ctx, validTest.header, validTest.params, validTest.queryParams)
ms.dashboardConfig(ctx)
assert.EqualValues(t, validTest.expectedResult, w.Code)
})
}
}

// mock gin context
func GetTestGinContext(w *httptest.ResponseRecorder) *gin.Context {
gin.SetMode(gin.TestMode)
ctx, _ := gin.CreateTestContext(w)
ctx.Request = &http.Request{
Header: make(http.Header),
URL: &url.URL{},
}
return ctx
}

// mock get request
func MockJsonGet(c *gin.Context, header map[string][]string, pathParams map[string]string, queryParams map[string]string) {
c.Request.Method = "GET"
c.Request.Header.Set("Content-Type", "application/json")
c.Request.Header = header
for key, value := range pathParams {
c.AddParam(key, value)
}
temp := url.Values{}
for key, value := range queryParams {
temp.Add(key, value)
}
c.Request.URL.RawQuery = temp.Encode()
}

0 comments on commit 5d714b0

Please sign in to comment.