Path: blob/main/components/public-api-server/pkg/apiv1/identityprovider.go
2499 views
// Copyright (c) 2023 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"fmt"9"strings"1011connect "github.com/bufbuild/connect-go"12"github.com/gitpod-io/gitpod/common-go/experiments"13"github.com/gitpod-io/gitpod/common-go/log"14v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"15"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"16protocol "github.com/gitpod-io/gitpod/gitpod-protocol"17"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"18"github.com/zitadel/oidc/pkg/oidc"19)2021type IDTokenSource interface {22IDToken(ctx context.Context, org string, audience []string, userInfo oidc.UserInfo) (string, error)23}2425func NewIdentityProviderService(serverConnPool proxy.ServerConnectionPool, source IDTokenSource, expClient experiments.Client) *IdentityProviderService {26return &IdentityProviderService{27connectionPool: serverConnPool,28idTokenSource: source,29expClient: expClient,30}31}3233type IdentityProviderService struct {34connectionPool proxy.ServerConnectionPool35idTokenSource IDTokenSource36expClient experiments.Client3738v1connect.UnimplementedWorkspacesServiceHandler39}4041var _ v1connect.IdentityProviderServiceHandler = ((*IdentityProviderService)(nil))4243// GetIDToken implements v1connect.IDPServiceHandler44func (srv *IdentityProviderService) GetIDToken(ctx context.Context, req *connect.Request[v1.GetIDTokenRequest]) (*connect.Response[v1.GetIDTokenResponse], error) {45workspaceID, err := validateWorkspaceID(ctx, req.Msg.GetWorkspaceId())46if err != nil {47return nil, err48}4950if len(req.Msg.Audience) < 1 {51return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Must have at least one audience entry"))52}5354conn, err := getConnection(ctx, srv.connectionPool)55if err != nil {56return nil, err57}5859// We use GetIDToken as standin for the IDP operation authorisation until we have a better way of handling this60err = conn.GetIDToken(ctx)61if err != nil {62return nil, err63}6465workspace, err := conn.GetWorkspace(ctx, workspaceID)66if err != nil {67log.Extract(ctx).WithError(err).Error("Failed to get workspace.")68return nil, proxy.ConvertError(err)69}7071user, err := conn.GetLoggedInUser(ctx)72if err != nil {73log.Extract(ctx).WithError(err).Error("Failed to get calling user.")74return nil, proxy.ConvertError(err)75}7677if workspace.Workspace == nil {78log.Extract(ctx).WithError(err).Error("Server did not return a workspace.")79return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("workspace not found"))80}8182userInfo := oidc.NewUserInfo()83userInfo.SetName(user.Name)84userInfo.AppendClaims("user_id", user.ID)85userInfo.AppendClaims("org_id", workspace.Workspace.OrganizationId)86userInfo.AppendClaims("context", getContext(workspace))87userInfo.AppendClaims("workspace_id", workspaceID)8889if req.Msg.GetScope() != "" {90userInfo.AppendClaims("scope", req.Msg.GetScope())91}9293if workspace.Workspace.Context != nil && workspace.Workspace.Context.Repository != nil && workspace.Workspace.Context.Repository.CloneURL != "" {94userInfo.AppendClaims("repository", workspace.Workspace.Context.Repository.CloneURL)95}9697var email string98var emailVerified bool99if user.OrganizationId != "" {100emailVerified = true101email = user.GetSSOEmail()102if email == "" {103return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("SSO email is empty"))104}105} else {106emailVerified = false107email = user.GetRandomEmail()108}109if email != "" {110userInfo.SetEmail(email, emailVerified)111userInfo.AppendClaims("email", email)112}113114userInfo.SetSubject(srv.getOIDCSubject(ctx, userInfo, user, workspace))115116token, err := srv.idTokenSource.IDToken(ctx, "gitpod", req.Msg.Audience, userInfo)117if err != nil {118log.Extract(ctx).WithError(err).Error("Failed to produce ID token.")119return nil, proxy.ConvertError(err)120}121return &connect.Response[v1.GetIDTokenResponse]{122Msg: &v1.GetIDTokenResponse{123Token: token,124},125}, nil126}127128func (srv *IdentityProviderService) getOIDCSubject(ctx context.Context, userInfo oidc.UserInfoSetter, user *protocol.User, workspace *protocol.WorkspaceInfo) string {129claimKeys := experiments.GetIdPClaimKeys(ctx, srv.expClient, experiments.Attributes{130UserID: user.ID,131TeamID: workspace.Workspace.OrganizationId,132})133subject := getContext(workspace)134if len(claimKeys) != 0 {135subArr := []string{}136for _, key := range claimKeys {137value := userInfo.GetClaim(key)138if value == nil {139value = ""140}141subArr = append(subArr, fmt.Sprintf("%s:%+v", key, value))142}143subject = strings.Join(subArr, ":")144}145return subject146}147148func getContext(workspace *protocol.WorkspaceInfo) string {149context := "no-context"150if workspace.Workspace.Context != nil && workspace.Workspace.Context.NormalizedContextURL != "" {151// using Workspace.Context.NormalizedContextURL to not include prefixes (like "referrer:jetbrains-gateway", or other prefix contexts)152context = workspace.Workspace.Context.NormalizedContextURL153}154return context155}156157158