Skip to content

Commit e010879

Browse files
committed
[public-api] Implement list personal access tokens
1 parent 740fad9 commit e010879

11 files changed

+537
-32
lines changed
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package dbtest
6+
7+
import (
8+
"context"
9+
"testing"
10+
"time"
11+
12+
db "github.com/gitpod-io/gitpod/components/gitpod-db/go"
13+
"github.com/google/uuid"
14+
"github.com/stretchr/testify/require"
15+
"gorm.io/gorm"
16+
)
17+
18+
func NewPersonalAccessToken(t *testing.T, record db.PersonalAccessToken) db.PersonalAccessToken {
19+
t.Helper()
20+
21+
now := time.Now().UTC().Round(time.Millisecond)
22+
tokenID := uuid.New()
23+
24+
result := db.PersonalAccessToken{
25+
ID: tokenID,
26+
UserID: uuid.New(),
27+
Hash: "some-secure-hash",
28+
Name: "some-name",
29+
Description: "some-description",
30+
Scopes: []string{"read", "write"},
31+
ExpirationTime: now.Add(5 * time.Hour),
32+
CreatedAt: now,
33+
LastModified: now,
34+
}
35+
36+
if record.ID != uuid.Nil {
37+
result.ID = record.ID
38+
}
39+
40+
if record.UserID != uuid.Nil {
41+
result.UserID = record.UserID
42+
}
43+
44+
if record.Hash != "" {
45+
result.Hash = record.Hash
46+
}
47+
48+
if record.Name != "" {
49+
result.Name = record.Name
50+
}
51+
52+
if record.Description != "" {
53+
result.Description = record.Description
54+
}
55+
56+
if len(record.Scopes) == 0 {
57+
result.Scopes = record.Scopes
58+
}
59+
60+
if !record.ExpirationTime.IsZero() {
61+
result.ExpirationTime = record.ExpirationTime
62+
}
63+
64+
if !record.CreatedAt.IsZero() {
65+
result.CreatedAt = record.CreatedAt
66+
}
67+
68+
if !record.LastModified.IsZero() {
69+
result.LastModified = record.LastModified
70+
}
71+
72+
return result
73+
}
74+
75+
func CreatePersonalAccessTokenRecords(t *testing.T, conn *gorm.DB, entries ...db.PersonalAccessToken) []db.PersonalAccessToken {
76+
t.Helper()
77+
78+
var records []db.PersonalAccessToken
79+
var ids []string
80+
for _, tokenEntry := range entries {
81+
record := NewPersonalAccessToken(t, tokenEntry)
82+
records = append(records, record)
83+
ids = append(ids, record.ID.String())
84+
85+
_, err := db.CreateToken(context.Background(), conn, tokenEntry)
86+
require.NoError(t, err)
87+
}
88+
89+
t.Cleanup(func() {
90+
if len(ids) > 0 {
91+
require.NoError(t, conn.Where(ids).Delete(&db.PersonalAccessToken{}).Error)
92+
}
93+
})
94+
95+
return records
96+
}

components/gitpod-db/go/pagination.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package db
6+
7+
import (
8+
"gorm.io/gorm"
9+
)
10+
11+
type Pagination struct {
12+
Page int
13+
PageSize int
14+
}
15+
16+
func Paginate(pagination Pagination) func(*gorm.DB) *gorm.DB {
17+
return func(conn *gorm.DB) *gorm.DB {
18+
page := 1
19+
if pagination.Page > 0 {
20+
page = pagination.Page
21+
}
22+
23+
pageSize := 25
24+
if pagination.PageSize >= 0 {
25+
pageSize = pagination.PageSize
26+
}
27+
28+
offset := (page - 1) * pageSize
29+
return conn.Offset(offset).Limit(pageSize)
30+
}
31+
}
32+
33+
type PaginatedResult[T any] struct {
34+
Results []T
35+
Total int64
36+
}

components/gitpod-db/go/personal_access_token.go

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ type PersonalAccessToken struct {
3131
_ bool `gorm:"column:deleted;type:tinyint;default:0;" json:"deleted"`
3232
}
3333

34-
type Scopes []string
35-
3634
// TableName sets the insert table name for this struct type
3735
func (d *PersonalAccessToken) TableName() string {
3836
return "d_b_personal_access_token"
@@ -77,23 +75,66 @@ func CreateToken(ctx context.Context, conn *gorm.DB, req PersonalAccessToken) (P
7775
LastModified: time.Now().UTC(),
7876
}
7977

80-
db := conn.WithContext(ctx).Create(req)
81-
if db.Error != nil {
78+
tx := conn.WithContext(ctx).Create(req)
79+
if tx.Error != nil {
8280
return PersonalAccessToken{}, fmt.Errorf("Failed to create token for user %s", req.UserID)
8381
}
8482

8583
return token, nil
8684
}
8785

86+
func ListPersonalAccessTokensForUser(ctx context.Context, conn *gorm.DB, userID uuid.UUID, pagination Pagination) (*PaginatedResult[PersonalAccessToken], error) {
87+
if userID == uuid.Nil {
88+
return nil, fmt.Errorf("user ID is a required argument to list personal access tokens for user, got nil")
89+
}
90+
91+
var results []PersonalAccessToken
92+
93+
tx := conn.
94+
WithContext(ctx).
95+
Table((&PersonalAccessToken{}).TableName()).
96+
Where("userId = ?", userID).
97+
Order("createdAt").
98+
Scopes(Paginate(pagination)).
99+
Find(&results)
100+
if tx.Error != nil {
101+
return nil, fmt.Errorf("failed to list personal access tokens for user %s: %w", userID.String(), tx.Error)
102+
}
103+
104+
var count int64
105+
tx = conn.
106+
WithContext(ctx).
107+
Table((&PersonalAccessToken{}).TableName()).
108+
Where("userId = ?", userID).
109+
Count(&count)
110+
if tx.Error != nil {
111+
return nil, fmt.Errorf("failed to count total number of personal access tokens for user %s: %w", userID.String(), tx.Error)
112+
}
113+
114+
return &PaginatedResult[PersonalAccessToken]{
115+
Results: results,
116+
Total: count,
117+
}, nil
118+
}
119+
120+
type Scopes []string
121+
88122
// Scan() and Value() allow having a list of strings as a type for Scopes
89123
func (s *Scopes) Scan(src any) error {
90124
bytes, ok := src.([]byte)
91125
if !ok {
92126
return errors.New("src value cannot cast to []byte")
93127
}
128+
129+
if len(bytes) == 0 {
130+
*s = nil
131+
return nil
132+
}
133+
94134
*s = strings.Split(string(bytes), ",")
95135
return nil
96136
}
137+
97138
func (s Scopes) Value() (driver.Value, error) {
98139
if len(s) == 0 {
99140
return "", nil

components/gitpod-db/go/personal_access_token_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package db_test
66

77
import (
88
"context"
9+
"strconv"
910
"testing"
1011
"time"
1112

@@ -58,3 +59,85 @@ func TestPersonalAccessToken_Create(t *testing.T) {
5859

5960
require.Equal(t, request.ID, result.ID)
6061
}
62+
63+
func TestListPersonalAccessTokensForUser(t *testing.T) {
64+
ctx := context.Background()
65+
conn := dbtest.ConnectForTests(t)
66+
pagination := db.Pagination{
67+
Page: 1,
68+
PageSize: 10,
69+
}
70+
71+
userA := uuid.New()
72+
userB := uuid.New()
73+
74+
now := time.Now().UTC()
75+
76+
dbtest.CreatePersonalAccessTokenRecords(t, conn,
77+
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
78+
UserID: userA,
79+
CreatedAt: now.Add(-1 * time.Minute),
80+
}),
81+
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
82+
UserID: userA,
83+
CreatedAt: now,
84+
}),
85+
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
86+
UserID: userB,
87+
}),
88+
)
89+
90+
tokensForUserA, err := db.ListPersonalAccessTokensForUser(ctx, conn, userA, pagination)
91+
require.NoError(t, err)
92+
require.Len(t, tokensForUserA.Results, 2)
93+
94+
tokensForUserB, err := db.ListPersonalAccessTokensForUser(ctx, conn, userB, pagination)
95+
require.NoError(t, err)
96+
require.Len(t, tokensForUserB.Results, 1)
97+
98+
tokensForUserWithNoData, err := db.ListPersonalAccessTokensForUser(ctx, conn, uuid.New(), pagination)
99+
require.NoError(t, err)
100+
require.Len(t, tokensForUserWithNoData.Results, 0)
101+
}
102+
103+
func TestListPersonalAccessTokensForUser_PaginateThroughResults(t *testing.T) {
104+
ctx := context.Background()
105+
conn := dbtest.ConnectForTests(t).Debug()
106+
107+
userA := uuid.New()
108+
109+
total := 11
110+
var toCreate []db.PersonalAccessToken
111+
for i := 0; i < total; i++ {
112+
toCreate = append(toCreate, dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
113+
UserID: userA,
114+
Name: strconv.Itoa(i),
115+
}))
116+
}
117+
118+
dbtest.CreatePersonalAccessTokenRecords(t, conn, toCreate...)
119+
120+
batch1, err := db.ListPersonalAccessTokensForUser(ctx, conn, userA, db.Pagination{
121+
Page: 1,
122+
PageSize: 5,
123+
})
124+
require.NoError(t, err)
125+
require.Len(t, batch1.Results, 5)
126+
require.EqualValues(t, batch1.Total, total)
127+
128+
batch2, err := db.ListPersonalAccessTokensForUser(ctx, conn, userA, db.Pagination{
129+
Page: 2,
130+
PageSize: 5,
131+
})
132+
require.NoError(t, err)
133+
require.Len(t, batch2.Results, 5)
134+
require.EqualValues(t, batch2.Total, total)
135+
136+
batch3, err := db.ListPersonalAccessTokensForUser(ctx, conn, userA, db.Pagination{
137+
Page: 3,
138+
PageSize: 5,
139+
})
140+
require.NoError(t, err)
141+
require.Len(t, batch3.Results, 1)
142+
require.EqualValues(t, batch3.Total, total)
143+
}

components/public-api-server/BUILD.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ packages:
1111
- components/usage-api/go:lib
1212
- components/gitpod-protocol/go:lib
1313
- components/gitpod-db/go:lib
14+
- components/gitpod-db/go:init-testdb
1415
env:
1516
- CGO_ENABLED=0
1617
- GOOS=linux
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright (c) 2022 Gitpod GmbH. All rights reserved.
2+
// Licensed under the GNU Affero General Public License (AGPL).
3+
// See License-AGPL.txt in the project root for license information.
4+
5+
package apiv1
6+
7+
import (
8+
db "github.com/gitpod-io/gitpod/components/gitpod-db/go"
9+
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
10+
)
11+
12+
func validatePagination(p *v1.Pagination) *v1.Pagination {
13+
pagination := &v1.Pagination{
14+
PageSize: 25,
15+
Page: 1,
16+
}
17+
18+
if p == nil {
19+
return pagination
20+
}
21+
22+
if p.Page > 0 {
23+
pagination.Page = p.Page
24+
}
25+
if p.PageSize > 0 && p.PageSize <= 100 {
26+
pagination.PageSize = p.PageSize
27+
}
28+
29+
return pagination
30+
}
31+
32+
func paginationToDB(p *v1.Pagination) db.Pagination {
33+
validated := validatePagination(p)
34+
return db.Pagination{
35+
Page: int(validated.GetPage()),
36+
PageSize: int(validated.GetPageSize()),
37+
}
38+
}

0 commit comments

Comments
 (0)