diff --git a/components/public-api-server/pkg/apiv1/tokens.go b/components/public-api-server/pkg/apiv1/tokens.go index b7edf3b80be587..6dd2d55c583ea0 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" + "strings" "github.com/bufbuild/connect-go" "github.com/gitpod-io/gitpod/common-go/experiments" @@ -17,6 +18,7 @@ import ( protocol "github.com/gitpod-io/gitpod/gitpod-protocol" "github.com/gitpod-io/gitpod/public-api-server/pkg/auth" "github.com/gitpod-io/gitpod/public-api-server/pkg/proxy" + "github.com/google/uuid" ) func NewTokensService(connPool proxy.ServerConnectionPool, expClient experiments.Client) *TokensService { @@ -35,24 +37,50 @@ type TokensService struct { } func (s *TokensService) CreatePersonalAccessToken(ctx context.Context, req *connect.Request[v1.CreatePersonalAccessTokenRequest]) (*connect.Response[v1.CreatePersonalAccessTokenResponse], error) { + conn, err := getConnection(ctx, s.connectionPool) + if err != nil { + return nil, err + } + + _, err = s.getUser(ctx, conn) + if err != nil { + return nil, err + } + + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.TokensService.CreatePersonalAccessToken is not implemented")) +} + +func (s *TokensService) GetPersonalAccessToken(ctx context.Context, req *connect.Request[v1.GetPersonalAccessTokenRequest]) (*connect.Response[v1.GetPersonalAccessTokenResponse], error) { + tokenID, err := validateTokenID(req.Msg.GetId()) + if err != nil { + return nil, err + } conn, err := getConnection(ctx, s.connectionPool) if err != nil { return nil, err } + _, err = s.getUser(ctx, conn) + if err != nil { + return nil, err + } + + log.Infof("Handling GetPersonalAccessToken request for Token ID '%s'", tokenID.String()) + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.TokensService.GetPersonalAccessToken is not implemented")) +} + +func (s *TokensService) getUser(ctx context.Context, conn protocol.APIInterface) (*protocol.User, error) { user, err := conn.GetLoggedInUser(ctx) if err != nil { return nil, proxy.ConvertError(err) } - isEnabled := experiments.IsPersonalAccessTokensEnabled(ctx, s.expClient, experiments.Attributes{UserID: user.ID}) - - if !isEnabled { + if !experiments.IsPersonalAccessTokensEnabled(ctx, s.expClient, experiments.Attributes{UserID: user.ID}) { return nil, connect.NewError(connect.CodePermissionDenied, errors.New("This feature is currently in beta. If you would like to be part of the beta, please contact us.")) } - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.TokensService.CreatePersonalAccessToken is not implemented")) + return user, nil } func getConnection(ctx context.Context, pool proxy.ServerConnectionPool) (protocol.APIInterface, error) { @@ -69,3 +97,17 @@ func getConnection(ctx context.Context, pool proxy.ServerConnectionPool) (protoc return conn, nil } + +func validateTokenID(id string) (uuid.UUID, error) { + trimmed := strings.TrimSpace(id) + if trimmed == "" { + return uuid.Nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Token ID is a required argument.")) + } + + tokenID, err := uuid.Parse(trimmed) + if err != nil { + return uuid.Nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Token ID must be a valid UUID")) + } + + return tokenID, nil +} diff --git a/components/public-api-server/pkg/apiv1/tokens_test.go b/components/public-api-server/pkg/apiv1/tokens_test.go index 13db142aacd6ad..8444c664dfd434 100644 --- a/components/public-api-server/pkg/apiv1/tokens_test.go +++ b/components/public-api-server/pkg/apiv1/tokens_test.go @@ -18,24 +18,28 @@ import ( protocol "github.com/gitpod-io/gitpod/gitpod-protocol" "github.com/gitpod-io/gitpod/public-api-server/pkg/auth" "github.com/golang/mock/gomock" + "github.com/google/uuid" "github.com/stretchr/testify/require" ) -func TestTokensService_CreatePersonalAccessTokenWithoutFeatureFlag(t *testing.T) { - experimentsClient := &experimentstest.Client{ +var ( + withTokenFeatureDisabled = &experimentstest.Client{ + BoolMatcher: func(ctx context.Context, experiment string, defaultValue bool, attributes experiments.Attributes) bool { + return false + }, + } + withTokenFeatureEnabled = &experimentstest.Client{ BoolMatcher: func(ctx context.Context, experiment string, defaultValue bool, attributes experiments.Attributes) bool { return experiment == experiments.PersonalAccessTokensEnabledFlag }, } +) +func TestTokensService_CreatePersonalAccessTokenWithoutFeatureFlag(t *testing.T) { user := newUser(&protocol.User{}) t.Run("permission denied when feature flag is not enabled", func(t *testing.T) { - serverMock, client := setupTokensService(t, &experimentstest.Client{ - BoolMatcher: func(ctx context.Context, experiment string, defaultValue bool, attributes experiments.Attributes) bool { - return false - }, - }) + serverMock, client := setupTokensService(t, withTokenFeatureDisabled) serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil) @@ -46,7 +50,7 @@ func TestTokensService_CreatePersonalAccessTokenWithoutFeatureFlag(t *testing.T) }) t.Run("unimplemented when feature flag enabled", func(t *testing.T) { - serverMock, client := setupTokensService(t, experimentsClient) + serverMock, client := setupTokensService(t, withTokenFeatureEnabled) serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil) @@ -55,6 +59,53 @@ func TestTokensService_CreatePersonalAccessTokenWithoutFeatureFlag(t *testing.T) }) } +func TestTokensService_GetPersonalAccessToken(t *testing.T) { + user := newUser(&protocol.User{}) + + t.Run("permission denied when feature flag is disabled", func(t *testing.T) { + serverMock, client := setupTokensService(t, withTokenFeatureDisabled) + + serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil) + + _, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{ + Id: uuid.New().String(), + })) + + require.Error(t, err, "This feature is currently in beta. If you would like to be part of the beta, please contact us.") + require.Equal(t, connect.CodePermissionDenied, connect.CodeOf(err)) + }) + + t.Run("invalid argument when Token ID is empty", func(t *testing.T) { + _, client := setupTokensService(t, withTokenFeatureEnabled) + + _, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{})) + + require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) + }) + + t.Run("invalid argument when Token ID is not a valid UUID", func(t *testing.T) { + _, client := setupTokensService(t, withTokenFeatureEnabled) + + _, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{ + Id: "foo-bar", + })) + + require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) + }) + + t.Run("unimplemented when feature flag enabled", func(t *testing.T) { + serverMock, client := setupTokensService(t, withTokenFeatureEnabled) + + serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil) + + _, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{ + Id: uuid.New().String(), + })) + + require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err)) + }) +} + func setupTokensService(t *testing.T, expClient experiments.Client) (*protocol.MockAPIInterface, v1connect.TokensServiceClient) { t.Helper()