Skip to content

Commit 4712ee0

Browse files
Yael-FYael
authored and
Yael
committed
feat(backend): Reject invalid requests.
Signed-off-by: Yael <fishel.yael@gmail.com>
1 parent d84621a commit 4712ee0

6 files changed

+445
-15
lines changed

workspaces/backend/api/errors.go

-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ func (a *App) LogError(r *http.Request, err error) {
4646
a.logger.Error(err.Error(), "method", method, "uri", uri)
4747
}
4848

49-
//nolint:unused
5049
func (a *App) badRequestResponse(w http.ResponseWriter, r *http.Request, err error) {
5150
httpError := &HTTPError{
5251
StatusCode: http.StatusBadRequest,

workspaces/backend/api/validation.go

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
Copyright 2024.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package api
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
23+
"k8s.io/apimachinery/pkg/util/validation"
24+
)
25+
26+
// ValidationError represents a field-specific validation error.
27+
type ValidationError struct {
28+
Field string
29+
Message string
30+
}
31+
32+
// Field represents a field's value and its type for validation.
33+
type Field struct {
34+
Value string
35+
Type string
36+
}
37+
38+
// Error generates an error message for a given validation error.
39+
func Error(err *ValidationError) error {
40+
return fmt.Errorf("request validation failed on %s: %s", err.Field, err.Message)
41+
}
42+
43+
// Validator defines the interface for field validation.
44+
type Validator interface {
45+
Validate(field *Field) error
46+
}
47+
48+
// NotNullValidator ensures the field value is not empty.
49+
type NotNullValidator struct{}
50+
51+
func (v *NotNullValidator) Validate(field *Field) error {
52+
if field.Value == "" {
53+
Error(&ValidationError{
54+
Field: field.Type,
55+
Message: fmt.Sprintf("%s cannot be empty", field.Type),
56+
})
57+
}
58+
return nil
59+
}
60+
61+
// DNSLabelValidator validates that the field value conforms to DNS label standards.
62+
type DNSLabelValidator struct{}
63+
64+
func (v *DNSLabelValidator) Validate(field *Field) error {
65+
if errors := validation.IsDNS1123Label(field.Value); errors != nil {
66+
return Error(&ValidationError{
67+
Field: field.Type,
68+
Message: strings.Join(errors, "; "),
69+
})
70+
}
71+
return nil
72+
}
73+
74+
// DNSSubdomainValidator validates that the field value conforms to DNS subdomain standards.
75+
type DNSSubdomainValidator struct{}
76+
77+
func (v *DNSSubdomainValidator) Validate(field *Field) error {
78+
if errors := validation.IsDNS1123Subdomain(field.Value); errors != nil {
79+
return Error(&ValidationError{
80+
Field: field.Type,
81+
Message: strings.Join(errors, "; "),
82+
})
83+
}
84+
return nil
85+
}
86+
87+
// ValidateWorkspace validates namespace and name of a workspace.
88+
func ValidateWorkspace(namespace string, workspaceName string) error {
89+
if err := ValidateNamespace(namespace, true); err != nil {
90+
return err
91+
}
92+
93+
if err := ValidateWorkspaceName(workspaceName); err != nil {
94+
return err
95+
}
96+
97+
return nil
98+
}
99+
100+
// ValidateNamespace validates the namespace field, ensuring it is not null (if required)
101+
// and conforms to DNS label standards.
102+
func ValidateNamespace(namespace string, required bool) error {
103+
if !required && namespace == "" {
104+
return nil
105+
}
106+
107+
field := Field{namespace, "namespace"}
108+
validators := []Validator{
109+
&NotNullValidator{},
110+
&DNSLabelValidator{},
111+
}
112+
return runValidators(&field, validators)
113+
}
114+
115+
// ValidateWorkspaceName validates the workspace name, ensuring it is not null
116+
// and conforms to DNS label standards.
117+
func ValidateWorkspaceName(workspaceName string) error {
118+
field := Field{workspaceName, "workspace"}
119+
validators := []Validator{
120+
&NotNullValidator{},
121+
&DNSLabelValidator{},
122+
}
123+
return runValidators(&field, validators)
124+
}
125+
126+
// ValidateWorkspaceKind validates the workspace kind, ensuring it is not null
127+
// and conforms to DNS subdomain standards.
128+
func ValidateWorkspaceKind(param string) error {
129+
field := Field{param, "workspacekind"}
130+
validators := []Validator{
131+
&NotNullValidator{},
132+
&DNSSubdomainValidator{},
133+
}
134+
return runValidators(&field, validators)
135+
}
136+
137+
// runValidators applies all validators to a given field.
138+
func runValidators(field *Field, validators []Validator) error {
139+
for _, validator := range validators {
140+
if err := validator.Validate(field); err != nil {
141+
return err
142+
}
143+
}
144+
return nil
145+
}

workspaces/backend/api/workspacekinds_handler.go

+2-3
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ package api
1818

1919
import (
2020
"errors"
21-
"fmt"
2221
"net/http"
2322

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

36-
if name == "" {
37-
a.serverErrorResponse(w, r, fmt.Errorf("workspace kind name is missing"))
35+
if err := ValidateWorkspaceKind(name); err != nil {
36+
a.badRequestResponse(w, r, err)
3837
return
3938
}
4039

workspaces/backend/api/workspacekinds_handler_test.go

+110
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"encoding/json"
2121
"fmt"
2222
"io"
23+
"math/rand"
2324
"net/http"
2425
"net/http/httptest"
2526
"strings"
@@ -265,4 +266,113 @@ var _ = Describe("WorkspaceKinds Handler", func() {
265266
Expect(rs.StatusCode).To(Equal(http.StatusNotFound), "Expected HTTP status 404 Not Found")
266267
})
267268
})
269+
270+
Context("with unsupported request parameters", Ordered, func() {
271+
272+
var (
273+
a App
274+
validResourceName string
275+
invalidResourceName string
276+
validMaxLengthName string
277+
invalidLengthName string
278+
)
279+
280+
// generateResourceName generates a random string of the specified length and allowed chars.
281+
generateResourceName := func(length int) string {
282+
const allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789.-"
283+
284+
var sb strings.Builder
285+
for i := 0; i < length; i++ {
286+
if i == 0 || i == length-1 {
287+
sb.WriteByte(allowedChars[rand.Intn(len(allowedChars)-2)])
288+
} else {
289+
sb.WriteByte(allowedChars[rand.Intn(len(allowedChars))])
290+
}
291+
}
292+
return sb.String()
293+
}
294+
295+
BeforeAll(func() {
296+
validResourceName = "test"
297+
invalidResourceName = validResourceName + string(rune(rand.Intn(0x10FFFF-128)+128))
298+
validMaxLengthName = generateResourceName(253)
299+
invalidLengthName = generateResourceName(254)
300+
301+
repos := repositories.NewRepositories(k8sClient)
302+
a = App{
303+
Config: config.EnvConfig{
304+
Port: 4000,
305+
},
306+
repositories: repos,
307+
}
308+
})
309+
310+
It("should return 400 status code for a invalid workspacekind name", func() {
311+
By("creating the HTTP request")
312+
path := strings.Replace(WorkspacesByNamespacePath, ":"+WorkspaceNamePathParam, invalidResourceName, 1)
313+
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
314+
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
315+
316+
By("executing GetWorkspaceKindHandler")
317+
ps := httprouter.Params{
318+
httprouter.Param{
319+
Key: WorkspaceNamePathParam,
320+
Value: invalidResourceName,
321+
},
322+
}
323+
rr := httptest.NewRecorder()
324+
a.GetWorkspaceKindHandler(rr, req, ps)
325+
rs := rr.Result()
326+
defer rs.Body.Close()
327+
328+
By("verifying the HTTP response status code")
329+
Expect(rs.StatusCode).To(Equal(http.StatusBadRequest), "Expected HTTP status 400 Bad Request")
330+
})
331+
332+
It("should return 400 status code for a workspace longer than 253", func() {
333+
By("creating the HTTP request")
334+
path := strings.Replace(WorkspacesByNamespacePath, ":"+WorkspaceNamePathParam, invalidLengthName, 1)
335+
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
336+
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
337+
338+
By("executing GetWorkspaceKindHandler")
339+
ps := httprouter.Params{
340+
httprouter.Param{
341+
Key: WorkspaceNamePathParam,
342+
Value: invalidLengthName,
343+
},
344+
}
345+
rr := httptest.NewRecorder()
346+
a.GetWorkspaceKindHandler(rr, req, ps)
347+
rs := rr.Result()
348+
defer rs.Body.Close()
349+
350+
By("verifying the HTTP response status code")
351+
Expect(rs.StatusCode).To(Equal(http.StatusBadRequest), "Expected HTTP status 400 Bad Request")
352+
353+
})
354+
355+
It("should return 200 status code for a workspace with a length of 253 characters", func() {
356+
By("creating the HTTP request")
357+
fmt.Printf("Here Should except 253 length params %s %d", validMaxLengthName, len(validMaxLengthName))
358+
path := strings.Replace(WorkspacesByNamespacePath, ":"+WorkspaceNamePathParam, validMaxLengthName, 1)
359+
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
360+
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
361+
362+
By("executing GetWorkspaceKindHandler")
363+
ps := httprouter.Params{
364+
httprouter.Param{
365+
Key: WorkspaceNamePathParam,
366+
Value: validMaxLengthName,
367+
},
368+
}
369+
rr := httptest.NewRecorder()
370+
a.GetWorkspaceKindHandler(rr, req, ps)
371+
rs := rr.Result()
372+
defer rs.Body.Close()
373+
374+
By("verifying the HTTP response status code")
375+
Expect(rs.StatusCode).To(Equal(http.StatusNotFound), "Expected HTTP status 404 Not Found")
376+
})
377+
})
268378
})

workspaces/backend/api/workspaces_handler.go

+18-10
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,9 @@ func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps htt
3737

3838
var workspace models.WorkspaceModel
3939
var err error
40-
if namespace == "" {
41-
a.serverErrorResponse(w, r, fmt.Errorf("namespace is nil"))
42-
return
43-
}
44-
if workspaceName == "" {
45-
a.serverErrorResponse(w, r, fmt.Errorf("workspaceName is nil"))
40+
41+
if err := ValidateWorkspace(namespace, workspaceName); err != nil {
42+
a.badRequestResponse(w, r, err)
4643
return
4744
}
4845

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

70+
if err := ValidateNamespace(namespace, false); err != nil {
71+
a.badRequestResponse(w, r, err)
72+
fmt.Printf("@@%s \n %s", namespace, err)
73+
return
74+
}
75+
7376
var workspaces []models.WorkspaceModel
7477
var err error
7578
if namespace == "" {
@@ -95,8 +98,8 @@ func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps ht
9598
func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
9699
namespace := ps.ByName("namespace")
97100

98-
if namespace == "" {
99-
a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing"))
101+
if err := ValidateNamespace(namespace, true); err != nil {
102+
a.badRequestResponse(w, r, err)
100103
return
101104
}
102105

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

112+
if err := ValidateWorkspace(workspaceModel.Namespace, workspaceModel.Name); err != nil {
113+
a.badRequestResponse(w, r, err)
114+
return
115+
}
116+
109117
workspaceModel.Namespace = namespace
110118

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

133141
if namespace == "" {
134-
a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing"))
142+
a.badRequestResponse(w, r, fmt.Errorf("namespace is missing"))
135143
return
136144
}
137145

138146
if workspaceName == "" {
139-
a.serverErrorResponse(w, r, fmt.Errorf("workspace name is missing"))
147+
a.badRequestResponse(w, r, fmt.Errorf("workspace name is missing"))
140148
return
141149
}
142150

0 commit comments

Comments
 (0)