From 0be95875a7688ff2fd99aaad56380c4e69104f99 Mon Sep 17 00:00:00 2001 From: schakrad Date: Fri, 31 Mar 2023 13:41:19 -0400 Subject: [PATCH 1/4] validate Header changes Signed-off-by: schakrad --- server/server.go | 109 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/server/server.go b/server/server.go index a827f04..d6785de 100644 --- a/server/server.go +++ b/server/server.go @@ -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" ) @@ -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, @@ -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) } @@ -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) @@ -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) } From 24055eb332794e35d856e82c38ea3f48a4115962 Mon Sep 17 00:00:00 2001 From: schakrad Date: Thu, 20 Jul 2023 12:35:55 -0700 Subject: [PATCH 2/4] test cases for server.go Signed-off-by: schakrad --- server/server_test.go | 1 + 1 file changed, 1 insertion(+) create mode 100644 server/server_test.go diff --git a/server/server_test.go b/server/server_test.go new file mode 100644 index 0000000..abb4e43 --- /dev/null +++ b/server/server_test.go @@ -0,0 +1 @@ +package server From c8e6e783aea4bdfc88d1595d8ce188eaa14ce75b Mon Sep 17 00:00:00 2001 From: schakrad Date: Thu, 20 Jul 2023 12:38:12 -0700 Subject: [PATCH 3/4] testcases Signed-off-by: schakrad --- server/server_test.go | 216 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/server/server_test.go b/server/server_test.go index abb4e43..e369d3c 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -1 +1,217 @@ 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 { + 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 { + 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 { + 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 { + 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() +} From defb7f0be3cb46e598a5a30c1a717c93ed0c5484 Mon Sep 17 00:00:00 2001 From: schakrad Date: Thu, 20 Jul 2023 13:02:49 -0700 Subject: [PATCH 4/4] changes Signed-off-by: schakrad --- server/server_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/server_test.go b/server/server_test.go index e369d3c..c2b39c4 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -76,6 +76,7 @@ func TestQueryMetrics(t *testing.T) { } //test invalid scenarios for _, invalidTest := range invalidTests { + invalidTest := invalidTest t.Run(invalidTest.testName, func(t *testing.T) { w := httptest.NewRecorder() ctx, ms := createContextAndNewO11yServer(w) @@ -100,6 +101,7 @@ func TestQueryMetrics(t *testing.T) { }, } for _, validTest := range validTests { + validTest := validTest t.Run(validTest.testName, func(t *testing.T) { w := httptest.NewRecorder() ctx, ms := createContextAndNewO11yServer(w) @@ -156,6 +158,7 @@ func TestDashboardConfig(t *testing.T) { } //test invalid scenarios for _, invalidTest := range invalidTests { + invalidTest := invalidTest t.Run(invalidTest.testName, func(t *testing.T) { w := httptest.NewRecorder() ctx, ms := createContextAndNewO11yServer(w) @@ -180,6 +183,7 @@ func TestDashboardConfig(t *testing.T) { }, } for _, validTest := range validTests { + validTest := validTest t.Run(validTest.testName, func(t *testing.T) { w := httptest.NewRecorder() ctx, ms := createContextAndNewO11yServer(w)