From 9e81f962f3762cc5549a1c54ab4b422248fa1f5e Mon Sep 17 00:00:00 2001 From: Milan Pavlik Date: Wed, 23 Nov 2022 21:43:05 +0000 Subject: [PATCH] [pat] Validate PAT name --- .../public-api-server/pkg/apiv1/tokens.go | 25 ++++++++++++++++--- .../pkg/apiv1/tokens_test.go | 16 ++++++++++++ .../gitpod/experimental/v1/tokens.proto | 3 ++- .../go/experimental/v1/tokens.pb.go | 3 ++- .../src/gitpod/experimental/v1/tokens_pb.ts | 3 ++- 5 files changed, 44 insertions(+), 6 deletions(-) diff --git a/components/public-api-server/pkg/apiv1/tokens.go b/components/public-api-server/pkg/apiv1/tokens.go index f9473f672951b9..e10b516775bf5e 100644 --- a/components/public-api-server/pkg/apiv1/tokens.go +++ b/components/public-api-server/pkg/apiv1/tokens.go @@ -8,6 +8,7 @@ import ( "context" "errors" "fmt" + "regexp" "strings" connect "github.com/bufbuild/connect-go" @@ -45,9 +46,9 @@ type TokensService struct { func (s *TokensService) CreatePersonalAccessToken(ctx context.Context, req *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error) { tokenReq := req.Msg.GetToken() - name := strings.TrimSpace(tokenReq.GetName()) - if name == "" { - return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("Token Name is a required parameter.")) + name, err := validatePersonalAccessTokenName(tokenReq.GetName()) + if err != nil { + return nil, err } expiry := tokenReq.GetExpirationTime() @@ -328,3 +329,21 @@ func personalAccessTokenToAPI(t db.PersonalAccessToken, value string) *v1.Person CreatedAt: timestamppb.New(t.CreatedAt), } } + +var ( + // alpha-numeric characters, dashes, underscore, spaces, between 3 and 63 chars + personalAccessTokenNameRegex = regexp.MustCompile(`^[a-zA-Z0-9-_ ]{3,63}$`) +) + +func validatePersonalAccessTokenName(name string) (string, error) { + trimmed := strings.TrimSpace(name) + if trimmed == "" { + return "", connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Token Name is a required parameter, but got empty.")) + } + + if !personalAccessTokenNameRegex.MatchString(trimmed) { + return "", connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Token Name is required to match regexp %s.", personalAccessTokenNameRegex.String())) + } + + return trimmed, nil +} diff --git a/components/public-api-server/pkg/apiv1/tokens_test.go b/components/public-api-server/pkg/apiv1/tokens_test.go index 4d6cf138372347..254672b60ae595 100644 --- a/components/public-api-server/pkg/apiv1/tokens_test.go +++ b/components/public-api-server/pkg/apiv1/tokens_test.go @@ -9,6 +9,7 @@ import ( "fmt" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -74,6 +75,21 @@ func TestTokensService_CreatePersonalAccessTokenWithoutFeatureFlag(t *testing.T) require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) }) + t.Run("invalid argument when name does not match required regex", func(t *testing.T) { + _, _, client := setupTokensService(t, withTokenFeatureDisabled) + + names := []string{"a", "ab", strings.Repeat("a", 64), "!#$!%"} + + for _, name := range names { + _, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{ + Token: &v1.PersonalAccessToken{ + Name: name, + }, + })) + require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) + } + }) + t.Run("invalid argument when expiration time is unspecified", func(t *testing.T) { _, _, client := setupTokensService(t, withTokenFeatureDisabled) diff --git a/components/public-api/gitpod/experimental/v1/tokens.proto b/components/public-api/gitpod/experimental/v1/tokens.proto index 7ed2c53864ea87..bc363ac59755c6 100644 --- a/components/public-api/gitpod/experimental/v1/tokens.proto +++ b/components/public-api/gitpod/experimental/v1/tokens.proto @@ -19,7 +19,8 @@ message PersonalAccessToken { // Read only. string value = 2; - // name is the name of the token for humans, set by the user + // name is the name of the token for humans, set by the user. + // Must match regexp ^[a-zA-Z0-9-_ ]{3,63}$ string name = 3; // expiration_time is the time when the token expires diff --git a/components/public-api/go/experimental/v1/tokens.pb.go b/components/public-api/go/experimental/v1/tokens.pb.go index 06b7c94d414708..4554e24a641862 100644 --- a/components/public-api/go/experimental/v1/tokens.pb.go +++ b/components/public-api/go/experimental/v1/tokens.pb.go @@ -39,7 +39,8 @@ type PersonalAccessToken struct { // The value property is only populated when the PersonalAccessToken is first created, and never again. // Read only. Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` - // name is the name of the token for humans, set by the user + // name is the name of the token for humans, set by the user. + // Must match regexp ^[a-zA-Z0-9-_ ]{3,63}$ Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` // expiration_time is the time when the token expires // Read only. diff --git a/components/public-api/typescript/src/gitpod/experimental/v1/tokens_pb.ts b/components/public-api/typescript/src/gitpod/experimental/v1/tokens_pb.ts index 4663bf9b4659f8..18275291542363 100644 --- a/components/public-api/typescript/src/gitpod/experimental/v1/tokens_pb.ts +++ b/components/public-api/typescript/src/gitpod/experimental/v1/tokens_pb.ts @@ -37,7 +37,8 @@ export class PersonalAccessToken extends Message { value = ""; /** - * name is the name of the token for humans, set by the user + * name is the name of the token for humans, set by the user. + * Must match regexp ^[a-zA-Z0-9-_ ]{3,63}$ * * @generated from field: string name = 3; */