Skip to content

Commit

Permalink
feat(backend): Reject invalid requests.
Browse files Browse the repository at this point in the history
Signed-off-by: Yael <[email protected]>
  • Loading branch information
Yael-F authored and Yael committed Jan 16, 2025
1 parent d84621a commit 4712ee0
Show file tree
Hide file tree
Showing 6 changed files with 445 additions and 15 deletions.
1 change: 0 additions & 1 deletion workspaces/backend/api/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ func (a *App) LogError(r *http.Request, err error) {
a.logger.Error(err.Error(), "method", method, "uri", uri)
}

//nolint:unused
func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
httpError := &HTTPError{
StatusCode: http.StatusBadRequest,
Expand Down
145 changes: 145 additions & 0 deletions workspaces/backend/api/validation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
Copyright 2024.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package api

import (
"fmt"
"strings"

"k8s.io/apimachinery/pkg/util/validation"
)

// ValidationError represents a field-specific validation error.
type ValidationError struct {
Field string
Message string
}

// Field represents a field's value and its type for validation.
type Field struct {
Value string
Type string
}

// Error generates an error message for a given validation error.
func Error(err *ValidationError) error {
return fmt.Errorf("request validation failed on %s: %s", err.Field, err.Message)
}

// Validator defines the interface for field validation.
type Validator interface {
Validate(field *Field) error
}

// NotNullValidator ensures the field value is not empty.
type NotNullValidator struct{}

func (v *NotNullValidator) Validate(field *Field) error {
if field.Value == "" {
Error(&ValidationError{
Field: field.Type,
Message: fmt.Sprintf("%s cannot be empty", field.Type),
})
}
return nil
}

// DNSLabelValidator validates that the field value conforms to DNS label standards.
type DNSLabelValidator struct{}

func (v *DNSLabelValidator) Validate(field *Field) error {
if errors := validation.IsDNS1123Label(field.Value); errors != nil {
return Error(&ValidationError{
Field: field.Type,
Message: strings.Join(errors, "; "),
})
}
return nil
}

// DNSSubdomainValidator validates that the field value conforms to DNS subdomain standards.
type DNSSubdomainValidator struct{}

func (v *DNSSubdomainValidator) Validate(field *Field) error {
if errors := validation.IsDNS1123Subdomain(field.Value); errors != nil {
return Error(&ValidationError{
Field: field.Type,
Message: strings.Join(errors, "; "),
})
}
return nil
}

// ValidateWorkspace validates namespace and name of a workspace.
func ValidateWorkspace(namespace string, workspaceName string) error {
if err := ValidateNamespace(namespace, true); err != nil {
return err
}

if err := ValidateWorkspaceName(workspaceName); err != nil {
return err
}

return nil
}

// ValidateNamespace validates the namespace field, ensuring it is not null (if required)
// and conforms to DNS label standards.
func ValidateNamespace(namespace string, required bool) error {
if !required && namespace == "" {
return nil
}

field := Field{namespace, "namespace"}
validators := []Validator{
&NotNullValidator{},
&DNSLabelValidator{},
}
return runValidators(&field, validators)
}

// ValidateWorkspaceName validates the workspace name, ensuring it is not null
// and conforms to DNS label standards.
func ValidateWorkspaceName(workspaceName string) error {
field := Field{workspaceName, "workspace"}
validators := []Validator{
&NotNullValidator{},
&DNSLabelValidator{},
}
return runValidators(&field, validators)
}

// ValidateWorkspaceKind validates the workspace kind, ensuring it is not null
// and conforms to DNS subdomain standards.
func ValidateWorkspaceKind(param string) error {
field := Field{param, "workspacekind"}
validators := []Validator{
&NotNullValidator{},
&DNSSubdomainValidator{},
}
return runValidators(&field, validators)
}

// runValidators applies all validators to a given field.
func runValidators(field *Field, validators []Validator) error {
for _, validator := range validators {
if err := validator.Validate(field); err != nil {
return err
}
}
return nil
}
5 changes: 2 additions & 3 deletions workspaces/backend/api/workspacekinds_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package api

import (
"errors"
"fmt"
"net/http"

"github.com/julienschmidt/httprouter"
Expand All @@ -33,8 +32,8 @@ type WorkspaceKindEnvelope Envelope[models.WorkspaceKindModel]
func (a *App) GetWorkspaceKindHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
name := ps.ByName("name")

if name == "" {
a.serverErrorResponse(w, r, fmt.Errorf("workspace kind name is missing"))
if err := ValidateWorkspaceKind(name); err != nil {
a.badRequestResponse(w, r, err)
return
}

Expand Down
110 changes: 110 additions & 0 deletions workspaces/backend/api/workspacekinds_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"encoding/json"
"fmt"
"io"
"math/rand"
"net/http"
"net/http/httptest"
"strings"
Expand Down Expand Up @@ -265,4 +266,113 @@ var _ = Describe("WorkspaceKinds Handler", func() {
Expect(rs.StatusCode).To(Equal(http.StatusNotFound), "Expected HTTP status 404 Not Found")
})
})

Context("with unsupported request parameters", Ordered, func() {

var (
a App
validResourceName string
invalidResourceName string
validMaxLengthName string
invalidLengthName string
)

// generateResourceName generates a random string of the specified length and allowed chars.
generateResourceName := func(length int) string {
const allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789.-"

var sb strings.Builder
for i := 0; i < length; i++ {
if i == 0 || i == length-1 {
sb.WriteByte(allowedChars[rand.Intn(len(allowedChars)-2)])
} else {
sb.WriteByte(allowedChars[rand.Intn(len(allowedChars))])
}
}
return sb.String()
}

BeforeAll(func() {
validResourceName = "test"
invalidResourceName = validResourceName + string(rune(rand.Intn(0x10FFFF-128)+128))
validMaxLengthName = generateResourceName(253)
invalidLengthName = generateResourceName(254)

repos := repositories.NewRepositories(k8sClient)
a = App{
Config: config.EnvConfig{
Port: 4000,
},
repositories: repos,
}
})

It("should return 400 status code for a invalid workspacekind name", func() {
By("creating the HTTP request")
path := strings.Replace(WorkspacesByNamespacePath, ":"+WorkspaceNamePathParam, invalidResourceName, 1)
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")

By("executing GetWorkspaceKindHandler")
ps := httprouter.Params{
httprouter.Param{
Key: WorkspaceNamePathParam,
Value: invalidResourceName,
},
}
rr := httptest.NewRecorder()
a.GetWorkspaceKindHandler(rr, req, ps)
rs := rr.Result()
defer rs.Body.Close()

By("verifying the HTTP response status code")
Expect(rs.StatusCode).To(Equal(http.StatusBadRequest), "Expected HTTP status 400 Bad Request")
})

It("should return 400 status code for a workspace longer than 253", func() {
By("creating the HTTP request")
path := strings.Replace(WorkspacesByNamespacePath, ":"+WorkspaceNamePathParam, invalidLengthName, 1)
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")

By("executing GetWorkspaceKindHandler")
ps := httprouter.Params{
httprouter.Param{
Key: WorkspaceNamePathParam,
Value: invalidLengthName,
},
}
rr := httptest.NewRecorder()
a.GetWorkspaceKindHandler(rr, req, ps)
rs := rr.Result()
defer rs.Body.Close()

By("verifying the HTTP response status code")
Expect(rs.StatusCode).To(Equal(http.StatusBadRequest), "Expected HTTP status 400 Bad Request")

})

It("should return 200 status code for a workspace with a length of 253 characters", func() {
By("creating the HTTP request")
fmt.Printf("Here Should except 253 length params %s %d", validMaxLengthName, len(validMaxLengthName))
path := strings.Replace(WorkspacesByNamespacePath, ":"+WorkspaceNamePathParam, validMaxLengthName, 1)
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")

By("executing GetWorkspaceKindHandler")
ps := httprouter.Params{
httprouter.Param{
Key: WorkspaceNamePathParam,
Value: validMaxLengthName,
},
}
rr := httptest.NewRecorder()
a.GetWorkspaceKindHandler(rr, req, ps)
rs := rr.Result()
defer rs.Body.Close()

By("verifying the HTTP response status code")
Expect(rs.StatusCode).To(Equal(http.StatusNotFound), "Expected HTTP status 404 Not Found")
})
})
})
28 changes: 18 additions & 10 deletions workspaces/backend/api/workspaces_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,9 @@ func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps htt

var workspace models.WorkspaceModel
var err error
if namespace == "" {
a.serverErrorResponse(w, r, fmt.Errorf("namespace is nil"))
return
}
if workspaceName == "" {
a.serverErrorResponse(w, r, fmt.Errorf("workspaceName is nil"))

if err := ValidateWorkspace(namespace, workspaceName); err != nil {
a.badRequestResponse(w, r, err)
return
}

Expand Down Expand Up @@ -70,6 +67,12 @@ func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps htt
func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
namespace := ps.ByName(NamespacePathParam)

if err := ValidateNamespace(namespace, false); err != nil {
a.badRequestResponse(w, r, err)
fmt.Printf("@@%s \n %s", namespace, err)
return
}

var workspaces []models.WorkspaceModel
var err error
if namespace == "" {
Expand All @@ -95,8 +98,8 @@ func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps ht
func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
namespace := ps.ByName("namespace")

if namespace == "" {
a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing"))
if err := ValidateNamespace(namespace, true); err != nil {
a.badRequestResponse(w, r, err)
return
}

Expand All @@ -106,6 +109,11 @@ func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps
return
}

if err := ValidateWorkspace(workspaceModel.Namespace, workspaceModel.Name); err != nil {
a.badRequestResponse(w, r, err)
return
}

workspaceModel.Namespace = namespace

createdWorkspace, err := a.repositories.Workspace.CreateWorkspace(r.Context(), workspaceModel)
Expand All @@ -131,12 +139,12 @@ func (a *App) DeleteWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps
workspaceName := ps.ByName("name")

if namespace == "" {
a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing"))
a.badRequestResponse(w, r, fmt.Errorf("namespace is missing"))
return
}

if workspaceName == "" {
a.serverErrorResponse(w, r, fmt.Errorf("workspace name is missing"))
a.badRequestResponse(w, r, fmt.Errorf("workspace name is missing"))
return
}

Expand Down
Loading

0 comments on commit 4712ee0

Please sign in to comment.