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/team.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
"fmt"
10
"sync"
11
12
connect "github.com/bufbuild/connect-go"
13
"github.com/gitpod-io/gitpod/common-go/log"
14
v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1"
15
"github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect"
16
protocol "github.com/gitpod-io/gitpod/gitpod-protocol"
17
"github.com/gitpod-io/gitpod/public-api-server/pkg/proxy"
18
)
19
20
func NewTeamsService(pool proxy.ServerConnectionPool) *TeamService {
21
return &TeamService{
22
connectionPool: pool,
23
}
24
}
25
26
var _ v1connect.TeamsServiceHandler = (*TeamService)(nil)
27
28
type TeamService struct {
29
connectionPool proxy.ServerConnectionPool
30
31
v1connect.UnimplementedTeamsServiceHandler
32
}
33
34
func (s *TeamService) CreateTeam(ctx context.Context, req *connect.Request[v1.CreateTeamRequest]) (*connect.Response[v1.CreateTeamResponse], error) {
35
if req.Msg.GetName() == "" {
36
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Name is a required argument when creating a team."))
37
}
38
39
conn, err := getConnection(ctx, s.connectionPool)
40
if err != nil {
41
return nil, err
42
}
43
44
created, err := conn.CreateTeam(ctx, req.Msg.GetName())
45
if err != nil {
46
log.Extract(ctx).Error("Failed to create team.")
47
return nil, proxy.ConvertError(err)
48
}
49
50
team, err := s.toTeamAPIResponse(ctx, conn, created)
51
if err != nil {
52
log.Extract(ctx).WithError(err).Error("Failed to populate team with details.")
53
return nil, err
54
}
55
56
return connect.NewResponse(&v1.CreateTeamResponse{
57
Team: team,
58
}), nil
59
}
60
61
func (s *TeamService) GetTeam(ctx context.Context, req *connect.Request[v1.GetTeamRequest]) (*connect.Response[v1.GetTeamResponse], error) {
62
teamID, err := validateTeamID(ctx, req.Msg.GetTeamId())
63
if err != nil {
64
return nil, err
65
}
66
67
conn, err := getConnection(ctx, s.connectionPool)
68
if err != nil {
69
return nil, err
70
}
71
72
team, err := conn.GetTeam(ctx, teamID.String())
73
if err != nil {
74
return nil, proxy.ConvertError(err)
75
}
76
77
response, err := s.toTeamAPIResponse(ctx, conn, team)
78
if err != nil {
79
return nil, err
80
}
81
82
return connect.NewResponse(&v1.GetTeamResponse{
83
Team: response,
84
}), nil
85
}
86
87
func (s *TeamService) ListTeams(ctx context.Context, req *connect.Request[v1.ListTeamsRequest]) (*connect.Response[v1.ListTeamsResponse], error) {
88
conn, err := getConnection(ctx, s.connectionPool)
89
if err != nil {
90
return nil, err
91
}
92
93
teams, err := conn.GetTeams(ctx)
94
if err != nil {
95
log.Extract(ctx).WithError(err).Error("Failed to list teams from server.")
96
return nil, proxy.ConvertError(err)
97
}
98
99
type result struct {
100
team *v1.Team
101
err error
102
}
103
104
wg := sync.WaitGroup{}
105
resultsChan := make(chan result, len(teams))
106
for _, t := range teams {
107
wg.Add(1)
108
go func(t *protocol.Team) {
109
team, err := s.toTeamAPIResponse(ctx, conn, t)
110
resultsChan <- result{
111
team: team,
112
err: err,
113
}
114
defer wg.Done()
115
}(t)
116
}
117
118
// Block until we've fetched all teams
119
wg.Wait()
120
close(resultsChan)
121
122
// We want to maintain the order of results that we got from server
123
// So we convert our concurrent results to a map, so we can index into it
124
resultMap := map[string]*v1.Team{}
125
for res := range resultsChan {
126
if res.err != nil {
127
log.Extract(ctx).WithError(err).Error("Failed to populate team with details.")
128
return nil, res.err
129
}
130
131
resultMap[res.team.GetId()] = res.team
132
}
133
134
// Map the original order of teams against the populated results
135
var response []*v1.Team
136
for _, t := range teams {
137
response = append(response, resultMap[t.ID])
138
}
139
140
return connect.NewResponse(&v1.ListTeamsResponse{
141
Teams: response,
142
}), nil
143
}
144
145
func (s *TeamService) DeleteTeam(ctx context.Context, req *connect.Request[v1.DeleteTeamRequest]) (*connect.Response[v1.DeleteTeamResponse], error) {
146
teamID, err := validateTeamID(ctx, req.Msg.GetTeamId())
147
if err != nil {
148
return nil, err
149
}
150
151
conn, err := getConnection(ctx, s.connectionPool)
152
if err != nil {
153
return nil, err
154
}
155
156
err = conn.DeleteTeam(ctx, teamID.String())
157
if err != nil {
158
return nil, proxy.ConvertError(err)
159
}
160
161
return connect.NewResponse(&v1.DeleteTeamResponse{}), nil
162
}
163
164
func (s *TeamService) GetTeamInvitation(ctx context.Context, req *connect.Request[v1.GetTeamInvitationRequest]) (*connect.Response[v1.GetTeamInvitationResponse], error) {
165
teamID, err := validateTeamID(ctx, req.Msg.GetTeamId())
166
if err != nil {
167
return nil, err
168
}
169
170
conn, err := getConnection(ctx, s.connectionPool)
171
if err != nil {
172
return nil, err
173
}
174
175
invite, err := conn.GetGenericInvite(ctx, teamID.String())
176
if err != nil {
177
return nil, proxy.ConvertError(err)
178
}
179
180
return connect.NewResponse(&v1.GetTeamInvitationResponse{
181
TeamInvitation: teamInviteToAPIResponse(invite),
182
}), nil
183
}
184
185
func (s *TeamService) JoinTeam(ctx context.Context, req *connect.Request[v1.JoinTeamRequest]) (*connect.Response[v1.JoinTeamResponse], error) {
186
if req.Msg.GetInvitationId() == "" {
187
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("invitation id is a required argument to join a team"))
188
}
189
190
conn, err := getConnection(ctx, s.connectionPool)
191
if err != nil {
192
return nil, err
193
}
194
195
team, err := conn.JoinTeam(ctx, req.Msg.GetInvitationId())
196
if err != nil {
197
return nil, proxy.ConvertError(err)
198
}
199
200
response, err := s.toTeamAPIResponse(ctx, conn, team)
201
if err != nil {
202
log.Extract(ctx).WithError(err).Error("Failed to populate team with details.")
203
return nil, err
204
}
205
206
return connect.NewResponse(&v1.JoinTeamResponse{
207
Team: response,
208
}), nil
209
}
210
211
func (s *TeamService) ResetTeamInvitation(ctx context.Context, req *connect.Request[v1.ResetTeamInvitationRequest]) (*connect.Response[v1.ResetTeamInvitationResponse], error) {
212
teamID, err := validateTeamID(ctx, req.Msg.GetTeamId())
213
if err != nil {
214
return nil, err
215
}
216
217
conn, err := getConnection(ctx, s.connectionPool)
218
if err != nil {
219
return nil, err
220
}
221
222
invite, err := conn.ResetGenericInvite(ctx, teamID.String())
223
if err != nil {
224
return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("Failed to reset team invitation"))
225
}
226
227
return connect.NewResponse(&v1.ResetTeamInvitationResponse{
228
TeamInvitation: teamInviteToAPIResponse(invite),
229
}), nil
230
}
231
232
func (s *TeamService) ListTeamMembers(ctx context.Context, req *connect.Request[v1.ListTeamMembersRequest]) (*connect.Response[v1.ListTeamMembersResponse], error) {
233
teamID, err := validateTeamID(ctx, req.Msg.GetTeamId())
234
if err != nil {
235
return nil, err
236
}
237
238
conn, err := getConnection(ctx, s.connectionPool)
239
if err != nil {
240
return nil, err
241
}
242
243
members, err := conn.GetTeamMembers(ctx, teamID.String())
244
if err != nil {
245
return nil, proxy.ConvertError(err)
246
}
247
248
return connect.NewResponse(&v1.ListTeamMembersResponse{
249
Members: teamMembersToAPIResponse(members),
250
}), nil
251
}
252
253
func (s *TeamService) UpdateTeamMember(ctx context.Context, req *connect.Request[v1.UpdateTeamMemberRequest]) (*connect.Response[v1.UpdateTeamMemberResponse], error) {
254
teamID := req.Msg.GetTeamId()
255
if teamID == "" {
256
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Team ID is a required parameter to update team member."))
257
}
258
259
userID := req.Msg.GetTeamMember().GetUserId()
260
if userID == "" {
261
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("TeamMember.UserID is a required parameter to update team member."))
262
}
263
264
var role protocol.TeamMemberRole
265
switch req.Msg.GetTeamMember().GetRole() {
266
case v1.TeamRole_TEAM_ROLE_MEMBER:
267
role = protocol.TeamMember_Member
268
case v1.TeamRole_TEAM_ROLE_OWNER:
269
role = protocol.TeamMember_Owner
270
default:
271
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Unknown TeamMember.Role specified."))
272
}
273
274
conn, err := getConnection(ctx, s.connectionPool)
275
if err != nil {
276
return nil, err
277
}
278
279
err = conn.SetTeamMemberRole(ctx, teamID, userID, role)
280
if err != nil {
281
return nil, proxy.ConvertError(err)
282
}
283
284
return connect.NewResponse(&v1.UpdateTeamMemberResponse{
285
TeamMember: req.Msg.GetTeamMember(),
286
}), nil
287
288
}
289
290
func (s *TeamService) DeleteTeamMember(ctx context.Context, req *connect.Request[v1.DeleteTeamMemberRequest]) (*connect.Response[v1.DeleteTeamMemberResponse], error) {
291
teamID := req.Msg.GetTeamId()
292
if teamID == "" {
293
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Team ID is a required parameter to delete team member."))
294
}
295
296
memberID := req.Msg.GetTeamMemberId()
297
if memberID == "" {
298
return nil, connect.NewError(connect.CodeInvalidArgument, fmt.Errorf("Team Member ID is a required parameter to delete team member."))
299
}
300
301
conn, err := getConnection(ctx, s.connectionPool)
302
if err != nil {
303
return nil, err
304
}
305
306
err = conn.RemoveTeamMember(ctx, teamID, memberID)
307
if err != nil {
308
return nil, proxy.ConvertError(err)
309
}
310
311
return connect.NewResponse(&v1.DeleteTeamMemberResponse{}), nil
312
}
313
314
func (s *TeamService) toTeamAPIResponse(ctx context.Context, conn protocol.APIInterface, team *protocol.Team) (*v1.Team, error) {
315
logger := log.Extract(ctx).WithFields(log.OrganizationID(team.ID))
316
members, err := conn.GetTeamMembers(ctx, team.ID)
317
if err != nil {
318
logger.WithError(err).Error("Failed to get team members.")
319
return nil, proxy.ConvertError(err)
320
}
321
322
invite, err := conn.GetGenericInvite(ctx, team.ID)
323
324
if err != nil {
325
convertedError := proxy.ConvertError(err)
326
// code not found is expected if the organization is SSO-enabled
327
if connectError, ok := convertedError.(*connect.Error); !ok || !(connectError.Code() == connect.CodeNotFound || connectError.Code() == connect.CodePermissionDenied) {
328
logger.WithError(err).Error("Failed to get generic invite")
329
return nil, convertedError
330
}
331
}
332
333
return teamToAPIResponse(team, members, invite), nil
334
}
335
336
func teamToAPIResponse(team *protocol.Team, members []*protocol.TeamMemberInfo, invite *protocol.TeamMembershipInvite) *v1.Team {
337
return &v1.Team{
338
Id: team.ID,
339
Name: team.Name,
340
Members: teamMembersToAPIResponse(members),
341
TeamInvitation: teamInviteToAPIResponse(invite),
342
}
343
}
344
345
func teamMembersToAPIResponse(members []*protocol.TeamMemberInfo) []*v1.TeamMember {
346
var result []*v1.TeamMember
347
348
for _, m := range members {
349
result = append(result, &v1.TeamMember{
350
UserId: m.UserId,
351
Role: teamRoleToAPIResponse(m.Role),
352
MemberSince: parseGitpodTimeStampOrDefault(m.MemberSince),
353
AvatarUrl: m.AvatarUrl,
354
FullName: m.FullName,
355
PrimaryEmail: m.PrimaryEmail,
356
OwnedByOrganization: m.OwnedByOrganization,
357
})
358
}
359
360
return result
361
}
362
363
func teamRoleToAPIResponse(role protocol.TeamMemberRole) v1.TeamRole {
364
switch role {
365
case protocol.TeamMember_Owner:
366
return v1.TeamRole_TEAM_ROLE_OWNER
367
case protocol.TeamMember_Member:
368
return v1.TeamRole_TEAM_ROLE_MEMBER
369
default:
370
return v1.TeamRole_TEAM_ROLE_UNSPECIFIED
371
}
372
}
373
374
func teamInviteToAPIResponse(invite *protocol.TeamMembershipInvite) *v1.TeamInvitation {
375
if invite == nil {
376
return nil
377
}
378
return &v1.TeamInvitation{
379
Id: invite.ID,
380
}
381
}
382
383