From 4917f6ee7f8c2a688fd0d7fc113617a0eed88f76 Mon Sep 17 00:00:00 2001 From: Jean Pierre Date: Tue, 22 Nov 2022 18:25:31 +0000 Subject: [PATCH] [public-api] regenerate personal access token Co-authored-by: Milan Pavlik Co-authored-by: mustard --- components/gitpod-db/go/dbtest/conn.go | 2 +- .../gitpod-db/go/personal_access_token.go | 32 ++++++++++ .../go/personal_access_token_test.go | 45 ++++++++++++++ .../public-api-server/pkg/apiv1/tokens.go | 29 ++++++++- .../pkg/apiv1/tokens_test.go | 59 +++++++++++++++++-- 5 files changed, 158 insertions(+), 9 deletions(-) diff --git a/components/gitpod-db/go/dbtest/conn.go b/components/gitpod-db/go/dbtest/conn.go index 7a1787d89fd9d7..d4d26f5af4ae4f 100644 --- a/components/gitpod-db/go/dbtest/conn.go +++ b/components/gitpod-db/go/dbtest/conn.go @@ -37,7 +37,7 @@ func ConnectForTests(t *testing.T) *gorm.DB { Host: "localhost:23306", Database: "gitpod", }) - require.NoError(t, err, "Failed to establish connection to In a workspace, run `leeway build components/gitpod-db/go:init-testdb` once to bootstrap the ") + require.NoError(t, err, "Failed to establish connection to In a workspace, run `leeway build components/gitpod-db/go:init-testdb` once to bootstrap the db") return conn } diff --git a/components/gitpod-db/go/personal_access_token.go b/components/gitpod-db/go/personal_access_token.go index dc9c5c0b01b99f..d557cc1eb1201a 100644 --- a/components/gitpod-db/go/personal_access_token.go +++ b/components/gitpod-db/go/personal_access_token.go @@ -87,6 +87,38 @@ func CreatePersonalAccessToken(ctx context.Context, conn *gorm.DB, req PersonalA return token, nil } +func UpdatePersonalAccessTokenHash(ctx context.Context, conn *gorm.DB, tokenID uuid.UUID, userID uuid.UUID, hash string, expirationTime time.Time) (PersonalAccessToken, error) { + if tokenID == uuid.Nil { + return PersonalAccessToken{}, fmt.Errorf("Invalid or empty tokenID") + } + if userID == uuid.Nil { + return PersonalAccessToken{}, fmt.Errorf("Invalid or empty userID") + } + if hash == "" { + return PersonalAccessToken{}, fmt.Errorf("Token hash required") + } + if expirationTime.IsZero() { + return PersonalAccessToken{}, fmt.Errorf("Expiration time required") + } + + db := conn.WithContext(ctx) + + err := db. + Where("id = ?", tokenID). + Where("userId = ?", userID). + Where("deleted = ?", 0). + Select("hash", "expirationTime").Updates(PersonalAccessToken{Hash: hash, ExpirationTime: expirationTime}). + Error + if err != nil { + if errors.Is(db.Error, gorm.ErrRecordNotFound) { + return PersonalAccessToken{}, fmt.Errorf("Token with ID %s does not exist: %w", tokenID, ErrorNotFound) + } + return PersonalAccessToken{}, fmt.Errorf("Failed to update token: %v", db.Error) + } + + return GetPersonalAccessTokenForUser(ctx, conn, tokenID, userID) +} + func ListPersonalAccessTokensForUser(ctx context.Context, conn *gorm.DB, userID uuid.UUID, pagination Pagination) (*PaginatedResult[PersonalAccessToken], error) { if userID == uuid.Nil { return nil, fmt.Errorf("user ID is a required argument to list personal access tokens for user, got nil") diff --git a/components/gitpod-db/go/personal_access_token_test.go b/components/gitpod-db/go/personal_access_token_test.go index 748fe3500b6f8d..55454fd23823dd 100644 --- a/components/gitpod-db/go/personal_access_token_test.go +++ b/components/gitpod-db/go/personal_access_token_test.go @@ -74,6 +74,51 @@ func TestPersonalAccessToken_Create(t *testing.T) { require.Equal(t, request.ID, result.ID) } +func TestPersonalAccessToken_UpdateHash(t *testing.T) { + conn := dbtest.ConnectForTests(t) + + firstUserId := uuid.New() + secondUserId := uuid.New() + + token := dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{UserID: firstUserId}) + token2 := dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{UserID: secondUserId}) + + tokenEntries := []db.PersonalAccessToken{token, token2} + + dbtest.CreatePersonalAccessTokenRecords(t, conn, tokenEntries...) + + var newHash = "another-secure-hash" + var newExpirationTime = time.Now().Add(24 * time.Hour).UTC().Truncate(time.Millisecond) + + t.Run("not matching user", func(t *testing.T) { + _, err := db.UpdatePersonalAccessTokenHash(context.Background(), conn, token.ID, token2.UserID, newHash, newExpirationTime) + require.Error(t, err, db.ErrorNotFound) + }) + + t.Run("not matching token", func(t *testing.T) { + _, err := db.UpdatePersonalAccessTokenHash(context.Background(), conn, token2.ID, token.UserID, newHash, newExpirationTime) + require.Error(t, err, db.ErrorNotFound) + }) + + t.Run("both token and user don't exist in the DB", func(t *testing.T) { + _, err := db.UpdatePersonalAccessTokenHash(context.Background(), conn, uuid.New(), uuid.New(), newHash, newExpirationTime) + require.Error(t, err, db.ErrorNotFound) + }) + + t.Run("valid", func(t *testing.T) { + returned, err := db.UpdatePersonalAccessTokenHash(context.Background(), conn, token.ID, token.UserID, newHash, newExpirationTime) + require.NoError(t, err) + require.Equal(t, token.ID, returned.ID) + require.Equal(t, token.UserID, returned.UserID) + require.Equal(t, newHash, returned.Hash) + require.Equal(t, token.Name, returned.Name) + require.Equal(t, token.Description, returned.Description) + require.Equal(t, token.Scopes, returned.Scopes) + require.Equal(t, newExpirationTime, returned.ExpirationTime) + require.Equal(t, token.CreatedAt, returned.CreatedAt) + }) +} + func TestListPersonalAccessTokensForUser(t *testing.T) { ctx := context.Background() conn := dbtest.ConnectForTests(t) diff --git a/components/public-api-server/pkg/apiv1/tokens.go b/components/public-api-server/pkg/apiv1/tokens.go index 2ec1e4aafca4c5..b9a461916ee74a 100644 --- a/components/public-api-server/pkg/apiv1/tokens.go +++ b/components/public-api-server/pkg/apiv1/tokens.go @@ -159,18 +159,41 @@ func (s *TokensService) RegeneratePersonalAccessToken(ctx context.Context, req * return nil, err } + expiry := req.Msg.GetExpirationTime() + if !expiry.IsValid() { + return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("Received invalid Expiration Time, it is a required parameter.")) + } + conn, err := getConnection(ctx, s.connectionPool) if err != nil { return nil, err } - _, _, err = s.getUser(ctx, conn) + _, userID, err := s.getUser(ctx, conn) if err != nil { return nil, err } + pat, err := auth.GeneratePersonalAccessToken(s.signer) + if err != nil { + log.WithError(err).Errorf("Failed to regenerate personal access token for user %s", userID.String()) + return nil, connect.NewError(connect.CodeInternal, errors.New("Failed to regenerate personal access token.")) + } - log.Infof("Handling RegeneratePersonalAccessToken request for Token ID '%s'", tokenID.String()) - return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.TokensService.RegeneratePersonalAccessToken is not implemented")) + hash, err := pat.ValueHash() + if err != nil { + log.WithError(err).Errorf("Failed to regenerate personal access token value hash for user %s", userID.String()) + return nil, connect.NewError(connect.CodeInternal, errors.New("Failed to compute personal access token hash.")) + } + + token, err := db.UpdatePersonalAccessTokenHash(ctx, s.dbConn, tokenID, userID, hash, expiry.AsTime().UTC()) + if err != nil { + log.WithError(err).Errorf("Failed to store personal access token for user %s", userID.String()) + return nil, connect.NewError(connect.CodeInternal, errors.New("Failed to store personal access token.")) + } + + return connect.NewResponse(&v1.RegeneratePersonalAccessTokenResponse{ + Token: personalAccessTokenToAPI(token, pat.String()), + }), nil } func (s *TokensService) UpdatePersonalAccessToken(ctx context.Context, req *connect.Request[v1.UpdatePersonalAccessTokenRequest]) (*connect.Response[v1.UpdatePersonalAccessTokenResponse], error) { diff --git a/components/public-api-server/pkg/apiv1/tokens_test.go b/components/public-api-server/pkg/apiv1/tokens_test.go index 8813f1d0d2925d..d52547070e8134 100644 --- a/components/public-api-server/pkg/apiv1/tokens_test.go +++ b/components/public-api-server/pkg/apiv1/tokens_test.go @@ -332,6 +332,7 @@ func TestTokensService_ListPersonalAccessTokens(t *testing.T) { func TestTokensService_RegeneratePersonalAccessToken(t *testing.T) { user := newUser(&protocol.User{}) + user2 := newUser(&protocol.User{}) t.Run("permission denied when feature flag is disabled", func(t *testing.T) { serverMock, _, client := setupTokensService(t, withTokenFeatureDisabled) @@ -339,7 +340,8 @@ func TestTokensService_RegeneratePersonalAccessToken(t *testing.T) { serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil) _, err := client.RegeneratePersonalAccessToken(context.Background(), connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{ - Id: uuid.New().String(), + Id: uuid.New().String(), + ExpirationTime: timestamppb.Now(), })) require.Error(t, err, "This feature is currently in beta. If you would like to be part of the beta, please contact us.") @@ -364,16 +366,63 @@ func TestTokensService_RegeneratePersonalAccessToken(t *testing.T) { require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err)) }) - t.Run("unimplemented when feature flag enabled", func(t *testing.T) { - serverMock, _, client := setupTokensService(t, withTokenFeatureEnabled) + t.Run("responds with not found when token is not found", func(t *testing.T) { + serverMock, dbConn, client := setupTokensService(t, withTokenFeatureEnabled) + + someTokenId := uuid.New().String() + + dbtest.CreatePersonalAccessTokenRecords(t, dbConn, + dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{ + UserID: uuid.MustParse(user.ID), + }), + dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{ + UserID: uuid.MustParse(user2.ID), + }), + ) serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil) + newTimestamp := timestamppb.New(time.Date(2023, 1, 2, 15, 4, 5, 0, time.UTC)) _, err := client.RegeneratePersonalAccessToken(context.Background(), connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{ - Id: uuid.New().String(), + Id: someTokenId, + ExpirationTime: newTimestamp, })) + require.Error(t, err, fmt.Errorf("Token with ID %s does not exist: not found", someTokenId)) + }) - require.Equal(t, connect.CodeUnimplemented, connect.CodeOf(err)) + t.Run("regenerate correct token", func(t *testing.T) { + serverMock, dbConn, client := setupTokensService(t, withTokenFeatureEnabled) + + tokens := dbtest.CreatePersonalAccessTokenRecords(t, dbConn, + dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{ + UserID: uuid.MustParse(user.ID), + }), + dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{ + UserID: uuid.MustParse(user2.ID), + }), + ) + + serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).MaxTimes(2) + + origResponse, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{ + Id: tokens[0].ID.String(), + })) + require.NoError(t, err) + + newTimestamp := timestamppb.New(time.Now().Add(24 * time.Hour).UTC().Truncate(time.Millisecond)) + response, err := client.RegeneratePersonalAccessToken(context.Background(), connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{ + Id: tokens[0].ID.String(), + ExpirationTime: newTimestamp, + })) + require.NoError(t, err) + + require.Equal(t, origResponse.Msg.Token.Id, response.Msg.Token.Id) + require.NotEqual(t, "", response.Msg.Token.Value) + require.Equal(t, origResponse.Msg.Token.Name, response.Msg.Token.Name) + require.Equal(t, origResponse.Msg.Token.Description, response.Msg.Token.Description) + require.Equal(t, origResponse.Msg.Token.Scopes, response.Msg.Token.Scopes) + require.Equal(t, newTimestamp.AsTime(), response.Msg.Token.ExpirationTime.AsTime()) + require.Equal(t, origResponse.Msg.Token.CreatedAt, response.Msg.Token.CreatedAt) }) }