Skip to content

Commit a5c1b40

Browse files
Yael-FYael
authored andcommitted
feat(backend): Reject invalid requests.
Signed-off-by: Yael <[email protected]>
1 parent d84621a commit a5c1b40

File tree

6 files changed

+443
-15
lines changed

6 files changed

+443
-15
lines changed

workspaces/backend/api/errors.go

Lines changed: 0 additions & 1 deletion
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

Lines changed: 145 additions & 0 deletions
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

Lines changed: 2 additions & 3 deletions
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

Lines changed: 109 additions & 0 deletions
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,112 @@ 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)-1)])
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(WorkspaceKindsByNamePath, ":"+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(WorkspaceKindsByNamePath, ":"+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+
path := strings.Replace(WorkspaceKindsByNamePath, ":"+WorkspaceNamePathParam, validMaxLengthName, 1)
358+
req, err := http.NewRequest(http.MethodGet, path, http.NoBody)
359+
Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request")
360+
361+
By("executing GetWorkspaceKindHandler")
362+
ps := httprouter.Params{
363+
httprouter.Param{
364+
Key: WorkspaceNamePathParam,
365+
Value: validMaxLengthName,
366+
},
367+
}
368+
rr := httptest.NewRecorder()
369+
a.GetWorkspaceKindHandler(rr, req, ps)
370+
rs := rr.Result()
371+
defer rs.Body.Close()
372+
373+
By("verifying the HTTP response status code")
374+
Expect(rs.StatusCode).To(Equal(http.StatusNotFound), "Expected HTTP status 404 Not Found")
375+
})
376+
})
268377
})

workspaces/backend/api/workspaces_handler.go

Lines changed: 17 additions & 10 deletions
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,11 @@ 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+
return
73+
}
74+
7375
var workspaces []models.WorkspaceModel
7476
var err error
7577
if namespace == "" {
@@ -95,8 +97,8 @@ func (a *App) GetWorkspacesHandler(w http.ResponseWriter, r *http.Request, ps ht
9597
func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
9698
namespace := ps.ByName("namespace")
9799

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

@@ -106,6 +108,11 @@ func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps
106108
return
107109
}
108110

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

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

133140
if namespace == "" {
134-
a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing"))
141+
a.badRequestResponse(w, r, fmt.Errorf("namespace is missing"))
135142
return
136143
}
137144

138145
if workspaceName == "" {
139-
a.serverErrorResponse(w, r, fmt.Errorf("workspace name is missing"))
146+
a.badRequestResponse(w, r, fmt.Errorf("workspace name is missing"))
140147
return
141148
}
142149

0 commit comments

Comments
 (0)