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.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
"fmt"
10
"strings"
11
12
connect "github.com/bufbuild/connect-go"
13
"github.com/gitpod-io/gitpod/common-go/experiments"
14
"github.com/gitpod-io/gitpod/common-go/log"
15
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
16
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
17
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
18
"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"
19
"github.com/zitadel/oidc/pkg/oidc"
20
)
21
22
type IDTokenSource interface {
23
IDToken(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error)
24
}
25
26
func NewIdentityProviderService(serverConnPool proxy.ServerConnectionPool, source IDTokenSource, expClient experiments.Client) *IdentityProviderService {
27
return &IdentityProviderService{
28
connectionPool: serverConnPool,
29
idTokenSource: source,
30
expClient: expClient,
31
}
32
}
33
34
type IdentityProviderService struct {
35
connectionPool proxy.ServerConnectionPool
36
idTokenSource IDTokenSource
37
expClient experiments.Client
38
39
v1connect.UnimplementedWorkspacesServiceHandler
40
}
41
42
var _ v1connect.IdentityProviderServiceHandler = ((*IdentityProviderService)(nil))
43
44
// GetIDToken implements v1connect.IDPServiceHandler
45
func (srv *IdentityProviderService) GetIDToken(ctx context.Context, req *connect.Request[v1.GetIDTokenRequest]) (*connect.Response[v1.GetIDTokenResponse], error) {
46
workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())
47
if err != nil {
48
return nil, err
49
}
50
51
if len(req.Msg.Audience) < 1 {
52
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Must have at least one audience entry"))
53
}
54
55
conn, err := getConnection(ctx, srv.connectionPool)
56
if err != nil {
57
return nil, err
58
}
59
60
// We use GetIDToken as standin for the IDP operation authorisation until we have a better way of handling this
61
err = conn.GetIDToken(ctx)
62
if err != nil {
63
return nil, err
64
}
65
66
workspace, err := conn.GetWorkspace(ctx, workspaceID)
67
if err != nil {
68
log.Extract(ctx).WithError(err).Error("Failed to get workspace.")
69
return nil, proxy.ConvertError(err)
70
}
71
72
user, err := conn.GetLoggedInUser(ctx)
73
if err != nil {
74
log.Extract(ctx).WithError(err).Error("Failed to get calling user.")
75
return nil, proxy.ConvertError(err)
76
}
77
78
if workspace.Workspace == nil {
79
log.Extract(ctx).WithError(err).Error("Server did not return a workspace.")
80
return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("workspace not found"))
81
}
82
83
userInfo := oidc.NewUserInfo()
84
userInfo.SetName(user.Name)
85
userInfo.AppendClaims("user_id", user.ID)
86
userInfo.AppendClaims("org_id", workspace.Workspace.OrganizationId)
87
userInfo.AppendClaims("context", getContext(workspace))
88
userInfo.AppendClaims("workspace_id", workspaceID)
89
90
if req.Msg.GetScope() != "" {
91
userInfo.AppendClaims("scope", req.Msg.GetScope())
92
}
93
94
if workspace.Workspace.Context != nil && workspace.Workspace.Context.Repository != nil && workspace.Workspace.Context.Repository.CloneURL != "" {
95
userInfo.AppendClaims("repository", workspace.Workspace.Context.Repository.CloneURL)
96
}
97
98
var email string
99
var emailVerified bool
100
if user.OrganizationId != "" {
101
emailVerified = true
102
email = user.GetSSOEmail()
103
if email == "" {
104
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("SSO email is empty"))
105
}
106
} else {
107
emailVerified = false
108
email = user.GetRandomEmail()
109
}
110
if email != "" {
111
userInfo.SetEmail(email, emailVerified)
112
userInfo.AppendClaims("email", email)
113
}
114
115
userInfo.SetSubject(srv.getOIDCSubject(ctx, userInfo, user, workspace))
116
117
token, err := srv.idTokenSource.IDToken(ctx, "gitpod", req.Msg.Audience, userInfo)
118
if err != nil {
119
log.Extract(ctx).WithError(err).Error("Failed to produce ID token.")
120
return nil, proxy.ConvertError(err)
121
}
122
return &connect.Response[v1.GetIDTokenResponse]{
123
Msg: &v1.GetIDTokenResponse{
124
Token: token,
125
},
126
}, nil
127
}
128
129
func (srv *IdentityProviderService) getOIDCSubject(ctx context.Context, userInfo oidc.UserInfoSetter, user *protocol.User, workspace *protocol.WorkspaceInfo) string {
130
claimKeys := experiments.GetIdPClaimKeys(ctx, srv.expClient, experiments.Attributes{
131
UserID: user.ID,
132
TeamID: workspace.Workspace.OrganizationId,
133
})
134
subject := getContext(workspace)
135
if len(claimKeys) != 0 {
136
subArr := []string{}
137
for _, key := range claimKeys {
138
value := userInfo.GetClaim(key)
139
if value == nil {
140
value = ""
141
}
142
subArr = append(subArr, fmt.Sprintf("%s:%+v", key, value))
143
}
144
subject = strings.Join(subArr, ":")
145
}
146
return subject
147
}
148
149
func getContext(workspace *protocol.WorkspaceInfo) string {
150
context := "no-context"
151
if workspace.Workspace.Context != nil && workspace.Workspace.Context.NormalizedContextURL != "" {
152
// using Workspace.Context.NormalizedContextURL to not include prefixes (like "referrer:jetbrains-gateway", or other prefix contexts)
153
context = workspace.Workspace.Context.NormalizedContextURL
154
}
155
return context
156
}
157
158