Skip to content

Commit

Permalink
Add static validation to middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
katherinelc321 committed May 15, 2024
1 parent dd28057 commit 21273de
Show file tree
Hide file tree
Showing 8 changed files with 272 additions and 17 deletions.
15 changes: 9 additions & 6 deletions frontend/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ const (
APIVersionKey = "api-version"

// Wildcard path segment names for request multiplexing, must be lowercase as we lowercase the request URL pattern when registering handlers
PageSegmentLocation = "location"
PathSegmentSubscriptionID = "subscriptionid"
PathSegmentResourceGroupName = "resourcegroupname"
PathSegmentResourceName = "resourcename"
PathSegmentDeploymentName = "deploymentname"
PathSegmentActionName = "actionname"
PageSegmentLocation = "location"
PathSegmentSubscriptionID = "subscriptionid"
PathSegmentResourceGroupName = "resourcegroupname"
PathSegmentResourceName = "resourcename"
PathSegmentDeploymentName = "deploymentname"
PathSegmentActionName = "actionname"
PathSegmentOperationID = "operationId"
PathSegmentResourceType = "resourceType"
PathSegmentResourceProviderNamespace = "resourceProviderNamespace"
)
1 change: 1 addition & 0 deletions frontend/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ func NewFrontend(logger *slog.Logger, listener net.Listener, emitter metrics.Emi
MiddlewareBody,
MiddlewareLowercase,
MiddlewareSystemData,
MiddlewareValidateStatic,
metricsMiddleware.Metrics(),
)

Expand Down
2 changes: 1 addition & 1 deletion frontend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/logr v1.4.1
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
Expand Down
77 changes: 77 additions & 0 deletions frontend/middleware_validatestatic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package main

// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.

import (
"errors"
"net/http"
"regexp"
"strings"

"github.com/go-logr/logr"

"github.com/Azure/ARO-HCP/internal/api"
"github.com/Azure/ARO-HCP/internal/api/arm"
)

// Referenced in https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/resource-name-rules#microsoftresources
var rxResourceGroupName = regexp.MustCompile(`^[-a-z0-9_().]{0,89}[-a-z0-9_()]$`)
var rxResourceName = regexp.MustCompile(`^[a-zA-Z0-9-]{3,24}$`)

func MiddlewareValidateStatic(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {

subId := r.PathValue(PathSegmentSubscriptionID)
resourceGroupName := r.PathValue(PathSegmentResourceGroupName)
resourceProviderNamespace := r.PathValue(PathSegmentResourceProviderNamespace)
resourceType := r.PathValue(PathSegmentResourceType)
operationId := r.PathValue(PathSegmentOperationID)
resourceName := r.PathValue(PathSegmentResourceName)

if r.URL.Path != strings.ToLower(r.URL.Path) {
if log, ok := r.Context().Value(LoggerFromContext).(logr.Logger); ok {
log.Error(errors.New("LowerCase error"), "path was not lower case")
}
arm.WriteInternalServerError(w)
return
}

if subId != "" {
valid := api.IsValid(subId)
if !valid {
arm.WriteError(w, http.StatusBadRequest, arm.CloudErrorCodeInvalidSubscriptionID, "", "The provided subscription identifier '%s' is malformed or invalid.", subId)
return
}
}

if resourceGroupName != "" {
if !rxResourceGroupName.MatchString(resourceGroupName) {
arm.WriteError(w, http.StatusBadRequest, arm.CloudErrorCodeResourceGroupNotFound, "", "Resource group '%s' is invalid.", resourceGroupName)
return
}
}

if resourceProviderNamespace != "" {
if resourceProviderNamespace != strings.ToLower(api.ProviderNamespace) {
arm.WriteError(w, http.StatusBadRequest, arm.CloudErrorCodeInvalidResourceNamespace, "", "The resource namespace '%s' is invalid.", resourceProviderNamespace)
return
}
}

if resourceName != "" {
if !rxResourceName.MatchString(resourceName) {
arm.WriteError(w, http.StatusBadRequest, arm.CloudErrorCodeResourceNotFound, "", "The Resource '%s/%s/%s' under resource group '%s' is invalid.", resourceProviderNamespace, resourceType, resourceName, resourceGroupName)
return
}
}

if operationId != "" {
valid := api.IsValid(operationId)
if !valid {
arm.WriteError(w, http.StatusBadRequest, arm.CloudErrorCodeInvalidOperationID, "", "The provided operation identifier '%s' is malformed or invalid.", operationId)
return
}
}

next(w, r)
}
125 changes: 125 additions & 0 deletions frontend/middleware_validatestatic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package main

import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/Azure/ARO-HCP/internal/api/arm"
)

type CloudErrorContainer struct {
Error arm.CloudErrorBody `json:"error"`
}

func TestMiddlewareValidateStatic(t *testing.T) {
// This will act as the next handler if middleware validation passes
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK) // indicate success
})

tests := []struct {
name string
path string
subscriptionID string
resourceGroupName string
resourceProviderNamespace string
resourceType string
resourceName string
operationsId string
expectedStatusCode int
expectedBody string
}{
{
name: "Valid request",
path: "/subscriptions/42d9eac4-d29a-4d6e-9e26-3439758b1491",
subscriptionID: "42d9eac4-d29a-4d6e-9e26-3439758b1491",
expectedStatusCode: http.StatusOK,
},
{
name: "Invalid subscription ID",
path: "/subscriptions/invalid!sub!id",
subscriptionID: "invalid!sub!id",
expectedStatusCode: http.StatusBadRequest,
expectedBody: "The provided subscription identifier 'invalid!sub!id' is malformed or invalid.",
},
{
name: "Invalid resource group name",
path: "/resourcegroups/resourcegroup!",
resourceGroupName: "resourcegroup!",
expectedStatusCode: http.StatusBadRequest,
expectedBody: "Resource group 'resourcegroup!' is invalid.",
},
{
name: "Invalid resource provider namespace",
path: "/providers/invalid",
resourceProviderNamespace: "invalid",
expectedStatusCode: http.StatusBadRequest,
expectedBody: "The resource namespace 'invalid' is invalid.",
},
{
name: "Invalid resource name",
path: "/resourcegroup/providers/microsoft.redhatopenshift/hcpopenshiftcluster/$",
resourceGroupName: "resourcegroup",
resourceProviderNamespace: "microsoft.redhatopenshift",
resourceType: "hcpopenshiftcluster",
resourceName: "$",
expectedStatusCode: http.StatusBadRequest,
expectedBody: "The Resource 'microsoft.redhatopenshift/hcpopenshiftcluster/$' under resource group 'resourcegroup' is invalid.",
},
{
name: "Invalid operation id",
path: "/operations/abc",
operationsId: "abc",
expectedStatusCode: http.StatusBadRequest,
expectedBody: "The provided operation identifier 'abc' is malformed or invalid.",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "http://example.com"+tc.path, nil)

// Use httptest.ResponseRecorder to record the response
w := httptest.NewRecorder()

req.SetPathValue(PathSegmentSubscriptionID, tc.subscriptionID)
req.SetPathValue(PathSegmentResourceGroupName, tc.resourceGroupName)
req.SetPathValue(PathSegmentResourceProviderNamespace, tc.resourceProviderNamespace)
req.SetPathValue(PathSegmentResourceType, tc.resourceType)
req.SetPathValue(PathSegmentResourceName, tc.resourceName)
req.SetPathValue(PathSegmentOperationID, tc.operationsId)

// Execute the middleware
MiddlewareValidateStatic(w, req, nextHandler)

// Check the response status code
if status := w.Code; status != tc.expectedStatusCode {
t.Errorf("handler returned wrong status code: got %v want %v",
status, tc.expectedStatusCode)
}

if tc.expectedStatusCode != http.StatusOK {

var resp CloudErrorContainer
body, err := io.ReadAll(http.MaxBytesReader(w, w.Result().Body, 4*megabyte))
if err != nil {
t.Fatalf("failed to read response body: %v", err)
}
err = json.Unmarshal(body, &resp)
if err != nil {
t.Fatalf("failed to unmarshal response body: %v", err)
}

// Check if the error message contains the expected text
if !strings.Contains(resp.Error.Message, tc.expectedBody) {
t.Errorf("handler returned unexpected body: got %v want %v",
resp.Error.Message, tc.expectedBody)
}
}
})
}
}
34 changes: 34 additions & 0 deletions go.work.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs=
github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk=
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU=
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY=
github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE=
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
Expand Down Expand Up @@ -25,6 +33,9 @@ github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
Expand Down Expand Up @@ -69,17 +80,40 @@ github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcY
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
Expand Down
23 changes: 14 additions & 9 deletions internal/api/arm/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,20 @@ import (

// CloudError codes
const (
CloudErrorCodeInternalServerError = "InternalServerError"
CloudErrorCodeInvalidParameter = "InvalidParameter"
CloudErrorCodeInvalidRequestContent = "InvalidRequestContent"
CloudErrorCodeInvalidResource = "InvalidResource"
CloudErrorCodeInvalidResourceType = "InvalidResourceType"
CloudErrorCodeMultipleErrorsOccurred = "MultipleErrorsOccurred"
CloudErrorCodeUnsupportedMediaType = "UnsupportedMediaType"
CloudErrorCodeNotFound = "NotFound"
CloudErrorInvalidSubscriptionState = "InvalidSubscriptionState"
CloudErrorCodeInternalServerError = "InternalServerError"
CloudErrorCodeInvalidParameter = "InvalidParameter"
CloudErrorCodeInvalidRequestContent = "InvalidRequestContent"
CloudErrorCodeInvalidResource = "InvalidResource"
CloudErrorCodeInvalidResourceType = "InvalidResourceType"
CloudErrorCodeMultipleErrorsOccurred = "MultipleErrorsOccurred"
CloudErrorCodeUnsupportedMediaType = "UnsupportedMediaType"
CloudErrorCodeNotFound = "NotFound"
CloudErrorInvalidSubscriptionState = "InvalidSubscriptionState"
CloudErrorCodeInvalidOperationID = "InvalidOperationID"
CloudErrorCodeResourceNotFound = "ResourceNotFound"
CloudErrorCodeInvalidResourceNamespace = "InvalidResourceNamespace"
CloudErrorCodeResourceGroupNotFound = "ResourceGroupNotFound"
CloudErrorCodeInvalidSubscriptionID = "InvalidSubscriptionID"
)

// CloudError represents a complete resource provider error.
Expand Down
12 changes: 11 additions & 1 deletion internal/api/utils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package api

import "slices"
import (
"slices"

uuid "github.com/google/uuid"
)

// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
Expand Down Expand Up @@ -33,3 +37,9 @@ func StringPtrSliceToStringSlice(s []*string) []string {
}
return out
}

// Check that a string conforms to the UUID format
func IsValid(u string) bool {
err := uuid.Validate(u)
return err == nil
}

0 comments on commit 21273de

Please sign in to comment.