Skip to content

[public-api] Implement regenerate token #14867

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion components/gitpod-db/go/dbtest/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
32 changes: 32 additions & 0 deletions components/gitpod-db/go/personal_access_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
45 changes: 45 additions & 0 deletions components/gitpod-db/go/personal_access_token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 26 additions & 3 deletions components/public-api-server/pkg/apiv1/tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
59 changes: 54 additions & 5 deletions components/public-api-server/pkg/apiv1/tokens_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,14 +332,16 @@ 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)

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.")
Expand All @@ -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)
Comment on lines +419 to +425
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a follow-up (in another PR), we can add a helper method which compares the token and performs a custom check for the Value, ensuring it exists (and possibly trying to parse it) without matching on specific contents.

The alternative to be able to use require.Equal(t, struct, struct) would also be to have a seeded implementation of the Signer.

})
}

Expand Down