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/identityprovider_test.go
2499 views
1
// Copyright (c) 2023 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
"errors"
10
"fmt"
11
"net/http"
12
"net/http/httptest"
13
"testing"
14
15
connect "github.com/bufbuild/connect-go"
16
"github.com/gitpod-io/gitpod/common-go/experiments"
17
"github.com/gitpod-io/gitpod/common-go/experiments/experimentstest"
18
"github.com/gitpod-io/gitpod/components/public-api/go/config"
19
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
20
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
21
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
22
"github.com/gitpod-io/gitpod/public-api-server/pkg/auth"
23
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws"
24
"github.com/gitpod-io/gitpod/public-api-server/pkg/jws/jwstest"
25
"github.com/golang/mock/gomock"
26
"github.com/google/go-cmp/cmp"
27
"github.com/google/go-cmp/cmp/cmpopts"
28
"github.com/sourcegraph/jsonrpc2"
29
"github.com/stretchr/testify/require"
30
"github.com/zitadel/oidc/pkg/oidc"
31
)
32
33
func TestGetIDToken(t *testing.T) {
34
const workspaceID = "gitpodio-gitpod-te23l4bjejv"
35
type Expectation struct {
36
Error string
37
Response *v1.GetIDTokenResponse
38
}
39
tests := []struct {
40
Name string
41
TokenSource func(t *testing.T) IDTokenSource
42
ServerSetup func(*protocol.MockAPIInterface)
43
Request *v1.GetIDTokenRequest
44
45
Expectation Expectation
46
}{
47
{
48
Name: "org-owned user",
49
TokenSource: func(t *testing.T) IDTokenSource {
50
return functionIDTokenSource(func(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error) {
51
require.Equal(t, "[email protected]", userInfo.GetEmail())
52
require.True(t, userInfo.IsEmailVerified())
53
54
return "foobar", nil
55
})
56
},
57
ServerSetup: func(ma *protocol.MockAPIInterface) {
58
ma.EXPECT().GetIDToken(gomock.Any()).MinTimes(1).Return(nil)
59
ma.EXPECT().GetWorkspace(gomock.Any(), workspaceID).MinTimes(1).Return(
60
&protocol.WorkspaceInfo{
61
Workspace: &protocol.Workspace{
62
ContextURL: "https://github.com/gitpod-io/gitpod",
63
Context: &protocol.WorkspaceContext{
64
Repository: &protocol.Repository{
65
CloneURL: "https://github.com/gitpod-io/gitpod.git",
66
},
67
NormalizedContextURL: "https://github.com/gitpod-io/gitpod",
68
},
69
},
70
},
71
nil,
72
)
73
ma.EXPECT().GetLoggedInUser(gomock.Any()).Return(
74
&protocol.User{
75
Name: "foobar",
76
Identities: []*protocol.Identity{
77
nil,
78
{Deleted: true, PrimaryEmail: "[email protected]"},
79
{Deleted: false, PrimaryEmail: "[email protected]", LastSigninTime: "2021-01-01T00:00:00Z"},
80
},
81
OrganizationId: "test",
82
},
83
nil,
84
)
85
},
86
Request: &v1.GetIDTokenRequest{
87
WorkspaceId: workspaceID,
88
Audience: []string{"some.audience.com"},
89
},
90
Expectation: Expectation{
91
Response: &v1.GetIDTokenResponse{
92
Token: "foobar",
93
},
94
},
95
},
96
{
97
Name: "none org-owned user",
98
TokenSource: func(t *testing.T) IDTokenSource {
99
return functionIDTokenSource(func(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error) {
100
require.Equal(t, "[email protected]", userInfo.GetEmail())
101
require.False(t, userInfo.IsEmailVerified())
102
103
return "foobar", nil
104
})
105
},
106
ServerSetup: func(ma *protocol.MockAPIInterface) {
107
ma.EXPECT().GetIDToken(gomock.Any()).MinTimes(1).Return(nil)
108
ma.EXPECT().GetWorkspace(gomock.Any(), workspaceID).MinTimes(1).Return(
109
&protocol.WorkspaceInfo{
110
Workspace: &protocol.Workspace{
111
ContextURL: "https://github.com/gitpod-io/gitpod",
112
Context: &protocol.WorkspaceContext{
113
Repository: &protocol.Repository{
114
CloneURL: "https://github.com/gitpod-io/gitpod.git",
115
},
116
NormalizedContextURL: "https://github.com/gitpod-io/gitpod",
117
},
118
},
119
},
120
nil,
121
)
122
ma.EXPECT().GetLoggedInUser(gomock.Any()).Return(
123
&protocol.User{
124
Name: "foobar",
125
Identities: []*protocol.Identity{
126
nil,
127
{Deleted: true, PrimaryEmail: "[email protected]"},
128
{Deleted: false, PrimaryEmail: "[email protected]", LastSigninTime: "2021-01-01T00:00:00Z"},
129
},
130
},
131
nil,
132
)
133
},
134
Request: &v1.GetIDTokenRequest{
135
WorkspaceId: workspaceID,
136
Audience: []string{"some.audience.com"},
137
},
138
Expectation: Expectation{
139
Response: &v1.GetIDTokenResponse{
140
Token: "foobar",
141
},
142
},
143
},
144
{
145
Name: "workspace not found",
146
TokenSource: func(t *testing.T) IDTokenSource {
147
return functionIDTokenSource(func(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error) {
148
return "foobar", nil
149
})
150
},
151
ServerSetup: func(ma *protocol.MockAPIInterface) {
152
ma.EXPECT().GetIDToken(gomock.Any()).MinTimes(1).Return(nil)
153
ma.EXPECT().GetWorkspace(gomock.Any(), workspaceID).MinTimes(1).Return(
154
nil,
155
&jsonrpc2.Error{Code: 400, Message: "workspace not found"},
156
)
157
},
158
Request: &v1.GetIDTokenRequest{
159
WorkspaceId: workspaceID,
160
Audience: []string{"some.audience.com"},
161
},
162
Expectation: Expectation{
163
Error: connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("workspace not found")).Error(),
164
},
165
},
166
{
167
Name: "no logged in user",
168
TokenSource: func(t *testing.T) IDTokenSource {
169
return functionIDTokenSource(func(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error) {
170
return "foobar", nil
171
})
172
},
173
ServerSetup: func(ma *protocol.MockAPIInterface) {
174
ma.EXPECT().GetIDToken(gomock.Any()).MinTimes(1).Return(nil)
175
ma.EXPECT().GetWorkspace(gomock.Any(), workspaceID).MinTimes(1).Return(
176
&protocol.WorkspaceInfo{
177
Workspace: &protocol.Workspace{
178
ContextURL: "https://github.com/gitpod-io/gitpod",
179
Context: &protocol.WorkspaceContext{
180
NormalizedContextURL: "https://github.com/gitpod-io/gitpod",
181
},
182
},
183
},
184
nil,
185
)
186
ma.EXPECT().GetLoggedInUser(gomock.Any()).Return(
187
nil,
188
&jsonrpc2.Error{Code: 401, Message: "User is not authenticated. Please login."},
189
)
190
},
191
Request: &v1.GetIDTokenRequest{
192
WorkspaceId: workspaceID,
193
Audience: []string{"some.audience.com"},
194
},
195
Expectation: Expectation{
196
Error: connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("User is not authenticated. Please login.")).Error(),
197
},
198
},
199
{
200
Name: "no audience",
201
TokenSource: func(t *testing.T) IDTokenSource {
202
return functionIDTokenSource(func(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error) {
203
return "foobar", nil
204
})
205
},
206
Request: &v1.GetIDTokenRequest{
207
WorkspaceId: workspaceID,
208
},
209
Expectation: Expectation{
210
Error: connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Must have at least one audience entry")).Error(),
211
},
212
},
213
{
214
Name: "include scope",
215
TokenSource: func(t *testing.T) IDTokenSource {
216
return functionIDTokenSource(func(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error) {
217
require.Equal(t, "[email protected]", userInfo.GetEmail())
218
require.True(t, userInfo.IsEmailVerified())
219
require.Equal(t, "foo", userInfo.GetClaim("scope"))
220
221
return "foobar", nil
222
})
223
},
224
ServerSetup: func(ma *protocol.MockAPIInterface) {
225
ma.EXPECT().GetIDToken(gomock.Any()).MinTimes(1).Return(nil)
226
ma.EXPECT().GetWorkspace(gomock.Any(), workspaceID).MinTimes(1).Return(
227
&protocol.WorkspaceInfo{
228
Workspace: &protocol.Workspace{
229
ContextURL: "https://github.com/gitpod-io/gitpod",
230
Context: &protocol.WorkspaceContext{
231
Repository: &protocol.Repository{
232
CloneURL: "https://github.com/gitpod-io/gitpod.git",
233
},
234
NormalizedContextURL: "https://github.com/gitpod-io/gitpod",
235
},
236
},
237
},
238
nil,
239
)
240
ma.EXPECT().GetLoggedInUser(gomock.Any()).Return(
241
&protocol.User{
242
Name: "foobar",
243
Identities: []*protocol.Identity{
244
nil,
245
{Deleted: true, PrimaryEmail: "[email protected]"},
246
{Deleted: false, PrimaryEmail: "[email protected]", LastSigninTime: "2021-01-01T00:00:00Z"},
247
},
248
OrganizationId: "test",
249
},
250
nil,
251
)
252
},
253
Request: &v1.GetIDTokenRequest{
254
WorkspaceId: workspaceID,
255
Audience: []string{"some.audience.com"},
256
Scope: "foo",
257
},
258
Expectation: Expectation{
259
Response: &v1.GetIDTokenResponse{
260
Token: "foobar",
261
},
262
},
263
},
264
{
265
Name: "token source error",
266
TokenSource: func(t *testing.T) IDTokenSource {
267
return functionIDTokenSource(func(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error) {
268
return "", fmt.Errorf("cannot produce token")
269
})
270
},
271
ServerSetup: func(ma *protocol.MockAPIInterface) {
272
ma.EXPECT().GetIDToken(gomock.Any()).MinTimes(1).Return(nil)
273
ma.EXPECT().GetWorkspace(gomock.Any(), workspaceID).MinTimes(1).Return(
274
&protocol.WorkspaceInfo{
275
Workspace: &protocol.Workspace{
276
ContextURL: "https://github.com/gitpod-io/gitpod",
277
Context: &protocol.WorkspaceContext{
278
NormalizedContextURL: "https://github.com/gitpod-io/gitpod",
279
},
280
},
281
},
282
nil,
283
)
284
ma.EXPECT().GetLoggedInUser(gomock.Any()).Return(
285
&protocol.User{
286
Name: "foobar",
287
},
288
nil,
289
)
290
},
291
Request: &v1.GetIDTokenRequest{
292
WorkspaceId: workspaceID,
293
Audience: []string{"some.audience.com"},
294
},
295
Expectation: Expectation{
296
Error: connect.NewError(connect.CodeInternal, fmt.Errorf("cannot produce token")).Error(),
297
},
298
},
299
}
300
301
for _, test := range tests {
302
t.Run(test.Name, func(t *testing.T) {
303
ctrl := gomock.NewController(t)
304
t.Cleanup(ctrl.Finish)
305
serverMock := protocol.NewMockAPIInterface(ctrl)
306
if test.ServerSetup != nil {
307
test.ServerSetup(serverMock)
308
}
309
310
keyset := jwstest.GenerateKeySet(t)
311
rsa256, err := jws.NewRSA256(keyset)
312
require.NoError(t, err)
313
314
svc := NewIdentityProviderService(&FakeServerConnPool{api: serverMock}, test.TokenSource(t), &experimentstest.Client{
315
StringMatcher: func(ctx context.Context, experimentName, defaultValue string, attributes experiments.Attributes) string {
316
return ""
317
},
318
})
319
_, handler := v1connect.NewIdentityProviderServiceHandler(svc, connect.WithInterceptors(auth.NewServerInterceptor(config.SessionConfig{
320
Issuer: "unitetest.com",
321
Cookie: config.CookieConfig{
322
Name: "cookie_jwt",
323
},
324
}, rsa256)))
325
srv := httptest.NewServer(handler)
326
t.Cleanup(srv.Close)
327
328
client := v1connect.NewIdentityProviderServiceClient(http.DefaultClient, srv.URL, connect.WithInterceptors(
329
auth.NewClientInterceptor("auth-token"),
330
))
331
332
resp, err := client.GetIDToken(context.Background(), &connect.Request[v1.GetIDTokenRequest]{
333
Msg: test.Request,
334
})
335
var act Expectation
336
if err != nil {
337
act.Error = err.Error()
338
} else {
339
act.Response = resp.Msg
340
}
341
342
if diff := cmp.Diff(test.Expectation, act, cmpopts.IgnoreUnexported(v1.GetIDTokenResponse{})); diff != "" {
343
t.Errorf("GetIDToken() mismatch (-want +got):\n%s", diff)
344
}
345
})
346
}
347
}
348
349
type functionIDTokenSource func(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error)
350
351
func (f functionIDTokenSource) IDToken(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error) {
352
return f(ctx, org, audience, userInfo)
353
}
354
355
func TestGetOIDCSubject(t *testing.T) {
356
normalizedContextUrl := "https://github.com/gitpod-io/gitpod"
357
defaultWorkspace := &protocol.Workspace{
358
ContextURL: "SOME_ENV=test/" + normalizedContextUrl,
359
Context: &protocol.WorkspaceContext{
360
NormalizedContextURL: normalizedContextUrl,
361
}}
362
tests := []struct {
363
Name string
364
Keys string
365
Claims map[string]interface{}
366
Subject string
367
Workspace *protocol.Workspace
368
}{
369
{
370
Name: "happy path",
371
Keys: "",
372
Claims: map[string]interface{}{},
373
Subject: normalizedContextUrl,
374
Workspace: defaultWorkspace,
375
},
376
{
377
Name: "happy path 2",
378
Keys: "undefined",
379
Claims: map[string]interface{}{},
380
Subject: normalizedContextUrl,
381
Workspace: defaultWorkspace,
382
},
383
{
384
Name: "with custom keys",
385
Keys: "key1,key3,key2",
386
Claims: map[string]interface{}{"key1": 1, "key2": "hello"},
387
Subject: "key1:1:key3::key2:hello",
388
Workspace: defaultWorkspace,
389
},
390
{
391
Name: "with custom keys",
392
Keys: "key1,key3,key2",
393
Claims: map[string]interface{}{"key1": 1, "key3": errors.New("test")},
394
Subject: "key1:1:key3:test:key2:",
395
Workspace: defaultWorkspace,
396
},
397
{
398
Name: "happy path with strange prefix",
399
Keys: "",
400
Claims: map[string]interface{}{},
401
Subject: normalizedContextUrl,
402
Workspace: &protocol.Workspace{ContextURL: "referrer:jetbrains-gateway:intellij/" + normalizedContextUrl, Context: &protocol.WorkspaceContext{
403
NormalizedContextURL: normalizedContextUrl,
404
}},
405
},
406
{
407
Name: "happy path without NormalizedContextURL",
408
Keys: "",
409
Claims: map[string]interface{}{},
410
Subject: "no-context",
411
Workspace: &protocol.Workspace{ContextURL: "referrer:jetbrains-gateway:intellij/" + normalizedContextUrl, Context: &protocol.WorkspaceContext{
412
NormalizedContextURL: "",
413
}},
414
},
415
}
416
417
for _, test := range tests {
418
t.Run(test.Name, func(t *testing.T) {
419
svc := NewIdentityProviderService(nil, nil, &experimentstest.Client{
420
StringMatcher: func(ctx context.Context, experimentName string, defaultValue string, attributes experiments.Attributes) string {
421
return test.Keys
422
},
423
})
424
userinfo := oidc.NewUserInfo()
425
for k, v := range test.Claims {
426
userinfo.AppendClaims(k, v)
427
}
428
act := svc.getOIDCSubject(context.Background(), userinfo, &protocol.User{}, &protocol.WorkspaceInfo{
429
Workspace: test.Workspace,
430
})
431
if diff := cmp.Diff(test.Subject, act); diff != "" {
432
t.Errorf("getOIDCSubject() mismatch (-want +got):\n%s", diff)
433
}
434
})
435
}
436
}
437
438