Skip to content

Commit

Permalink
flushes out logic to validate subscription state
Browse files Browse the repository at this point in the history
  • Loading branch information
s-amann committed Apr 16, 2024
1 parent 186bd52 commit 6a37c78
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 26 deletions.
67 changes: 42 additions & 25 deletions frontend/middleware_validatesubscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ import (
"github.com/Azure/ARO-HCP/internal/api/subscription"
)

const (
UnregisteredSubscriptionStateMessage = "The subscription %s is not registered for this provider %s. Please re-register the subscription."
SubscriptionMissingMessage = "The request is missing required parameter '%s'."
InvalidSubscriptionStateMessage = "The subscription %s is in %s state, please ensure subscription is eligible to use the provider %s."
DeletedSubscriptionMessage = "The subscription %s is deleted and cannot be used to interact with %s."
)

type SubscriptionStateMuxValidator struct {
cache *Cache
}
Expand All @@ -23,44 +30,54 @@ func NewSubscriptionStateMuxValidator(c *Cache) *SubscriptionStateMuxValidator {
}
}

// MiddlewhareValidateSubscriptionState validates the state of the subscription as outlined by
// https://github.com/cloud-and-ai-microsoft/resource-provider-contract/blob/master/v1.0/subscription-lifecycle-api-reference.md
func (s *SubscriptionStateMuxValidator) MiddlewareValidateSubscriptionState(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
subscriptionId := r.PathValue(strings.ToLower(PathSegmentSubscriptionID))
sub, exists := s.cache.GetSubscription(subscriptionId)
if subscriptionId == "" {
arm.WriteError(
w, http.StatusBadRequest,
arm.CloudErrorCodeInvalidParameter, "",
"The request is missing required parameter '%s'.",
SubscriptionMissingMessage,
PathSegmentSubscriptionID)
} else if !exists {
return
}

if !exists {
arm.WriteError(
w, http.StatusBadRequest,
arm.CloudErrorInvalidSubscriptionState, "",
"The subscription %s is not registered for this provider %s. Please re-register the subscription",
UnregisteredSubscriptionStateMessage,
subscriptionId, api.ProviderNamespace)
} else if exists {
r = r.WithContext(context.WithValue(r.Context(), ContextKeySubscriptionState, sub.State))
switch sub.State {
case subscription.Registered:
next(w, r)
case subscription.Unregistered:
arm.WriteError(
w, http.StatusBadRequest,
arm.CloudErrorInvalidSubscriptionState, "",
"The subscription %s is not registered for this provider %s. Please re-register the subscription",
subscriptionId, api.ProviderNamespace)
case subscription.Warned:
// TODO: check request method
next(w, r)
case subscription.Suspended:
// TODO: check request method
return
}

// the subscription exists, store its current state as context
r = r.WithContext(context.WithValue(r.Context(), ContextKeySubscriptionState, sub.State))
switch sub.State {
case subscription.Registered:
next(w, r)
case subscription.Unregistered:
arm.WriteError(
w, http.StatusBadRequest,
arm.CloudErrorInvalidSubscriptionState, "",
UnregisteredSubscriptionStateMessage,
subscriptionId, api.ProviderNamespace)
case subscription.Warned:
fallthrough // Warned has the same behaviour as Suspended
case subscription.Suspended:
if r.Method == http.MethodGet || r.Method == http.MethodDelete {
next(w, r)
case subscription.Deleted:
arm.WriteError(
w, http.StatusBadRequest,
arm.CloudErrorInvalidSubscriptionState, "",
"The subscription %s is deleted and cannot be used to interact with %s",
subscriptionId, api.ProviderNamespace)
} else if r.Method == http.MethodPut || r.Method == http.MethodPatch || r.Method == http.MethodPost {
arm.WriteError(w, http.StatusConflict,
arm.CloudErrorInvalidSubscriptionState, "", InvalidSubscriptionStateMessage, subscriptionId, sub.State, api.ProviderNamespace)
}
case subscription.Deleted:
arm.WriteError(
w, http.StatusBadRequest,
arm.CloudErrorInvalidSubscriptionState, "",
DeletedSubscriptionMessage,
subscriptionId, api.ProviderNamespace)
}
}
207 changes: 207 additions & 0 deletions frontend/middleware_validatesubscription_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package main

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

import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"

"github.com/google/go-cmp/cmp"

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

func TestMiddlewareValidateSubscription(t *testing.T) {
subscriptionId := "1234-5678"
defaultRequestPath := fmt.Sprintf("subscriptions/%s/resourceGroups/xyz", subscriptionId)
cache := NewCache()
middleware := NewSubscriptionStateMuxValidator(cache)

tests := []struct {
name string
subscriptionId string
cachedState subscription.RegistrationState
expectedState subscription.RegistrationState
httpMethod string
requestPath string
expectedError *arm.CloudError
}{
{
name: "subscription is already registered",
cachedState: subscription.Registered,
expectedState: subscription.Registered,
httpMethod: http.MethodGet,
requestPath: defaultRequestPath,
},
{
name: "subscription is missing from path",
httpMethod: http.MethodGet,
cachedState: subscription.Registered,
requestPath: "/resourceGroups/abc",
expectedError: &arm.CloudError{
StatusCode: http.StatusBadRequest,
CloudErrorBody: &arm.CloudErrorBody{
Code: arm.CloudErrorCodeInvalidParameter,
Message: fmt.Sprintf(SubscriptionMissingMessage, PathSegmentSubscriptionID),
},
},
},
{
name: "subscription is not found",
expectedError: &arm.CloudError{
StatusCode: http.StatusBadRequest,
CloudErrorBody: &arm.CloudErrorBody{
Code: arm.CloudErrorInvalidSubscriptionState,
Message: fmt.Sprintf(UnregisteredSubscriptionStateMessage, subscriptionId, api.ProviderNamespace),
},
},
httpMethod: http.MethodGet,
requestPath: defaultRequestPath,
},
{
name: "subscription is deleted",
cachedState: subscription.Deleted,
expectedError: &arm.CloudError{
StatusCode: http.StatusBadRequest,
CloudErrorBody: &arm.CloudErrorBody{
Code: arm.CloudErrorInvalidSubscriptionState,
Message: fmt.Sprintf(DeletedSubscriptionMessage, subscriptionId, api.ProviderNamespace),
},
},
httpMethod: http.MethodGet,
requestPath: defaultRequestPath,
},
{
name: "subscription is unregistered",
cachedState: subscription.Unregistered,
expectedError: &arm.CloudError{
StatusCode: http.StatusBadRequest,
CloudErrorBody: &arm.CloudErrorBody{
Code: arm.CloudErrorInvalidSubscriptionState,
Message: fmt.Sprintf(UnregisteredSubscriptionStateMessage, subscriptionId, api.ProviderNamespace),
},
},
httpMethod: http.MethodGet,
requestPath: defaultRequestPath,
},
{
name: "subscription is suspended - GET is allowed",
cachedState: subscription.Suspended,
expectedState: subscription.Suspended,
httpMethod: http.MethodGet,
requestPath: defaultRequestPath,
},
{
name: "subscription is warned - GET is allowed",
cachedState: subscription.Warned,
expectedState: subscription.Warned,
httpMethod: http.MethodGet,
requestPath: defaultRequestPath,
},
{
name: "subscription is warned - DELETE is allowed",
cachedState: subscription.Warned,
expectedState: subscription.Warned,
httpMethod: http.MethodDelete,
requestPath: defaultRequestPath,
},
{
name: "subscription is warned - PUT is not allowed",
cachedState: subscription.Warned,
httpMethod: http.MethodPut,
expectedError: &arm.CloudError{
StatusCode: http.StatusConflict,
CloudErrorBody: &arm.CloudErrorBody{
Code: arm.CloudErrorInvalidSubscriptionState,
Message: fmt.Sprintf(InvalidSubscriptionStateMessage, subscriptionId, subscription.Warned, api.ProviderNamespace),
},
},
requestPath: defaultRequestPath,
},
{
name: "subscription is suspended - POST is not allowed",
cachedState: subscription.Suspended,
httpMethod: http.MethodPost,
expectedError: &arm.CloudError{
StatusCode: http.StatusConflict,
CloudErrorBody: &arm.CloudErrorBody{
Code: arm.CloudErrorInvalidSubscriptionState,
Message: fmt.Sprintf(InvalidSubscriptionStateMessage, subscriptionId, subscription.Suspended, api.ProviderNamespace),
},
},
requestPath: defaultRequestPath,
},
{
name: "subscription is suspended - PATCH is not allowed",
cachedState: subscription.Suspended,
httpMethod: http.MethodPatch,
expectedError: &arm.CloudError{
StatusCode: http.StatusConflict,
CloudErrorBody: &arm.CloudErrorBody{
Code: arm.CloudErrorInvalidSubscriptionState,
Message: fmt.Sprintf(InvalidSubscriptionStateMessage, subscriptionId, subscription.Suspended, api.ProviderNamespace),
},
},
requestPath: defaultRequestPath,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

if tt.cachedState != "" {
cache.SetSubscription(subscriptionId, &subscription.Subscription{State: tt.cachedState})
}

writer := httptest.NewRecorder()

request, err := http.NewRequest(tt.httpMethod, tt.requestPath, nil)
if err != nil {
t.Fatal(err)
}

// Add a logger to the context so parsing errors will be logged.
request = request.WithContext(context.WithValue(request.Context(), ContextKeyLogger, slog.Default()))
next := func(w http.ResponseWriter, r *http.Request) {
request = r // capture modified request
}
if tt.requestPath == defaultRequestPath {
request.SetPathValue(strings.ToLower(PathSegmentSubscriptionID), subscriptionId)
}

middleware.MiddlewareValidateSubscriptionState(writer, request, next)

// clear the cache for the next test
cache.DeleteSubscription(subscriptionId)

result, ok := request.Context().Value(ContextKeySubscriptionState).(subscription.RegistrationState)
if ok {
if !reflect.DeepEqual(result, tt.expectedState) {
t.Error(cmp.Diff(result, tt.expectedState))
}
}
if tt.expectedState != "" && !ok {
t.Errorf("Expected RegistrationState %s in request context", tt.expectedState)
}
if tt.expectedError != nil {
var actualError *arm.CloudError
body, _ := io.ReadAll(http.MaxBytesReader(writer, writer.Result().Body, 4*megabyte))
_ = json.Unmarshal(body, &actualError)
if (writer.Result().StatusCode != tt.expectedError.StatusCode) || actualError.Code != tt.expectedError.Code || actualError.Message != tt.expectedError.Message {
t.Errorf("unexpected CloudError, wanted %v, got %v", tt.expectedError, actualError)
}
}
})
}
}
10 changes: 9 additions & 1 deletion go.work.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
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/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o=
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA=

0 comments on commit 6a37c78

Please sign in to comment.