diff --git a/workspaces/backend/api/errors.go b/workspaces/backend/api/errors.go index e8ec75c6..da17379f 100644 --- a/workspaces/backend/api/errors.go +++ b/workspaces/backend/api/errors.go @@ -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, diff --git a/workspaces/backend/api/validation.go b/workspaces/backend/api/validation.go new file mode 100644 index 00000000..55eab799 --- /dev/null +++ b/workspaces/backend/api/validation.go @@ -0,0 +1,54 @@ +/* +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" + "unicode/utf8" +) + +// ValidateKubernetesResourceName validates one or more Kubernetes resource names. +// It ensures each name meets the following criteria: +// 1. The name must not contain non-ASCII characters. +// 2. The name must not exceed 255 characters in length. +func ValidateKubernetesResourceName(params ...string) error { + for _, param := range params { + if err := NonASCIIValidator(param); err != nil { + return err + } + if err := LengthValidator(param); err != nil { + return err + } + } + return nil +} + +// NonASCIIValidator checks if a given string contains only ASCII characters. +func NonASCIIValidator(param string) error { + if utf8.ValidString(param) && len(param) == len([]rune(param)) { + return nil + } + return fmt.Errorf("Invalid value: '%s' contains non-ASCII characters.", param) +} + +// LengthValidator ensures a given string does not exceed 255 characters. +func LengthValidator(param string) error { + if len(param) > 255 { + return fmt.Errorf("Invalid value: '%s' exceeds the allowed limit of 255 characters.", param) + } + + return nil +} diff --git a/workspaces/backend/api/workspacekinds_handler.go b/workspaces/backend/api/workspacekinds_handler.go index ae4ad8ce..ede05142 100644 --- a/workspaces/backend/api/workspacekinds_handler.go +++ b/workspaces/backend/api/workspacekinds_handler.go @@ -35,6 +35,10 @@ func (a *App) GetWorkspaceKindHandler(w http.ResponseWriter, r *http.Request, ps if name == "" { a.serverErrorResponse(w, r, fmt.Errorf("workspace kind name is missing")) + } + + if err := ValidateKubernetesResourceName(name); err != nil { + a.badRequestResponse(w, r, err) return } diff --git a/workspaces/backend/api/workspacekinds_handler_test.go b/workspaces/backend/api/workspacekinds_handler_test.go index a9519178..44d73204 100644 --- a/workspaces/backend/api/workspacekinds_handler_test.go +++ b/workspaces/backend/api/workspacekinds_handler_test.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "math/rand" "net/http" "net/http/httptest" "strings" @@ -265,4 +266,109 @@ 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 + validAsciiName string + invalidAsciiName string + validMaxLengthName string + invalidLengthName string + ) + + // generateASCII generates a random ASCII string of the specified length. + generateASCII := func(length int) string { + const asciiChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + var sb strings.Builder + for i := 0; i < length; i++ { + sb.WriteByte(asciiChars[rand.Intn(len(asciiChars))]) + } + return sb.String() + } + + BeforeAll(func() { + validAsciiName = "test" + invalidAsciiName = validAsciiName + string(rune(rand.Intn(0x10FFFF-128)+128)) + validMaxLengthName = generateASCII(255) + invalidLengthName = generateASCII(256) + + repos := repositories.NewRepositories(k8sClient) + a = App{ + Config: config.EnvConfig{ + Port: 4000, + }, + repositories: repos, + } + }) + + It("should return 400 status code for a non-ascii workspace", func() { + By("creating the HTTP request") + path := strings.Replace(WorkspacesByNamespacePath, ":"+WorkspaceNamePathParam, invalidAsciiName, 1) + req, err := http.NewRequest(http.MethodGet, path, nil) + Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request") + + By("executing GetWorkspaceKindHandler") + ps := httprouter.Params{ + httprouter.Param{ + Key: WorkspaceNamePathParam, + Value: invalidAsciiName, + }, + } + rr := httptest.NewRecorder() + a.GetWorkspaceKindHandler(rr, req, ps) + rs := rr.Result() + defer rs.Body.Close() // nolint: errcheck + + 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 255", func() { + By("creating the HTTP request") + path := strings.Replace(WorkspacesByNamespacePath, ":"+WorkspaceNamePathParam, invalidLengthName, 1) + req, err := http.NewRequest(http.MethodGet, path, nil) + 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() // nolint: errcheck + + 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 255 characters", func() { + By("creating the HTTP request") + fmt.Println("Here Should except 255 length params") + path := strings.Replace(WorkspacesByNamespacePath, ":"+WorkspaceNamePathParam, validMaxLengthName, 1) + req, err := http.NewRequest(http.MethodGet, path, nil) + 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() // nolint: errcheck + + By("verifying the HTTP response status code") + Expect(rs.StatusCode).To(Equal(http.StatusNotFound), "Expected HTTP status 404 Not Found") + }) + }) }) diff --git a/workspaces/backend/api/workspaces_handler.go b/workspaces/backend/api/workspaces_handler.go index 25c8d080..7b13f0f3 100644 --- a/workspaces/backend/api/workspaces_handler.go +++ b/workspaces/backend/api/workspaces_handler.go @@ -43,6 +43,10 @@ func (a *App) GetWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps htt } if workspaceName == "" { a.serverErrorResponse(w, r, fmt.Errorf("workspaceName is nil")) + } + + if err := ValidateKubernetesResourceName(namespace, workspaceName); err != nil { + a.badRequestResponse(w, r, err) return } @@ -70,6 +74,11 @@ 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 := ValidateKubernetesResourceName(namespace); err != nil { + a.badRequestResponse(w, r, err) + return + } + var workspaces []models.WorkspaceModel var err error if namespace == "" { @@ -97,6 +106,10 @@ func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps if namespace == "" { a.serverErrorResponse(w, r, fmt.Errorf("namespace is missing")) + } + + if err := ValidateKubernetesResourceName(namespace); err != nil { + a.badRequestResponse(w, r, err) return } @@ -106,6 +119,11 @@ func (a *App) CreateWorkspaceHandler(w http.ResponseWriter, r *http.Request, ps return } + if err := ValidateKubernetesResourceName(workspaceModel.Name, workspaceModel.Namespace); err != nil { + a.badRequestResponse(w, r, err) + return + } + workspaceModel.Namespace = namespace createdWorkspace, err := a.repositories.Workspace.CreateWorkspace(r.Context(), workspaceModel) diff --git a/workspaces/backend/api/workspaces_handler_test.go b/workspaces/backend/api/workspaces_handler_test.go index 8dd783a1..5394f4bb 100644 --- a/workspaces/backend/api/workspaces_handler_test.go +++ b/workspaces/backend/api/workspaces_handler_test.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "io" + "math/rand" "net/http" "net/http/httptest" "strings" @@ -505,4 +506,168 @@ var _ = Describe("Workspaces Handler", func() { }) }) + + Context("with unsupported request parameters", Ordered, func() { + + var ( + a App + validAsciiName string + invalidAsciiName string + validMaxLengthName string + invalidLengthName string + ) + + // generateASCII generates a random ASCII string of the specified length. + generateASCII := func(length int) string { + const asciiChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + var sb strings.Builder + for i := 0; i < length; i++ { + sb.WriteByte(asciiChars[rand.Intn(len(asciiChars))]) + } + return sb.String() + } + + BeforeAll(func() { + validAsciiName = "test" + invalidAsciiName = validAsciiName + string(rune(rand.Intn(0x10FFFF-128)+128)) + validMaxLengthName = generateASCII(255) + invalidLengthName = generateASCII(256) + + repos := repositories.NewRepositories(k8sClient) + a = App{ + Config: config.EnvConfig{ + Port: 4000, + }, + repositories: repos, + } + }) + + It("should return 400 status code for a non-ascii namespace", func() { + By("creating the HTTP request") + path := strings.Replace(WorkspacesByNamespacePath, ":"+NamespacePathParam, invalidAsciiName, 1) + req, err := http.NewRequest(http.MethodGet, path, nil) + Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request") + + By("executing GetWorkspacesHandler") + ps := httprouter.Params{ + httprouter.Param{ + Key: NamespacePathParam, + Value: invalidAsciiName, + }, + } + rr := httptest.NewRecorder() + a.GetWorkspacesHandler(rr, req, ps) + rs := rr.Result() + defer rs.Body.Close() // nolint: errcheck + + 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 non-ascii payload params", func() { + By("creating the HTTP request") + workspaceModel := models.WorkspaceModel{ + Name: validAsciiName, + Namespace: invalidAsciiName, + } + + workspaceJSON, err := json.Marshal(workspaceModel) + Expect(err).NotTo(HaveOccurred(), "Failed to marshal WorkspaceModel to JSON") + path := strings.Replace(WorkspacesByNamespacePath, ":"+NamespacePathParam, invalidAsciiName, 1) + + req, err := http.NewRequest(http.MethodPost, path, strings.NewReader(string(workspaceJSON))) + Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request") + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + ps := httprouter.Params{ + httprouter.Param{ + Key: NamespacePathParam, + Value: "namespace", + }, + } + + a.CreateWorkspaceHandler(rr, req, ps) + rs := rr.Result() + defer rs.Body.Close() // nolint: errcheck + + By("verifying the HTTP response status code for creation") + Expect(rs.StatusCode).To(Equal(http.StatusBadRequest), "Expected HTTP status 400 Bad Request") + }) + + It("should return 400 status code for a namespace longer than 255", func() { + By("creating the HTTP request") + path := strings.Replace(WorkspacesByNamespacePath, ":"+NamespacePathParam, invalidLengthName, 1) + req, err := http.NewRequest(http.MethodGet, path, nil) + Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request") + + By("executing GetWorkspacesHandler") + ps := httprouter.Params{ + httprouter.Param{ + Key: NamespacePathParam, + Value: invalidLengthName, + }, + } + rr := httptest.NewRecorder() + a.GetWorkspacesHandler(rr, req, ps) + rs := rr.Result() + defer rs.Body.Close() // nolint: errcheck + + 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 parameters with a length of 255 characters", func() { + By("creating the HTTP request") + path := strings.Replace(WorkspacesByNamespacePath, ":"+NamespacePathParam, validMaxLengthName, 1) + req, err := http.NewRequest(http.MethodGet, path, nil) + Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request") + + By("executing GetWorkspacesHandler") + ps := httprouter.Params{ + httprouter.Param{ + Key: NamespacePathParam, + Value: validMaxLengthName, + }, + } + rr := httptest.NewRecorder() + a.GetWorkspacesHandler(rr, req, ps) + rs := rr.Result() + defer rs.Body.Close() // nolint: errcheck + + By("verifying the HTTP response status code") + Expect(rs.StatusCode).To(Equal(http.StatusOK), "Expected HTTP status 200 OK") // is it supported behavior? returning OK with list of [] workspaces + }) + It("should return 400 status code for a namespace longer than 255", func() { + workspaceModel := models.WorkspaceModel{ + Name: validMaxLengthName, + Namespace: invalidLengthName, + } + + workspaceJSON, err := json.Marshal(workspaceModel) + Expect(err).NotTo(HaveOccurred(), "Failed to marshal WorkspaceModel to JSON") + path := strings.Replace(WorkspacesByNamespacePath, ":"+NamespacePathParam, invalidLengthName, 1) + + req, err := http.NewRequest(http.MethodPost, path, strings.NewReader(string(workspaceJSON))) + Expect(err).NotTo(HaveOccurred(), "Failed to create HTTP request") + req.Header.Set("Content-Type", "application/json") + + rr := httptest.NewRecorder() + ps := httprouter.Params{ + httprouter.Param{ + Key: NamespacePathParam, + Value: "namespace", + }, + } + + a.CreateWorkspaceHandler(rr, req, ps) + rs := rr.Result() + defer rs.Body.Close() // nolint: errcheck + + By("verifying the HTTP response status code for creation") + Expect(rs.StatusCode).To(Equal(http.StatusBadRequest), "Expected HTTP status 400 Bad Request") + + }) + }) })