Path: blob/main/components/public-api-server/pkg/apiv1/tokens_test.go
2499 views
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package apiv156import (7"context"8"net/http"9"net/http/httptest"10"strings"11"testing"12"time"1314"google.golang.org/protobuf/types/known/fieldmaskpb"1516connect "github.com/bufbuild/connect-go"17"github.com/gitpod-io/gitpod/common-go/experiments"18"github.com/gitpod-io/gitpod/common-go/experiments/experimentstest"19db "github.com/gitpod-io/gitpod/components/gitpod-db/go"20"github.com/gitpod-io/gitpod/components/gitpod-db/go/dbtest"21"github.com/gitpod-io/gitpod/components/public-api/go/config"22v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"23"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"24protocol "github.com/gitpod-io/gitpod/gitpod-protocol"25"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"26"github.com/gitpod-io/gitpod/public-api-server/pkg/jws"27"github.com/gitpod-io/gitpod/public-api-server/pkg/jws/jwstest"28"github.com/golang/mock/gomock"29"github.com/google/uuid"30"github.com/stretchr/testify/require"31"google.golang.org/protobuf/types/known/timestamppb"32"gorm.io/gorm"33)3435var (36experimentsClient = &experimentstest.Client{}3738signer = auth.NewHS256Signer([]byte("my-secret"))3940teams = []*protocol.Team{newTeam(&protocol.Team{})}41)4243func TestTokensService_CreatePersonalAccessTokenWithoutFeatureFlag(t *testing.T) {44user := newUser(&protocol.User{})4546t.Run("invalid argument when name is not specified", func(t *testing.T) {47_, _, client := setupTokensService(t, experimentsClient)4849_, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{50Token: &v1.PersonalAccessToken{},51}))52require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))53})5455t.Run("invalid argument when name does not match required regex", func(t *testing.T) {56_, _, client := setupTokensService(t, experimentsClient)5758names := []string{"a", "ab", strings.Repeat("a", 64), "!#$!%"}5960for _, name := range names {61_, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{62Token: &v1.PersonalAccessToken{63Name: name,64},65}))66require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))67}68})6970t.Run("invalid argument when expiration time is unspecified", func(t *testing.T) {71_, _, client := setupTokensService(t, experimentsClient)7273_, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{74Token: &v1.PersonalAccessToken{75Name: "my-token",76},77}))78require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))79})8081t.Run("invalid argument when expiration time is invalid", func(t *testing.T) {82_, _, client := setupTokensService(t, experimentsClient)8384_, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{85Token: &v1.PersonalAccessToken{86Name: "my-token",87ExpirationTime: ×tamppb.Timestamp{88Seconds: 253402300799 + 1,89},90},91}))92require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))93})9495t.Run("invalid argument when disallowed scopes used", func(t *testing.T) {96_, _, client := setupTokensService(t, experimentsClient)9798_, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{99Token: &v1.PersonalAccessToken{100Name: "my-token",101ExpirationTime: timestamppb.Now(),102Scopes: []string{"random:scope"},103},104}))105require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))106})107108t.Run("crates personal access token", func(t *testing.T) {109serverMock, dbConn, client := setupTokensService(t, experimentsClient)110111serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)112113token := &v1.PersonalAccessToken{114Name: "my-token",115ExpirationTime: timestamppb.Now(),116}117118response, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{119Token: token,120}))121require.NoError(t, err)122123created := response.Msg.GetToken()124t.Cleanup(func() {125require.NoError(t, dbConn.Where("id = ?", created.GetId()).Delete(&db.PersonalAccessToken{}).Error)126})127128require.NotEmpty(t, created.GetId())129require.Equal(t, token.Name, created.GetName())130require.Equal(t, token.Scopes, created.GetScopes())131requireEqualProto(t, token.GetExpirationTime(), created.GetExpirationTime())132133// Returned token must be parseable134_, err = auth.ParsePersonalAccessToken(created.GetValue(), signer)135require.NoError(t, err)136137// token must exist in the DB, with the User ID of the requestor138storedInDB, err := db.GetPersonalAccessTokenForUser(context.Background(), dbConn, uuid.MustParse(created.GetId()), uuid.MustParse(user.ID))139require.NoError(t, err)140require.Equal(t, user.ID, storedInDB.UserID.String())141})142143t.Run("crates personal access token with no scopes when none provided", func(t *testing.T) {144serverMock, dbConn, client := setupTokensService(t, experimentsClient)145146serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)147148token := &v1.PersonalAccessToken{149Name: "my-token",150ExpirationTime: timestamppb.Now(),151}152153response, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{154Token: token,155}))156require.NoError(t, err)157158created := response.Msg.GetToken()159t.Cleanup(func() {160require.NoError(t, dbConn.Where("id = ?", created.GetId()).Delete(&db.PersonalAccessToken{}).Error)161})162163require.Len(t, created.GetScopes(), 0, "must have no scopes, none were provided in the request")164})165166t.Run("crates personal access token with full access when correct scopes provided", func(t *testing.T) {167serverMock, dbConn, client := setupTokensService(t, experimentsClient)168169serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)170171token := &v1.PersonalAccessToken{172Name: "my-token",173ExpirationTime: timestamppb.Now(),174Scopes: []string{"resource:default", "function:*"},175}176177response, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{178Token: token,179}))180require.NoError(t, err)181182created := response.Msg.GetToken()183t.Cleanup(func() {184require.NoError(t, dbConn.Where("id = ?", created.GetId()).Delete(&db.PersonalAccessToken{}).Error)185})186187require.Equal(t, []string{allFunctionsScope, defaultResourceScope}, created.GetScopes())188})189}190191func TestTokensService_GetPersonalAccessToken(t *testing.T) {192user := newUser(&protocol.User{})193user2 := newUser(&protocol.User{})194195t.Run("get correct token", func(t *testing.T) {196serverMock, dbConn, client := setupTokensService(t, experimentsClient)197198tokens := dbtest.CreatePersonalAccessTokenRecords(t, dbConn,199dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{200UserID: uuid.MustParse(user.ID),201}),202dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{203UserID: uuid.MustParse(user2.ID),204}),205)206207serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)208209response, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{210Id: tokens[0].ID.String(),211}))212213require.NoError(t, err)214215requireEqualProto(t, &v1.GetPersonalAccessTokenResponse{216Token: personalAccessTokenToAPI(tokens[0], ""),217}, response.Msg)218})219220t.Run("invalid argument when Token ID is empty", func(t *testing.T) {221_, _, client := setupTokensService(t, experimentsClient)222223_, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{}))224225require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))226})227228t.Run("invalid argument when Token ID is not a valid UUID", func(t *testing.T) {229_, _, client := setupTokensService(t, experimentsClient)230231_, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{232Id: "foo-bar",233}))234235require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))236})237238t.Run("responds with not found when token is not found", func(t *testing.T) {239serverMock, dbConn, client := setupTokensService(t, experimentsClient)240241someTokenId := uuid.New().String()242243dbtest.CreatePersonalAccessTokenRecords(t, dbConn,244dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{245UserID: uuid.MustParse(user.ID),246}),247)248249serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)250251_, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{252Id: someTokenId,253}))254require.Error(t, err)255require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))256})257}258259func TestTokensService_ListPersonalAccessTokens(t *testing.T) {260user := newUser(&protocol.User{})261262t.Run("no tokens returns empty list", func(t *testing.T) {263serverMock, _, client := setupTokensService(t, experimentsClient)264265serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)266267response, err := client.ListPersonalAccessTokens(context.Background(), connect.NewRequest(&v1.ListPersonalAccessTokensRequest{}))268require.NoError(t, err)269270requireEqualProto(t, &v1.ListPersonalAccessTokensResponse{271Tokens: nil,272TotalResults: 0,273}, response.Msg)274})275276t.Run("lists first page of results, when no pagination preference specified", func(t *testing.T) {277serverMock, dbConn, client := setupTokensService(t, experimentsClient)278279serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)280281now := time.Now().UTC().Round(time.Millisecond)282tokens := dbtest.CreatePersonalAccessTokenRecords(t, dbConn,283dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{284UserID: uuid.MustParse(user.ID),285CreatedAt: now.Add(-1 * time.Minute),286}),287dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{288UserID: uuid.MustParse(user.ID),289CreatedAt: now,290}),291)292293response, err := client.ListPersonalAccessTokens(context.Background(), connect.NewRequest(&v1.ListPersonalAccessTokensRequest{}))294require.NoError(t, err)295296requireEqualProto(t, &v1.ListPersonalAccessTokensResponse{297Tokens: personalAccessTokensToAPI(tokens),298TotalResults: 2,299}, response.Msg)300})301302t.Run("paginating through results", func(t *testing.T) {303now := time.Now().UTC().Round(time.Millisecond)304serverMock, dbConn, client := setupTokensService(t, experimentsClient)305306serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).Times(3)307308var toCreate []db.PersonalAccessToken309total := 5310for i := 5; i > 0; i-- {311toCreate = append(toCreate, dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{312UserID: uuid.MustParse(user.ID),313CreatedAt: now.Add(time.Duration(-1*i) * time.Minute),314}))315}316317tokens := dbtest.CreatePersonalAccessTokenRecords(t, dbConn, toCreate...)318319firstPage, err := client.ListPersonalAccessTokens(context.Background(), connect.NewRequest(&v1.ListPersonalAccessTokensRequest{320Pagination: &v1.Pagination{321PageSize: 2,322Page: 1,323},324}))325require.NoError(t, err)326requireEqualProto(t, &v1.ListPersonalAccessTokensResponse{327Tokens: personalAccessTokensToAPI(tokens[0:2]),328TotalResults: int64(total),329}, firstPage.Msg)330331secondPage, err := client.ListPersonalAccessTokens(context.Background(), connect.NewRequest(&v1.ListPersonalAccessTokensRequest{332Pagination: &v1.Pagination{333PageSize: 2,334Page: 2,335},336}))337require.NoError(t, err)338requireEqualProto(t, &v1.ListPersonalAccessTokensResponse{339Tokens: personalAccessTokensToAPI(tokens[2:4]),340TotalResults: int64(total),341}, secondPage.Msg)342343thirdPage, err := client.ListPersonalAccessTokens(context.Background(), connect.NewRequest(&v1.ListPersonalAccessTokensRequest{344Pagination: &v1.Pagination{345PageSize: 2,346Page: 3,347},348}))349require.NoError(t, err)350requireEqualProto(t, &v1.ListPersonalAccessTokensResponse{351Tokens: personalAccessTokensToAPI([]db.PersonalAccessToken{tokens[4]}),352TotalResults: int64(total),353}, thirdPage.Msg)354})355}356357func TestTokensService_RegeneratePersonalAccessToken(t *testing.T) {358user := newUser(&protocol.User{})359user2 := newUser(&protocol.User{})360361t.Run("invalid argument when Token ID is empty", func(t *testing.T) {362_, _, client := setupTokensService(t, experimentsClient)363364_, err := client.RegeneratePersonalAccessToken(context.Background(), connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{}))365366require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))367})368369t.Run("invalid argument when Token ID is not a valid UUID", func(t *testing.T) {370_, _, client := setupTokensService(t, experimentsClient)371372_, err := client.RegeneratePersonalAccessToken(context.Background(), connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{373Id: "foo-bar",374}))375376require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))377})378379t.Run("responds with not found when token is not found", func(t *testing.T) {380serverMock, dbConn, client := setupTokensService(t, experimentsClient)381382someTokenId := uuid.New().String()383384dbtest.CreatePersonalAccessTokenRecords(t, dbConn,385dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{386UserID: uuid.MustParse(user.ID),387}),388dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{389UserID: uuid.MustParse(user2.ID),390}),391)392393serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)394395newTimestamp := timestamppb.New(time.Date(2023, 1, 2, 15, 4, 5, 0, time.UTC))396_, err := client.RegeneratePersonalAccessToken(context.Background(), connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{397Id: someTokenId,398ExpirationTime: newTimestamp,399}))400require.Error(t, err)401require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))402})403404t.Run("regenerate correct token", func(t *testing.T) {405serverMock, dbConn, client := setupTokensService(t, experimentsClient)406407tokens := dbtest.CreatePersonalAccessTokenRecords(t, dbConn,408dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{409UserID: uuid.MustParse(user.ID),410}),411dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{412UserID: uuid.MustParse(user2.ID),413}),414)415416serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).MaxTimes(2)417418origResponse, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{419Id: tokens[0].ID.String(),420}))421require.NoError(t, err)422423newTimestamp := timestamppb.New(time.Now().Add(24 * time.Hour).UTC().Truncate(time.Millisecond))424response, err := client.RegeneratePersonalAccessToken(context.Background(), connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{425Id: tokens[0].ID.String(),426ExpirationTime: newTimestamp,427}))428require.NoError(t, err)429430require.Equal(t, origResponse.Msg.Token.Id, response.Msg.Token.Id)431require.NotEqual(t, "", response.Msg.Token.Value)432require.Equal(t, origResponse.Msg.Token.Name, response.Msg.Token.Name)433require.Equal(t, origResponse.Msg.Token.Scopes, response.Msg.Token.Scopes)434require.Equal(t, newTimestamp.AsTime(), response.Msg.Token.ExpirationTime.AsTime())435require.Equal(t, origResponse.Msg.Token.CreatedAt, response.Msg.Token.CreatedAt)436})437}438439func TestTokensService_UpdatePersonalAccessToken(t *testing.T) {440user := newUser(&protocol.User{})441442t.Run("invalid argument when Token ID is empty", func(t *testing.T) {443_, _, client := setupTokensService(t, experimentsClient)444445_, err := client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{}))446447require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))448})449450t.Run("invalid argument when Token ID is not a valid UUID", func(t *testing.T) {451_, _, client := setupTokensService(t, experimentsClient)452453_, err := client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{454Token: &v1.PersonalAccessToken{455Id: "foo-bar",456},457}))458459require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))460})461462t.Run("allows unmodified udpate", func(t *testing.T) {463serverMock, _, client := setupTokensService(t, experimentsClient)464465serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).Times(2)466467createResponse, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{468Token: &v1.PersonalAccessToken{469Name: "first",470ExpirationTime: timestamppb.Now(),471},472}))473require.NoError(t, err)474475_, err = client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{476Token: &v1.PersonalAccessToken{477Id: createResponse.Msg.GetToken().GetId(),478Name: createResponse.Msg.GetToken().GetName(),479},480UpdateMask: &fieldmaskpb.FieldMask{481Paths: []string{"name", "scopes"},482},483}))484require.NoError(t, err)485})486487t.Run("default updates both name and scopes, when no mask specified", func(t *testing.T) {488serverMock, _, client := setupTokensService(t, experimentsClient)489490serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).Times(2)491492createResponse, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{493Token: &v1.PersonalAccessToken{494Name: "first",495ExpirationTime: timestamppb.Now(),496},497}))498require.NoError(t, err)499500updateResponse, err := client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{501Token: &v1.PersonalAccessToken{502Id: createResponse.Msg.GetToken().GetId(),503Name: "second",504Scopes: []string{allFunctionsScope, defaultResourceScope},505},506}))507require.NoError(t, err)508require.Equal(t, "second", updateResponse.Msg.GetToken().GetName())509require.Equal(t, []string{allFunctionsScope, defaultResourceScope}, updateResponse.Msg.GetToken().GetScopes())510})511512t.Run("updates only name, when mask specifies name", func(t *testing.T) {513serverMock, dbConn, client := setupTokensService(t, experimentsClient)514515serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)516517created := dbtest.CreatePersonalAccessTokenRecords(t, dbConn, dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{518Name: "first",519UserID: uuid.MustParse(user.ID),520Scopes: db.Scopes{allFunctionsScope, defaultResourceScope},521}))[0]522523updateResponse, err := client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{524Token: &v1.PersonalAccessToken{525Id: created.ID.String(),526Name: "second",527Scopes: []string{allFunctionsScope, defaultResourceScope},528},529UpdateMask: &fieldmaskpb.FieldMask{530Paths: []string{"name"},531},532}))533require.NoError(t, err)534require.Equal(t, "second", updateResponse.Msg.GetToken().GetName())535require.Equal(t, []string(created.Scopes), updateResponse.Msg.GetToken().GetScopes())536})537538t.Run("updates only scopes, when mask specifies scopes", func(t *testing.T) {539serverMock, dbConn, client := setupTokensService(t, experimentsClient)540541serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)542543created := dbtest.CreatePersonalAccessTokenRecords(t, dbConn, dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{544Name: "first",545UserID: uuid.MustParse(user.ID),546Scopes: db.Scopes{allFunctionsScope, defaultResourceScope},547}))[0]548549updateResponse, err := client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{550Token: &v1.PersonalAccessToken{551Id: created.ID.String(),552Name: "second",553Scopes: []string{allFunctionsScope, defaultResourceScope},554},555UpdateMask: &fieldmaskpb.FieldMask{556Paths: []string{"scopes"},557},558}))559require.NoError(t, err)560require.Equal(t, "first", updateResponse.Msg.GetToken().GetName())561require.Equal(t, []string{allFunctionsScope, defaultResourceScope}, updateResponse.Msg.GetToken().GetScopes())562})563}564565func TestTokensService_DeletePersonalAccessToken(t *testing.T) {566user := newUser(&protocol.User{})567568t.Run("invalid argument when Token ID is empty", func(t *testing.T) {569_, _, client := setupTokensService(t, experimentsClient)570571_, err := client.DeletePersonalAccessToken(context.Background(), connect.NewRequest(&v1.DeletePersonalAccessTokenRequest{}))572573require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))574})575576t.Run("invalid argument when Token ID is not a valid UUID", func(t *testing.T) {577_, _, client := setupTokensService(t, experimentsClient)578579_, err := client.DeletePersonalAccessToken(context.Background(), connect.NewRequest(&v1.DeletePersonalAccessTokenRequest{580Id: "foo-bar",581}))582583require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))584})585586t.Run("delete token", func(t *testing.T) {587serverMock, dbConn, client := setupTokensService(t, experimentsClient)588589tokens := dbtest.CreatePersonalAccessTokenRecords(t, dbConn,590dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{591UserID: uuid.MustParse(user.ID),592}),593)594595serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).Times(3)596597_, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{598Id: tokens[0].ID.String(),599}))600require.NoError(t, err)601602_, err = client.DeletePersonalAccessToken(context.Background(), connect.NewRequest(&v1.DeletePersonalAccessTokenRequest{603Id: tokens[0].ID.String(),604}))605require.NoError(t, err)606607_, err = client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{608Id: tokens[0].ID.String(),609}))610require.Error(t, err)611require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))612})613}614615func TestTokensService_Workflow(t *testing.T) {616ctx := context.Background()617now := time.Now().UTC().Round(time.Millisecond)618user := newUser(&protocol.User{})619serverMock, _, client := setupTokensService(t, experimentsClient)620621serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).AnyTimes()622623// Create 1 token624createdTokenResponse, err := client.CreatePersonalAccessToken(ctx, connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{625Token: &v1.PersonalAccessToken{626Name: "my-first-token",627ExpirationTime: timestamppb.New(now.Add(1 * time.Hour)),628},629}))630require.NoError(t, err)631632_, err = client.GetPersonalAccessToken(ctx, connect.NewRequest(&v1.GetPersonalAccessTokenRequest{633Id: createdTokenResponse.Msg.Token.GetId(),634}))635require.NoError(t, err, "must retrieve the token we created")636637response, err := client.ListPersonalAccessTokens(ctx, connect.NewRequest(&v1.ListPersonalAccessTokensRequest{}))638require.NoError(t, err)639require.Len(t, response.Msg.Tokens, 1, "must retrieve one token which we created earlier")640641// Create a second token642secondTokenResponse, err := client.CreatePersonalAccessToken(ctx, connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{643Token: &v1.PersonalAccessToken{644Name: "my-second-token",645ExpirationTime: timestamppb.New(now.Add(1 * time.Hour)),646},647}))648require.NoError(t, err)649650response, err = client.ListPersonalAccessTokens(ctx, connect.NewRequest(&v1.ListPersonalAccessTokensRequest{}))651require.NoError(t, err)652require.Len(t, response.Msg.Tokens, 2, "must retrieve both tokens we created")653654_, err = client.RegeneratePersonalAccessToken(ctx, connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{655Id: secondTokenResponse.Msg.GetToken().GetId(),656ExpirationTime: timestamppb.New(now.Add(2 * time.Hour)),657}))658require.NoError(t, err)659660response, err = client.ListPersonalAccessTokens(ctx, connect.NewRequest(&v1.ListPersonalAccessTokensRequest{}))661require.NoError(t, err)662require.Len(t, response.Msg.Tokens, 2, "must retrieve both tokens")663664_, err = client.DeletePersonalAccessToken(ctx, connect.NewRequest(&v1.DeletePersonalAccessTokenRequest{665Id: secondTokenResponse.Msg.GetToken().GetId(),666}))667require.NoError(t, err)668}669670func TestValidateScopes(t *testing.T) {671for _, s := range []struct {672Name string673RequestedScopes []string674Error bool675}{676{677Name: "no scopes are permitted",678RequestedScopes: nil,679},680{681Name: "empty scopes are permitted",682RequestedScopes: []string{},683},684{685Name: "all scopes are permitted",686RequestedScopes: []string{"function:*", "resource:default"},687},688{689Name: "all scopes (unsorted) are permitted",690RequestedScopes: []string{"resource:default", "function:*"},691},692{693Name: "only all function scope is not permitted",694RequestedScopes: []string{"function:*"},695Error: true,696},697{698Name: "only all default resource scope is not permitted",699RequestedScopes: []string{"resource:default"},700Error: true,701},702{703Name: "unknown scope is rejected",704RequestedScopes: []string{"unknown"},705Error: true,706},707{708Name: "unknown scope, with all scopes, is rejected",709RequestedScopes: []string{"unknown", "function:*", "resource:default"},710Error: true,711},712} {713t.Run(s.Name, func(t *testing.T) {714_, err := validateScopes(s.RequestedScopes)715716if s.Error {717require.Error(t, err)718} else {719require.NoError(t, err)720}721})722723}724}725726func setupTokensService(t *testing.T, expClient experiments.Client) (*protocol.MockAPIInterface, *gorm.DB, v1connect.TokensServiceClient) {727t.Helper()728729dbConn := dbtest.ConnectForTests(t)730731ctrl := gomock.NewController(t)732t.Cleanup(ctrl.Finish)733734serverMock := protocol.NewMockAPIInterface(ctrl)735736svc := NewTokensService(&FakeServerConnPool{api: serverMock}, expClient, dbConn, signer)737738keyset := jwstest.GenerateKeySet(t)739rsa256, err := jws.NewRSA256(keyset)740require.NoError(t, err)741742_, handler := v1connect.NewTokensServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{743Issuer: "unitetest.com",744Cookie: config.CookieConfig{745Name: "cookie_jwt",746},747}, rsa256)))748749srv := httptest.NewServer(handler)750t.Cleanup(srv.Close)751752client := v1connect.NewTokensServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(753auth.NewClientInterceptor("auth-token"),754))755756return serverMock, dbConn, client757}758759760