Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/public-api-server/pkg/apiv1/tokens_test.go
2499 views
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
"context"
9
"net/http"
10
"net/http/httptest"
11
"strings"
12
"testing"
13
"time"
14
15
"google.golang.org/protobuf/types/known/fieldmaskpb"
16
17
connect "github.com/bufbuild/connect-go"
18
"github.com/gitpod-io/gitpod/common-go/experiments"
19
"github.com/gitpod-io/gitpod/common-go/experiments/experimentstest"
20
db "github.com/gitpod-io/gitpod/components/gitpod-db/go"
21
"github.com/gitpod-io/gitpod/components/gitpod-db/go/dbtest"
22
"github.com/gitpod-io/gitpod/components/public-api/go/config"
23
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
24
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
25
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
26
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
27
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws"
28
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws/jwstest"
29
"github.com/golang/mock/gomock"
30
"github.com/google/uuid"
31
"github.com/stretchr/testify/require"
32
"google.golang.org/protobuf/types/known/timestamppb"
33
"gorm.io/gorm"
34
)
35
36
var (
37
experimentsClient = &experimentstest.Client{}
38
39
signer = auth.NewHS256Signer([]byte("my-secret"))
40
41
teams = []*protocol.Team{newTeam(&protocol.Team{})}
42
)
43
44
func TestTokensService_CreatePersonalAccessTokenWithoutFeatureFlag(t *testing.T) {
45
user := newUser(&protocol.User{})
46
47
t.Run("invalid argument when name is not specified", func(t *testing.T) {
48
_, _, client := setupTokensService(t, experimentsClient)
49
50
_, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
51
Token: &v1.PersonalAccessToken{},
52
}))
53
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
54
})
55
56
t.Run("invalid argument when name does not match required regex", func(t *testing.T) {
57
_, _, client := setupTokensService(t, experimentsClient)
58
59
names := []string{"a", "ab", strings.Repeat("a", 64), "!#$!%"}
60
61
for _, name := range names {
62
_, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
63
Token: &v1.PersonalAccessToken{
64
Name: name,
65
},
66
}))
67
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
68
}
69
})
70
71
t.Run("invalid argument when expiration time is unspecified", func(t *testing.T) {
72
_, _, client := setupTokensService(t, experimentsClient)
73
74
_, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
75
Token: &v1.PersonalAccessToken{
76
Name: "my-token",
77
},
78
}))
79
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
80
})
81
82
t.Run("invalid argument when expiration time is invalid", func(t *testing.T) {
83
_, _, client := setupTokensService(t, experimentsClient)
84
85
_, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
86
Token: &v1.PersonalAccessToken{
87
Name: "my-token",
88
ExpirationTime: &timestamppb.Timestamp{
89
Seconds: 253402300799 + 1,
90
},
91
},
92
}))
93
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
94
})
95
96
t.Run("invalid argument when disallowed scopes used", func(t *testing.T) {
97
_, _, client := setupTokensService(t, experimentsClient)
98
99
_, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
100
Token: &v1.PersonalAccessToken{
101
Name: "my-token",
102
ExpirationTime: timestamppb.Now(),
103
Scopes: []string{"random:scope"},
104
},
105
}))
106
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
107
})
108
109
t.Run("crates personal access token", func(t *testing.T) {
110
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
111
112
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
113
114
token := &v1.PersonalAccessToken{
115
Name: "my-token",
116
ExpirationTime: timestamppb.Now(),
117
}
118
119
response, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
120
Token: token,
121
}))
122
require.NoError(t, err)
123
124
created := response.Msg.GetToken()
125
t.Cleanup(func() {
126
require.NoError(t, dbConn.Where("id = ?", created.GetId()).Delete(&db.PersonalAccessToken{}).Error)
127
})
128
129
require.NotEmpty(t, created.GetId())
130
require.Equal(t, token.Name, created.GetName())
131
require.Equal(t, token.Scopes, created.GetScopes())
132
requireEqualProto(t, token.GetExpirationTime(), created.GetExpirationTime())
133
134
// Returned token must be parseable
135
_, err = auth.ParsePersonalAccessToken(created.GetValue(), signer)
136
require.NoError(t, err)
137
138
// token must exist in the DB, with the User ID of the requestor
139
storedInDB, err := db.GetPersonalAccessTokenForUser(context.Background(), dbConn, uuid.MustParse(created.GetId()), uuid.MustParse(user.ID))
140
require.NoError(t, err)
141
require.Equal(t, user.ID, storedInDB.UserID.String())
142
})
143
144
t.Run("crates personal access token with no scopes when none provided", func(t *testing.T) {
145
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
146
147
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
148
149
token := &v1.PersonalAccessToken{
150
Name: "my-token",
151
ExpirationTime: timestamppb.Now(),
152
}
153
154
response, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
155
Token: token,
156
}))
157
require.NoError(t, err)
158
159
created := response.Msg.GetToken()
160
t.Cleanup(func() {
161
require.NoError(t, dbConn.Where("id = ?", created.GetId()).Delete(&db.PersonalAccessToken{}).Error)
162
})
163
164
require.Len(t, created.GetScopes(), 0, "must have no scopes, none were provided in the request")
165
})
166
167
t.Run("crates personal access token with full access when correct scopes provided", func(t *testing.T) {
168
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
169
170
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
171
172
token := &v1.PersonalAccessToken{
173
Name: "my-token",
174
ExpirationTime: timestamppb.Now(),
175
Scopes: []string{"resource:default", "function:*"},
176
}
177
178
response, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
179
Token: token,
180
}))
181
require.NoError(t, err)
182
183
created := response.Msg.GetToken()
184
t.Cleanup(func() {
185
require.NoError(t, dbConn.Where("id = ?", created.GetId()).Delete(&db.PersonalAccessToken{}).Error)
186
})
187
188
require.Equal(t, []string{allFunctionsScope, defaultResourceScope}, created.GetScopes())
189
})
190
}
191
192
func TestTokensService_GetPersonalAccessToken(t *testing.T) {
193
user := newUser(&protocol.User{})
194
user2 := newUser(&protocol.User{})
195
196
t.Run("get correct token", func(t *testing.T) {
197
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
198
199
tokens := dbtest.CreatePersonalAccessTokenRecords(t, dbConn,
200
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
201
UserID: uuid.MustParse(user.ID),
202
}),
203
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
204
UserID: uuid.MustParse(user2.ID),
205
}),
206
)
207
208
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
209
210
response, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{
211
Id: tokens[0].ID.String(),
212
}))
213
214
require.NoError(t, err)
215
216
requireEqualProto(t, &v1.GetPersonalAccessTokenResponse{
217
Token: personalAccessTokenToAPI(tokens[0], ""),
218
}, response.Msg)
219
})
220
221
t.Run("invalid argument when Token ID is empty", func(t *testing.T) {
222
_, _, client := setupTokensService(t, experimentsClient)
223
224
_, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{}))
225
226
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
227
})
228
229
t.Run("invalid argument when Token ID is not a valid UUID", func(t *testing.T) {
230
_, _, client := setupTokensService(t, experimentsClient)
231
232
_, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{
233
Id: "foo-bar",
234
}))
235
236
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
237
})
238
239
t.Run("responds with not found when token is not found", func(t *testing.T) {
240
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
241
242
someTokenId := uuid.New().String()
243
244
dbtest.CreatePersonalAccessTokenRecords(t, dbConn,
245
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
246
UserID: uuid.MustParse(user.ID),
247
}),
248
)
249
250
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
251
252
_, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{
253
Id: someTokenId,
254
}))
255
require.Error(t, err)
256
require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
257
})
258
}
259
260
func TestTokensService_ListPersonalAccessTokens(t *testing.T) {
261
user := newUser(&protocol.User{})
262
263
t.Run("no tokens returns empty list", func(t *testing.T) {
264
serverMock, _, client := setupTokensService(t, experimentsClient)
265
266
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
267
268
response, err := client.ListPersonalAccessTokens(context.Background(), connect.NewRequest(&v1.ListPersonalAccessTokensRequest{}))
269
require.NoError(t, err)
270
271
requireEqualProto(t, &v1.ListPersonalAccessTokensResponse{
272
Tokens: nil,
273
TotalResults: 0,
274
}, response.Msg)
275
})
276
277
t.Run("lists first page of results, when no pagination preference specified", func(t *testing.T) {
278
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
279
280
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
281
282
now := time.Now().UTC().Round(time.Millisecond)
283
tokens := dbtest.CreatePersonalAccessTokenRecords(t, dbConn,
284
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
285
UserID: uuid.MustParse(user.ID),
286
CreatedAt: now.Add(-1 * time.Minute),
287
}),
288
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
289
UserID: uuid.MustParse(user.ID),
290
CreatedAt: now,
291
}),
292
)
293
294
response, err := client.ListPersonalAccessTokens(context.Background(), connect.NewRequest(&v1.ListPersonalAccessTokensRequest{}))
295
require.NoError(t, err)
296
297
requireEqualProto(t, &v1.ListPersonalAccessTokensResponse{
298
Tokens: personalAccessTokensToAPI(tokens),
299
TotalResults: 2,
300
}, response.Msg)
301
})
302
303
t.Run("paginating through results", func(t *testing.T) {
304
now := time.Now().UTC().Round(time.Millisecond)
305
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
306
307
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).Times(3)
308
309
var toCreate []db.PersonalAccessToken
310
total := 5
311
for i := 5; i > 0; i-- {
312
toCreate = append(toCreate, dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
313
UserID: uuid.MustParse(user.ID),
314
CreatedAt: now.Add(time.Duration(-1*i) * time.Minute),
315
}))
316
}
317
318
tokens := dbtest.CreatePersonalAccessTokenRecords(t, dbConn, toCreate...)
319
320
firstPage, err := client.ListPersonalAccessTokens(context.Background(), connect.NewRequest(&v1.ListPersonalAccessTokensRequest{
321
Pagination: &v1.Pagination{
322
PageSize: 2,
323
Page: 1,
324
},
325
}))
326
require.NoError(t, err)
327
requireEqualProto(t, &v1.ListPersonalAccessTokensResponse{
328
Tokens: personalAccessTokensToAPI(tokens[0:2]),
329
TotalResults: int64(total),
330
}, firstPage.Msg)
331
332
secondPage, err := client.ListPersonalAccessTokens(context.Background(), connect.NewRequest(&v1.ListPersonalAccessTokensRequest{
333
Pagination: &v1.Pagination{
334
PageSize: 2,
335
Page: 2,
336
},
337
}))
338
require.NoError(t, err)
339
requireEqualProto(t, &v1.ListPersonalAccessTokensResponse{
340
Tokens: personalAccessTokensToAPI(tokens[2:4]),
341
TotalResults: int64(total),
342
}, secondPage.Msg)
343
344
thirdPage, err := client.ListPersonalAccessTokens(context.Background(), connect.NewRequest(&v1.ListPersonalAccessTokensRequest{
345
Pagination: &v1.Pagination{
346
PageSize: 2,
347
Page: 3,
348
},
349
}))
350
require.NoError(t, err)
351
requireEqualProto(t, &v1.ListPersonalAccessTokensResponse{
352
Tokens: personalAccessTokensToAPI([]db.PersonalAccessToken{tokens[4]}),
353
TotalResults: int64(total),
354
}, thirdPage.Msg)
355
})
356
}
357
358
func TestTokensService_RegeneratePersonalAccessToken(t *testing.T) {
359
user := newUser(&protocol.User{})
360
user2 := newUser(&protocol.User{})
361
362
t.Run("invalid argument when Token ID is empty", func(t *testing.T) {
363
_, _, client := setupTokensService(t, experimentsClient)
364
365
_, err := client.RegeneratePersonalAccessToken(context.Background(), connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{}))
366
367
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
368
})
369
370
t.Run("invalid argument when Token ID is not a valid UUID", func(t *testing.T) {
371
_, _, client := setupTokensService(t, experimentsClient)
372
373
_, err := client.RegeneratePersonalAccessToken(context.Background(), connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{
374
Id: "foo-bar",
375
}))
376
377
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
378
})
379
380
t.Run("responds with not found when token is not found", func(t *testing.T) {
381
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
382
383
someTokenId := uuid.New().String()
384
385
dbtest.CreatePersonalAccessTokenRecords(t, dbConn,
386
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
387
UserID: uuid.MustParse(user.ID),
388
}),
389
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
390
UserID: uuid.MustParse(user2.ID),
391
}),
392
)
393
394
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
395
396
newTimestamp := timestamppb.New(time.Date(2023, 1, 2, 15, 4, 5, 0, time.UTC))
397
_, err := client.RegeneratePersonalAccessToken(context.Background(), connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{
398
Id: someTokenId,
399
ExpirationTime: newTimestamp,
400
}))
401
require.Error(t, err)
402
require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
403
})
404
405
t.Run("regenerate correct token", func(t *testing.T) {
406
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
407
408
tokens := dbtest.CreatePersonalAccessTokenRecords(t, dbConn,
409
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
410
UserID: uuid.MustParse(user.ID),
411
}),
412
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
413
UserID: uuid.MustParse(user2.ID),
414
}),
415
)
416
417
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).MaxTimes(2)
418
419
origResponse, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{
420
Id: tokens[0].ID.String(),
421
}))
422
require.NoError(t, err)
423
424
newTimestamp := timestamppb.New(time.Now().Add(24 * time.Hour).UTC().Truncate(time.Millisecond))
425
response, err := client.RegeneratePersonalAccessToken(context.Background(), connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{
426
Id: tokens[0].ID.String(),
427
ExpirationTime: newTimestamp,
428
}))
429
require.NoError(t, err)
430
431
require.Equal(t, origResponse.Msg.Token.Id, response.Msg.Token.Id)
432
require.NotEqual(t, "", response.Msg.Token.Value)
433
require.Equal(t, origResponse.Msg.Token.Name, response.Msg.Token.Name)
434
require.Equal(t, origResponse.Msg.Token.Scopes, response.Msg.Token.Scopes)
435
require.Equal(t, newTimestamp.AsTime(), response.Msg.Token.ExpirationTime.AsTime())
436
require.Equal(t, origResponse.Msg.Token.CreatedAt, response.Msg.Token.CreatedAt)
437
})
438
}
439
440
func TestTokensService_UpdatePersonalAccessToken(t *testing.T) {
441
user := newUser(&protocol.User{})
442
443
t.Run("invalid argument when Token ID is empty", func(t *testing.T) {
444
_, _, client := setupTokensService(t, experimentsClient)
445
446
_, err := client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{}))
447
448
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
449
})
450
451
t.Run("invalid argument when Token ID is not a valid UUID", func(t *testing.T) {
452
_, _, client := setupTokensService(t, experimentsClient)
453
454
_, err := client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{
455
Token: &v1.PersonalAccessToken{
456
Id: "foo-bar",
457
},
458
}))
459
460
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
461
})
462
463
t.Run("allows unmodified udpate", func(t *testing.T) {
464
serverMock, _, client := setupTokensService(t, experimentsClient)
465
466
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).Times(2)
467
468
createResponse, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
469
Token: &v1.PersonalAccessToken{
470
Name: "first",
471
ExpirationTime: timestamppb.Now(),
472
},
473
}))
474
require.NoError(t, err)
475
476
_, err = client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{
477
Token: &v1.PersonalAccessToken{
478
Id: createResponse.Msg.GetToken().GetId(),
479
Name: createResponse.Msg.GetToken().GetName(),
480
},
481
UpdateMask: &fieldmaskpb.FieldMask{
482
Paths: []string{"name", "scopes"},
483
},
484
}))
485
require.NoError(t, err)
486
})
487
488
t.Run("default updates both name and scopes, when no mask specified", func(t *testing.T) {
489
serverMock, _, client := setupTokensService(t, experimentsClient)
490
491
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).Times(2)
492
493
createResponse, err := client.CreatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
494
Token: &v1.PersonalAccessToken{
495
Name: "first",
496
ExpirationTime: timestamppb.Now(),
497
},
498
}))
499
require.NoError(t, err)
500
501
updateResponse, err := client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{
502
Token: &v1.PersonalAccessToken{
503
Id: createResponse.Msg.GetToken().GetId(),
504
Name: "second",
505
Scopes: []string{allFunctionsScope, defaultResourceScope},
506
},
507
}))
508
require.NoError(t, err)
509
require.Equal(t, "second", updateResponse.Msg.GetToken().GetName())
510
require.Equal(t, []string{allFunctionsScope, defaultResourceScope}, updateResponse.Msg.GetToken().GetScopes())
511
})
512
513
t.Run("updates only name, when mask specifies name", func(t *testing.T) {
514
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
515
516
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
517
518
created := dbtest.CreatePersonalAccessTokenRecords(t, dbConn, dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
519
Name: "first",
520
UserID: uuid.MustParse(user.ID),
521
Scopes: db.Scopes{allFunctionsScope, defaultResourceScope},
522
}))[0]
523
524
updateResponse, err := client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{
525
Token: &v1.PersonalAccessToken{
526
Id: created.ID.String(),
527
Name: "second",
528
Scopes: []string{allFunctionsScope, defaultResourceScope},
529
},
530
UpdateMask: &fieldmaskpb.FieldMask{
531
Paths: []string{"name"},
532
},
533
}))
534
require.NoError(t, err)
535
require.Equal(t, "second", updateResponse.Msg.GetToken().GetName())
536
require.Equal(t, []string(created.Scopes), updateResponse.Msg.GetToken().GetScopes())
537
})
538
539
t.Run("updates only scopes, when mask specifies scopes", func(t *testing.T) {
540
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
541
542
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil)
543
544
created := dbtest.CreatePersonalAccessTokenRecords(t, dbConn, dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
545
Name: "first",
546
UserID: uuid.MustParse(user.ID),
547
Scopes: db.Scopes{allFunctionsScope, defaultResourceScope},
548
}))[0]
549
550
updateResponse, err := client.UpdatePersonalAccessToken(context.Background(), connect.NewRequest(&v1.UpdatePersonalAccessTokenRequest{
551
Token: &v1.PersonalAccessToken{
552
Id: created.ID.String(),
553
Name: "second",
554
Scopes: []string{allFunctionsScope, defaultResourceScope},
555
},
556
UpdateMask: &fieldmaskpb.FieldMask{
557
Paths: []string{"scopes"},
558
},
559
}))
560
require.NoError(t, err)
561
require.Equal(t, "first", updateResponse.Msg.GetToken().GetName())
562
require.Equal(t, []string{allFunctionsScope, defaultResourceScope}, updateResponse.Msg.GetToken().GetScopes())
563
})
564
}
565
566
func TestTokensService_DeletePersonalAccessToken(t *testing.T) {
567
user := newUser(&protocol.User{})
568
569
t.Run("invalid argument when Token ID is empty", func(t *testing.T) {
570
_, _, client := setupTokensService(t, experimentsClient)
571
572
_, err := client.DeletePersonalAccessToken(context.Background(), connect.NewRequest(&v1.DeletePersonalAccessTokenRequest{}))
573
574
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
575
})
576
577
t.Run("invalid argument when Token ID is not a valid UUID", func(t *testing.T) {
578
_, _, client := setupTokensService(t, experimentsClient)
579
580
_, err := client.DeletePersonalAccessToken(context.Background(), connect.NewRequest(&v1.DeletePersonalAccessTokenRequest{
581
Id: "foo-bar",
582
}))
583
584
require.Equal(t, connect.CodeInvalidArgument, connect.CodeOf(err))
585
})
586
587
t.Run("delete token", func(t *testing.T) {
588
serverMock, dbConn, client := setupTokensService(t, experimentsClient)
589
590
tokens := dbtest.CreatePersonalAccessTokenRecords(t, dbConn,
591
dbtest.NewPersonalAccessToken(t, db.PersonalAccessToken{
592
UserID: uuid.MustParse(user.ID),
593
}),
594
)
595
596
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).Times(3)
597
598
_, err := client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{
599
Id: tokens[0].ID.String(),
600
}))
601
require.NoError(t, err)
602
603
_, err = client.DeletePersonalAccessToken(context.Background(), connect.NewRequest(&v1.DeletePersonalAccessTokenRequest{
604
Id: tokens[0].ID.String(),
605
}))
606
require.NoError(t, err)
607
608
_, err = client.GetPersonalAccessToken(context.Background(), connect.NewRequest(&v1.GetPersonalAccessTokenRequest{
609
Id: tokens[0].ID.String(),
610
}))
611
require.Error(t, err)
612
require.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
613
})
614
}
615
616
func TestTokensService_Workflow(t *testing.T) {
617
ctx := context.Background()
618
now := time.Now().UTC().Round(time.Millisecond)
619
user := newUser(&protocol.User{})
620
serverMock, _, client := setupTokensService(t, experimentsClient)
621
622
serverMock.EXPECT().GetLoggedInUser(gomock.Any()).Return(user, nil).AnyTimes()
623
624
// Create 1 token
625
createdTokenResponse, err := client.CreatePersonalAccessToken(ctx, connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
626
Token: &v1.PersonalAccessToken{
627
Name: "my-first-token",
628
ExpirationTime: timestamppb.New(now.Add(1 * time.Hour)),
629
},
630
}))
631
require.NoError(t, err)
632
633
_, err = client.GetPersonalAccessToken(ctx, connect.NewRequest(&v1.GetPersonalAccessTokenRequest{
634
Id: createdTokenResponse.Msg.Token.GetId(),
635
}))
636
require.NoError(t, err, "must retrieve the token we created")
637
638
response, err := client.ListPersonalAccessTokens(ctx, connect.NewRequest(&v1.ListPersonalAccessTokensRequest{}))
639
require.NoError(t, err)
640
require.Len(t, response.Msg.Tokens, 1, "must retrieve one token which we created earlier")
641
642
// Create a second token
643
secondTokenResponse, err := client.CreatePersonalAccessToken(ctx, connect.NewRequest(&v1.CreatePersonalAccessTokenRequest{
644
Token: &v1.PersonalAccessToken{
645
Name: "my-second-token",
646
ExpirationTime: timestamppb.New(now.Add(1 * time.Hour)),
647
},
648
}))
649
require.NoError(t, err)
650
651
response, err = client.ListPersonalAccessTokens(ctx, connect.NewRequest(&v1.ListPersonalAccessTokensRequest{}))
652
require.NoError(t, err)
653
require.Len(t, response.Msg.Tokens, 2, "must retrieve both tokens we created")
654
655
_, err = client.RegeneratePersonalAccessToken(ctx, connect.NewRequest(&v1.RegeneratePersonalAccessTokenRequest{
656
Id: secondTokenResponse.Msg.GetToken().GetId(),
657
ExpirationTime: timestamppb.New(now.Add(2 * time.Hour)),
658
}))
659
require.NoError(t, err)
660
661
response, err = client.ListPersonalAccessTokens(ctx, connect.NewRequest(&v1.ListPersonalAccessTokensRequest{}))
662
require.NoError(t, err)
663
require.Len(t, response.Msg.Tokens, 2, "must retrieve both tokens")
664
665
_, err = client.DeletePersonalAccessToken(ctx, connect.NewRequest(&v1.DeletePersonalAccessTokenRequest{
666
Id: secondTokenResponse.Msg.GetToken().GetId(),
667
}))
668
require.NoError(t, err)
669
}
670
671
func TestValidateScopes(t *testing.T) {
672
for _, s := range []struct {
673
Name string
674
RequestedScopes []string
675
Error bool
676
}{
677
{
678
Name: "no scopes are permitted",
679
RequestedScopes: nil,
680
},
681
{
682
Name: "empty scopes are permitted",
683
RequestedScopes: []string{},
684
},
685
{
686
Name: "all scopes are permitted",
687
RequestedScopes: []string{"function:*", "resource:default"},
688
},
689
{
690
Name: "all scopes (unsorted) are permitted",
691
RequestedScopes: []string{"resource:default", "function:*"},
692
},
693
{
694
Name: "only all function scope is not permitted",
695
RequestedScopes: []string{"function:*"},
696
Error: true,
697
},
698
{
699
Name: "only all default resource scope is not permitted",
700
RequestedScopes: []string{"resource:default"},
701
Error: true,
702
},
703
{
704
Name: "unknown scope is rejected",
705
RequestedScopes: []string{"unknown"},
706
Error: true,
707
},
708
{
709
Name: "unknown scope, with all scopes, is rejected",
710
RequestedScopes: []string{"unknown", "function:*", "resource:default"},
711
Error: true,
712
},
713
} {
714
t.Run(s.Name, func(t *testing.T) {
715
_, err := validateScopes(s.RequestedScopes)
716
717
if s.Error {
718
require.Error(t, err)
719
} else {
720
require.NoError(t, err)
721
}
722
})
723
724
}
725
}
726
727
func setupTokensService(t *testing.T, expClient experiments.Client) (*protocol.MockAPIInterface, *gorm.DB, v1connect.TokensServiceClient) {
728
t.Helper()
729
730
dbConn := dbtest.ConnectForTests(t)
731
732
ctrl := gomock.NewController(t)
733
t.Cleanup(ctrl.Finish)
734
735
serverMock := protocol.NewMockAPIInterface(ctrl)
736
737
svc := NewTokensService(&FakeServerConnPool{api: serverMock}, expClient, dbConn, signer)
738
739
keyset := jwstest.GenerateKeySet(t)
740
rsa256, err := jws.NewRSA256(keyset)
741
require.NoError(t, err)
742
743
_, handler := v1connect.NewTokensServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{
744
Issuer: "unitetest.com",
745
Cookie: config.CookieConfig{
746
Name: "cookie_jwt",
747
},
748
}, rsa256)))
749
750
srv := httptest.NewServer(handler)
751
t.Cleanup(srv.Close)
752
753
client := v1connect.NewTokensServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(
754
auth.NewClientInterceptor("auth-token"),
755
))
756
757
return serverMock, dbConn, client
758
}
759
760