Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

validate Header changes #38

Merged
merged 4 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
217 changes: 217 additions & 0 deletions server/server_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +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 {
schakrad marked this conversation as resolved.
Show resolved Hide resolved
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 {
schakrad marked this conversation as resolved.
Show resolved Hide resolved
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 {
schakrad marked this conversation as resolved.
Show resolved Hide resolved
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 {
schakrad marked this conversation as resolved.
Show resolved Hide resolved
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()
}
Loading