From 307762d9bd716646af9ae82fba1299e908febd4f Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Thu, 9 Apr 2026 14:53:30 -0700 Subject: [PATCH] Enforce dashboard team name validation in dashboard API --- .../dashboard-api/internal/api/api.gen.go | 97 ++++++++++--------- .../internal/handlers/team_handlers_test.go | 63 ++++++++++++ .../internal/handlers/team_update.go | 31 +++++- spec/openapi-dashboard.yml | 4 +- 4 files changed, 143 insertions(+), 52 deletions(-) diff --git a/packages/dashboard-api/internal/api/api.gen.go b/packages/dashboard-api/internal/api/api.gen.go index 5426f426e7..81407f6ed2 100644 --- a/packages/dashboard-api/internal/api/api.gen.go +++ b/packages/dashboard-api/internal/api/api.gen.go @@ -771,54 +771,55 @@ func RegisterHandlersWithOptions(router gin.IRouter, si ServerInterface, options // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "ANV44NZdX6//GwAA//86rG96N0IAAA==", + "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", + "//96RaIvhUIAAA==", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/packages/dashboard-api/internal/handlers/team_handlers_test.go b/packages/dashboard-api/internal/handlers/team_handlers_test.go index 6eb42fe775..0742a10f43 100644 --- a/packages/dashboard-api/internal/handlers/team_handlers_test.go +++ b/packages/dashboard-api/internal/handlers/team_handlers_test.go @@ -53,6 +53,69 @@ func TestParseUpdateTeamBody_NameNullRejected(t *testing.T) { } } +func TestValidateUpdateTeamName(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input string + expected string + expectedErr string + }{ + { + name: "trimmed valid name", + input: " Acme Inc ", + expected: "Acme Inc", + }, + { + name: "empty after trim", + input: " ", + expectedErr: "Team name cannot be empty", + }, + { + name: "too long", + input: "123456789012345678901234567890123", + expectedErr: "Team name cannot be longer than 32 characters", + }, + { + name: "invalid characters", + input: "Acme!", + expectedErr: "Names can only contain letters and numbers, separated by spaces, underscores, hyphens, or dots", + }, + { + name: "valid separators", + input: "Acme_Inc-1.0", + expected: "Acme_Inc-1.0", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := validateUpdateTeamName(tt.input) + if tt.expectedErr != "" { + if err == nil { + t.Fatalf("expected error %q, got nil", tt.expectedErr) + } + if err.Error() != tt.expectedErr { + t.Fatalf("expected error %q, got %q", tt.expectedErr, err.Error()) + } + + return + } + + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if got != tt.expected { + t.Fatalf("expected %q, got %q", tt.expected, got) + } + }) + } +} + func TestRequireAuthedTeamMatchesPath_Success(t *testing.T) { t.Parallel() diff --git a/packages/dashboard-api/internal/handlers/team_update.go b/packages/dashboard-api/internal/handlers/team_update.go index e844fc0b88..4d016b94c5 100644 --- a/packages/dashboard-api/internal/handlers/team_update.go +++ b/packages/dashboard-api/internal/handlers/team_update.go @@ -6,6 +6,7 @@ import ( "errors" "io" "net/http" + "regexp" "strings" "github.com/gin-gonic/gin" @@ -18,6 +19,8 @@ import ( "github.com/e2b-dev/infra/packages/shared/pkg/telemetry" ) +var teamNamePattern = regexp.MustCompile(`^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$`) + func (s *APIStore) PatchTeamsTeamID(c *gin.Context, teamID api.TeamID) { ctx := c.Request.Context() telemetry.ReportEvent(ctx, "update team") @@ -42,10 +45,15 @@ func (s *APIStore) PatchTeamsTeamID(c *gin.Context, teamID api.TeamID) { return } - if body.NameSet && strings.TrimSpace(body.Name) == "" { - s.sendAPIStoreError(c, http.StatusBadRequest, "Name must not be empty") + if body.NameSet { + name, err := validateUpdateTeamName(body.Name) + if err != nil { + s.sendAPIStoreError(c, http.StatusBadRequest, err.Error()) - return + return + } + + body.Name = name } row, err := s.db.UpdateTeam(ctx, queries.UpdateTeamParams{ @@ -84,6 +92,23 @@ func (b updateTeamBody) NamePtr() *string { return &b.Name } +func validateUpdateTeamName(name string) (string, error) { + trimmedName := strings.TrimSpace(name) + if trimmedName == "" { + return "", errors.New("Team name cannot be empty") + } + + if len(trimmedName) > 32 { + return "", errors.New("Team name cannot be longer than 32 characters") + } + + if !teamNamePattern.MatchString(trimmedName) { + return "", errors.New("Names can only contain letters and numbers, separated by spaces, underscores, hyphens, or dots") + } + + return trimmedName, nil +} + func parseUpdateTeamBody(bodyReader io.Reader) (updateTeamBody, error) { var body updateTeamBody diff --git a/spec/openapi-dashboard.yml b/spec/openapi-dashboard.yml index 9d0a4fbac1..b92f36b4fe 100644 --- a/spec/openapi-dashboard.yml +++ b/spec/openapi-dashboard.yml @@ -460,11 +460,13 @@ components: UpdateTeamRequest: type: object minProperties: 1 + additionalProperties: false properties: name: type: string minLength: 1 - maxLength: 255 + maxLength: 32 + pattern: '^[a-zA-Z0-9]+(?:[ _.-][a-zA-Z0-9]+)*$' profilePictureUrl: type: string nullable: true