mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
228 lines
7.6 KiB
Go
228 lines
7.6 KiB
Go
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
|
|
// Licensed under the GNU Affero General Public License (AGPL).
|
|
// See License-AGPL.txt in the project root for license information.
|
|
|
|
package apiv1
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/bufbuild/connect-go"
|
|
"github.com/gitpod-io/gitpod/common-go/experiments"
|
|
"github.com/gitpod-io/gitpod/common-go/log"
|
|
db "github.com/gitpod-io/gitpod/components/gitpod-db/go"
|
|
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
|
|
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
|
|
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"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func NewTokensService(connPool proxy.ServerConnectionPool, expClient experiments.Client, dbConn *gorm.DB) *TokensService {
|
|
return &TokensService{
|
|
connectionPool: connPool,
|
|
expClient: expClient,
|
|
dbConn: dbConn,
|
|
}
|
|
}
|
|
|
|
type TokensService struct {
|
|
connectionPool proxy.ServerConnectionPool
|
|
expClient experiments.Client
|
|
dbConn *gorm.DB
|
|
|
|
v1connect.UnimplementedTokensServiceHandler
|
|
}
|
|
|
|
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) ListPersonalAccessTokens(ctx context.Context, req *connect.Request[v1.ListPersonalAccessTokensRequest]) (*connect.Response[v1.ListPersonalAccessTokensResponse], error) {
|
|
conn, err := getConnection(ctx, s.connectionPool)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err := s.getUser(ctx, conn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userID, err := uuid.Parse(user.ID)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("Failed to parse user ID as UUID"))
|
|
}
|
|
|
|
result, err := db.ListPersonalAccessTokensForUser(ctx, s.dbConn, userID, paginationToDB(req.Msg.GetPagination()))
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("Failed to list Personal Access Tokens for User %s", userID.String()))
|
|
}
|
|
|
|
return connect.NewResponse(&v1.ListPersonalAccessTokensResponse{
|
|
Tokens: personalAccessTokensToAPI(result.Results),
|
|
TotalResults: result.Total,
|
|
}), nil
|
|
}
|
|
|
|
func (s *TokensService) RegeneratePersonalAccessToken(ctx context.Context, req *connect.Request[v1.RegeneratePersonalAccessTokenRequest]) (*connect.Response[v1.RegeneratePersonalAccessTokenResponse], 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 RegeneratePersonalAccessToken request for Token ID '%s'", tokenID.String())
|
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.TokensService.RegeneratePersonalAccessToken is not implemented"))
|
|
}
|
|
|
|
func (s *TokensService) UpdatePersonalAccessToken(ctx context.Context, req *connect.Request[v1.UpdatePersonalAccessTokenRequest]) (*connect.Response[v1.UpdatePersonalAccessTokenResponse], error) {
|
|
tokenID, err := validateTokenID(req.Msg.GetToken().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 UpdatePersonalAccessToken request for Token ID '%s'", tokenID.String())
|
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.TokensService.UpdatePersonalAccessToken is not implemented"))
|
|
}
|
|
|
|
func (s *TokensService) DeletePersonalAccessToken(ctx context.Context, req *connect.Request[v1.DeletePersonalAccessTokenRequest]) (*connect.Response[v1.DeletePersonalAccessTokenResponse], 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 DeletePersonalAccessToken request for Token ID '%s'", tokenID.String())
|
|
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("gitpod.experimental.v1.TokensService.DeletePersonalAccessToken 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)
|
|
}
|
|
|
|
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 user, nil
|
|
}
|
|
|
|
func getConnection(ctx context.Context, pool proxy.ServerConnectionPool) (protocol.APIInterface, error) {
|
|
token, err := auth.TokenFromContext(ctx)
|
|
if err != nil {
|
|
return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("No credentials present on request."))
|
|
}
|
|
|
|
conn, err := pool.Get(ctx, token)
|
|
if err != nil {
|
|
log.Log.WithError(err).Error("Failed to get connection to server.")
|
|
return nil, connect.NewError(connect.CodeInternal, errors.New("Failed to establish connection to downstream services. If this issue persists, please contact Gitpod Support."))
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func personalAccessTokensToAPI(ts []db.PersonalAccessToken) []*v1.PersonalAccessToken {
|
|
var tokens []*v1.PersonalAccessToken
|
|
for _, t := range ts {
|
|
tokens = append(tokens, personalAccessTokenToAPI(t, ""))
|
|
}
|
|
|
|
return tokens
|
|
}
|
|
|
|
func personalAccessTokenToAPI(t db.PersonalAccessToken, value string) *v1.PersonalAccessToken {
|
|
return &v1.PersonalAccessToken{
|
|
Id: t.ID.String(),
|
|
// value is only present when the token is first created, or regenerated. It's empty for all subsequent requests.
|
|
Value: value,
|
|
Hash: t.Hash,
|
|
Name: t.Name,
|
|
Description: t.Description,
|
|
Scopes: t.Scopes,
|
|
ExpirationTime: timestamppb.New(t.ExpirationTime),
|
|
CreatedAt: timestamppb.New(t.CreatedAt),
|
|
}
|
|
}
|