Path: blob/main/components/public-api-server/pkg/apiv1/team.go
2499 views
// Copyright (c) 2022 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"sync"1011connect "github.com/bufbuild/connect-go"12"github.com/gitpod-io/gitpod/common-go/log"13v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"14"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"15protocol "github.com/gitpod-io/gitpod/gitpod-protocol"16"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"17)1819func NewTeamsService(pool proxy.ServerConnectionPool) *TeamService {20return &TeamService{21connectionPool: pool,22}23}2425var _ v1connect.TeamsServiceHandler = (*TeamService)(nil)2627type TeamService struct {28connectionPool proxy.ServerConnectionPool2930v1connect.UnimplementedTeamsServiceHandler31}3233func (s *TeamService) CreateTeam(ctx context.Context, req *connect.Request[v1.CreateTeamRequest]) (*connect.Response[v1.CreateTeamResponse], error) {34if req.Msg.GetName() == "" {35return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Name is a required argument when creating a team."))36}3738conn, err := getConnection(ctx, s.connectionPool)39if err != nil {40return nil, err41}4243created, err := conn.CreateTeam(ctx, req.Msg.GetName())44if err != nil {45log.Extract(ctx).Error("Failed to create team.")46return nil, proxy.ConvertError(err)47}4849team, err := s.toTeamAPIResponse(ctx, conn, created)50if err != nil {51log.Extract(ctx).WithError(err).Error("Failed to populate team with details.")52return nil, err53}5455return connect.NewResponse(&v1.CreateTeamResponse{56Team: team,57}), nil58}5960func (s *TeamService) GetTeam(ctx context.Context, req *connect.Request[v1.GetTeamRequest]) (*connect.Response[v1.GetTeamResponse], error) {61teamID, err := validateTeamID(ctx, req.Msg.GetTeamId())62if err != nil {63return nil, err64}6566conn, err := getConnection(ctx, s.connectionPool)67if err != nil {68return nil, err69}7071team, err := conn.GetTeam(ctx, teamID.String())72if err != nil {73return nil, proxy.ConvertError(err)74}7576response, err := s.toTeamAPIResponse(ctx, conn, team)77if err != nil {78return nil, err79}8081return connect.NewResponse(&v1.GetTeamResponse{82Team: response,83}), nil84}8586func (s *TeamService) ListTeams(ctx context.Context, req *connect.Request[v1.ListTeamsRequest]) (*connect.Response[v1.ListTeamsResponse], error) {87conn, err := getConnection(ctx, s.connectionPool)88if err != nil {89return nil, err90}9192teams, err := conn.GetTeams(ctx)93if err != nil {94log.Extract(ctx).WithError(err).Error("Failed to list teams from server.")95return nil, proxy.ConvertError(err)96}9798type result struct {99team *v1.Team100err error101}102103wg := sync.WaitGroup{}104resultsChan := make(chan result, len(teams))105for _, t := range teams {106wg.Add(1)107go func(t *protocol.Team) {108team, err := s.toTeamAPIResponse(ctx, conn, t)109resultsChan <- result{110team: team,111err: err,112}113defer wg.Done()114}(t)115}116117// Block until we've fetched all teams118wg.Wait()119close(resultsChan)120121// We want to maintain the order of results that we got from server122// So we convert our concurrent results to a map, so we can index into it123resultMap := map[string]*v1.Team{}124for res := range resultsChan {125if res.err != nil {126log.Extract(ctx).WithError(err).Error("Failed to populate team with details.")127return nil, res.err128}129130resultMap[res.team.GetId()] = res.team131}132133// Map the original order of teams against the populated results134var response []*v1.Team135for _, t := range teams {136response = append(response, resultMap[t.ID])137}138139return connect.NewResponse(&v1.ListTeamsResponse{140Teams: response,141}), nil142}143144func (s *TeamService) DeleteTeam(ctx context.Context, req *connect.Request[v1.DeleteTeamRequest]) (*connect.Response[v1.DeleteTeamResponse], error) {145teamID, err := validateTeamID(ctx, req.Msg.GetTeamId())146if err != nil {147return nil, err148}149150conn, err := getConnection(ctx, s.connectionPool)151if err != nil {152return nil, err153}154155err = conn.DeleteTeam(ctx, teamID.String())156if err != nil {157return nil, proxy.ConvertError(err)158}159160return connect.NewResponse(&v1.DeleteTeamResponse{}), nil161}162163func (s *TeamService) GetTeamInvitation(ctx context.Context, req *connect.Request[v1.GetTeamInvitationRequest]) (*connect.Response[v1.GetTeamInvitationResponse], error) {164teamID, err := validateTeamID(ctx, req.Msg.GetTeamId())165if err != nil {166return nil, err167}168169conn, err := getConnection(ctx, s.connectionPool)170if err != nil {171return nil, err172}173174invite, err := conn.GetGenericInvite(ctx, teamID.String())175if err != nil {176return nil, proxy.ConvertError(err)177}178179return connect.NewResponse(&v1.GetTeamInvitationResponse{180TeamInvitation: teamInviteToAPIResponse(invite),181}), nil182}183184func (s *TeamService) JoinTeam(ctx context.Context, req *connect.Request[v1.JoinTeamRequest]) (*connect.Response[v1.JoinTeamResponse], error) {185if req.Msg.GetInvitationId() == "" {186return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invitation id is a required argument to join a team"))187}188189conn, err := getConnection(ctx, s.connectionPool)190if err != nil {191return nil, err192}193194team, err := conn.JoinTeam(ctx, req.Msg.GetInvitationId())195if err != nil {196return nil, proxy.ConvertError(err)197}198199response, err := s.toTeamAPIResponse(ctx, conn, team)200if err != nil {201log.Extract(ctx).WithError(err).Error("Failed to populate team with details.")202return nil, err203}204205return connect.NewResponse(&v1.JoinTeamResponse{206Team: response,207}), nil208}209210func (s *TeamService) ResetTeamInvitation(ctx context.Context, req *connect.Request[v1.ResetTeamInvitationRequest]) (*connect.Response[v1.ResetTeamInvitationResponse], error) {211teamID, err := validateTeamID(ctx, req.Msg.GetTeamId())212if err != nil {213return nil, err214}215216conn, err := getConnection(ctx, s.connectionPool)217if err != nil {218return nil, err219}220221invite, err := conn.ResetGenericInvite(ctx, teamID.String())222if err != nil {223return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("Failed to reset team invitation"))224}225226return connect.NewResponse(&v1.ResetTeamInvitationResponse{227TeamInvitation: teamInviteToAPIResponse(invite),228}), nil229}230231func (s *TeamService) ListTeamMembers(ctx context.Context, req *connect.Request[v1.ListTeamMembersRequest]) (*connect.Response[v1.ListTeamMembersResponse], error) {232teamID, err := validateTeamID(ctx, req.Msg.GetTeamId())233if err != nil {234return nil, err235}236237conn, err := getConnection(ctx, s.connectionPool)238if err != nil {239return nil, err240}241242members, err := conn.GetTeamMembers(ctx, teamID.String())243if err != nil {244return nil, proxy.ConvertError(err)245}246247return connect.NewResponse(&v1.ListTeamMembersResponse{248Members: teamMembersToAPIResponse(members),249}), nil250}251252func (s *TeamService) UpdateTeamMember(ctx context.Context, req *connect.Request[v1.UpdateTeamMemberRequest]) (*connect.Response[v1.UpdateTeamMemberResponse], error) {253teamID := req.Msg.GetTeamId()254if teamID == "" {255return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Team ID is a required parameter to update team member."))256}257258userID := req.Msg.GetTeamMember().GetUserId()259if userID == "" {260return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("TeamMember.UserID is a required parameter to update team member."))261}262263var role protocol.TeamMemberRole264switch req.Msg.GetTeamMember().GetRole() {265case v1.TeamRole_TEAM_ROLE_MEMBER:266role = protocol.TeamMember_Member267case v1.TeamRole_TEAM_ROLE_OWNER:268role = protocol.TeamMember_Owner269default:270return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Unknown TeamMember.Role specified."))271}272273conn, err := getConnection(ctx, s.connectionPool)274if err != nil {275return nil, err276}277278err = conn.SetTeamMemberRole(ctx, teamID, userID, role)279if err != nil {280return nil, proxy.ConvertError(err)281}282283return connect.NewResponse(&v1.UpdateTeamMemberResponse{284TeamMember: req.Msg.GetTeamMember(),285}), nil286287}288289func (s *TeamService) DeleteTeamMember(ctx context.Context, req *connect.Request[v1.DeleteTeamMemberRequest]) (*connect.Response[v1.DeleteTeamMemberResponse], error) {290teamID := req.Msg.GetTeamId()291if teamID == "" {292return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Team ID is a required parameter to delete team member."))293}294295memberID := req.Msg.GetTeamMemberId()296if memberID == "" {297return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Team Member ID is a required parameter to delete team member."))298}299300conn, err := getConnection(ctx, s.connectionPool)301if err != nil {302return nil, err303}304305err = conn.RemoveTeamMember(ctx, teamID, memberID)306if err != nil {307return nil, proxy.ConvertError(err)308}309310return connect.NewResponse(&v1.DeleteTeamMemberResponse{}), nil311}312313func (s *TeamService) toTeamAPIResponse(ctx context.Context, conn protocol.APIInterface, team *protocol.Team) (*v1.Team, error) {314logger := log.Extract(ctx).WithFields(log.OrganizationID(team.ID))315members, err := conn.GetTeamMembers(ctx, team.ID)316if err != nil {317logger.WithError(err).Error("Failed to get team members.")318return nil, proxy.ConvertError(err)319}320321invite, err := conn.GetGenericInvite(ctx, team.ID)322323if err != nil {324convertedError := proxy.ConvertError(err)325// code not found is expected if the organization is SSO-enabled326if connectError, ok := convertedError.(*connect.Error); !ok || !(connectError.Code() == connect.CodeNotFound || connectError.Code() == connect.CodePermissionDenied) {327logger.WithError(err).Error("Failed to get generic invite")328return nil, convertedError329}330}331332return teamToAPIResponse(team, members, invite), nil333}334335func teamToAPIResponse(team *protocol.Team, members []*protocol.TeamMemberInfo, invite *protocol.TeamMembershipInvite) *v1.Team {336return &v1.Team{337Id: team.ID,338Name: team.Name,339Members: teamMembersToAPIResponse(members),340TeamInvitation: teamInviteToAPIResponse(invite),341}342}343344func teamMembersToAPIResponse(members []*protocol.TeamMemberInfo) []*v1.TeamMember {345var result []*v1.TeamMember346347for _, m := range members {348result = append(result, &v1.TeamMember{349UserId: m.UserId,350Role: teamRoleToAPIResponse(m.Role),351MemberSince: parseGitpodTimeStampOrDefault(m.MemberSince),352AvatarUrl: m.AvatarUrl,353FullName: m.FullName,354PrimaryEmail: m.PrimaryEmail,355OwnedByOrganization: m.OwnedByOrganization,356})357}358359return result360}361362func teamRoleToAPIResponse(role protocol.TeamMemberRole) v1.TeamRole {363switch role {364case protocol.TeamMember_Owner:365return v1.TeamRole_TEAM_ROLE_OWNER366case protocol.TeamMember_Member:367return v1.TeamRole_TEAM_ROLE_MEMBER368default:369return v1.TeamRole_TEAM_ROLE_UNSPECIFIED370}371}372373func teamInviteToAPIResponse(invite *protocol.TeamMembershipInvite) *v1.TeamInvitation {374if invite == nil {375return nil376}377return &v1.TeamInvitation{378Id: invite.ID,379}380}381382383