1
+ import json
2
+ from collections import defaultdict
1
3
import logging
2
4
3
5
from urllib .parse import urlparse , parse_qs
9
11
CreateTeamInvitation ,
10
12
CreateTeamMember ,
11
13
CreateTeamMemberParameters ,
14
+ DeleteMemberParameters ,
12
15
Detail ,
13
16
IncidentPermission ,
14
17
Invitation ,
18
21
Team ,
19
22
TeamInvitation ,
20
23
TeamsParameters ,
24
+ UpdateMember ,
25
+ UpdateTeam ,
21
26
UpdateTeamSource ,
22
27
)
23
28
from pygitguardian .models_utils import (
24
29
CursorPaginatedResponse ,
25
30
PaginationParameter ,
26
31
)
27
- from typing import Any
32
+ from typing import Any , Iterable
28
33
29
34
from config import CONFIG
30
35
@@ -51,7 +56,9 @@ def pagination_max_results(
51
56
on all `list` methods
52
57
"""
53
58
54
- pagination_parameters = parameter_cls (** additional_parameters )
59
+ pagination_parameters = parameter_cls (
60
+ per_page = CONFIG .pagination_size , ** additional_parameters
61
+ )
55
62
paginated_response : CursorPaginatedResponse | Detail = method (
56
63
parameters = pagination_parameters
57
64
)
@@ -64,7 +71,9 @@ def pagination_max_results(
64
71
next = paginated_response .next
65
72
66
73
while next and (cursor := get_cursor (next )):
67
- pagination_parameters = parameter_cls (cursor = cursor , ** additional_parameters )
74
+ pagination_parameters = parameter_cls (
75
+ cursor = cursor , per_page = CONFIG .pagination_size , ** additional_parameters
76
+ )
68
77
paginated_response = method (parameters = pagination_parameters )
69
78
70
79
if isinstance (paginated_response , Detail ):
@@ -95,9 +104,10 @@ def list_all_team_members(
95
104
)
96
105
team_members = pagination_max_results (list_team_members )
97
106
for team_member in team_members :
98
- all_team_members [team .name ].append (
99
- (team_member .id , id_to_member [team_member .member_id ])
100
- )
107
+ if team_member .member_id in id_to_member :
108
+ all_team_members [team .name ].append (
109
+ (team_member .id , id_to_member [team_member .member_id ])
110
+ )
101
111
102
112
return all_team_members
103
113
@@ -113,15 +123,31 @@ def list_team_sources(team: Team) -> list[Source]:
113
123
return pagination_max_results (wrapper )
114
124
115
125
116
- def list_all_teams () -> list [Team ]:
126
+ def list_all_teams () -> tuple [ list [Team ], dict [ str , list [ Team ]] ]:
117
127
"""
118
- Get all teams from GitGuardian
128
+ Get syncable teams from GitGuardian and teams by external id
119
129
"""
120
130
121
- return pagination_max_results (
131
+ all_teams : list [ Team ] = pagination_max_results (
122
132
CONFIG .client .list_teams , TeamsParameters , {"is_global" : False }
123
133
)
124
134
135
+ sync_teams = []
136
+ teams_by_external_id : dict [str , Team ] = {}
137
+ for team in all_teams :
138
+ if team .description is None :
139
+ continue
140
+ try :
141
+ metadata = json .loads (team .description )
142
+ external_id = metadata ["id" ]
143
+
144
+ teams_by_external_id [external_id ] = team
145
+ sync_teams .append (team )
146
+ except json .JSONDecodeError :
147
+ pass
148
+
149
+ return sync_teams , teams_by_external_id
150
+
125
151
126
152
def list_all_members () -> list [Member ]:
127
153
"""
@@ -158,15 +184,22 @@ def remove_team_member(team: Team, team_member_id: int):
158
184
team_id = team .id , team_member_id = team_member_id
159
185
)
160
186
if isinstance (response , Detail ):
161
- raise RuntimeError (f"Unable to remove team member: { response .content } " )
187
+ raise RuntimeError (f"Unable to remove team member: { response .detail } " )
188
+
189
+ logger .warning (
190
+ f"Successfully removed member { team_member_id } from { team .name } " ,
191
+ )
192
+
193
+
194
+ def remove_team_invitation (team : Team , invitation_id : int , email : str ):
195
+ response = CONFIG .client .delete_team_invitation (
196
+ team_id = team .id , invitation_id = invitation_id
197
+ )
198
+ if isinstance (response , Detail ):
199
+ raise RuntimeError (f"Unable to remove team invitation: { response .detail } " )
162
200
163
201
logger .warning (
164
- "Successfully removed team member" ,
165
- extra = dict (
166
- team_id = team .id ,
167
- team_name = team .name ,
168
- team_member_id = team_member_id ,
169
- ),
202
+ f"Successfully removed team invitation for { email } from { team .name } " ,
170
203
)
171
204
172
205
@@ -184,9 +217,12 @@ def send_invitation(
184
217
)
185
218
186
219
if isinstance (response , Detail ):
187
- raise RuntimeError (f"Unable to invite member: { response .content } " )
220
+ if response .status_code == 409 :
221
+ logger .debug (f"User { member_email } is already invited to the workspace" )
222
+ else :
223
+ raise RuntimeError (f"Unable to invite member: { response .detail } " )
188
224
189
- logger .info ("Successfully invited member" , email = member_email )
225
+ logger .info (f "Successfully invited member { member_email } " )
190
226
191
227
return response
192
228
@@ -209,15 +245,10 @@ def send_team_invitation(
209
245
f"User { invitation .email } is already invited to the team ({ team .name } )"
210
246
)
211
247
return
212
- raise RuntimeError (f"Unable to invite member to the team: { response .content } " )
248
+ raise RuntimeError (f"Unable to invite member to the team: { response .detail } " )
213
249
214
250
logger .info (
215
- "Successfully invited member to the team" ,
216
- extra = dict (
217
- email = invitation .email ,
218
- team_id = team .id ,
219
- team_name = team .name ,
220
- ),
251
+ f"Successfully invited member { invitation .email } to the team { team .name } " ,
221
252
)
222
253
223
254
return response
@@ -250,33 +281,58 @@ def add_member_to_team(
250
281
)
251
282
return
252
283
else :
253
-
254
- raise RuntimeError (f"Unable to add member to the team: { response .content } " )
284
+ raise RuntimeError (f"Unable to add member to the team: { response .detail } " )
255
285
256
286
logger .info (
257
- "Successfully added member to the team" ,
258
- extra = dict (
259
- email = member .email ,
260
- team_id = team .id ,
261
- team_name = team .name ,
262
- ),
287
+ f"Successfully added member to the team { member .email } " ,
263
288
)
264
289
265
290
266
- def delete_teams_by_name (all_teams : list [Team ], team_names_to_delete : set [str ]):
291
+ def list_sources_by_team_id (all_teams : Iterable [Team ]) -> dict [id , list [Source ]]:
292
+ """
293
+ Give a list of teams, return all their sources mapped by team id to a list of source
294
+ """
295
+ source_by_team_id = defaultdict (list )
296
+ for team in all_teams :
297
+ team_sources = list_team_sources (team )
298
+ source_by_team_id [team .id ] = team_sources
299
+
300
+ return source_by_team_id
301
+
302
+
303
+ def delete_team (team : Team ):
304
+ response = CONFIG .client .delete_team (team .id )
305
+
306
+ if isinstance (response , Detail ):
307
+ raise RuntimeError (f"Unable to delete team { team .name } : { response .detail } " )
308
+
309
+ logger .info (f"Successfully deleted team { team .name } " )
310
+
311
+
312
+ def delete_teams_by_name (
313
+ all_teams : list [Team ],
314
+ team_names_to_delete : set [str ],
315
+ sources_by_team_id : dict [id , list [Source ]],
316
+ ):
267
317
"""
268
318
Given every team available in GitGuardian, remove teams that are in the set of
269
319
team to delete
320
+ It will not delete teams that have sources outside of gitlab
270
321
"""
271
322
272
323
to_remove = [team for team in all_teams if team .name in team_names_to_delete ]
273
324
274
325
for team in to_remove :
275
- CONFIG .client .delete_team (team .id )
276
- logger .warning (
277
- "Successfully deleted team" ,
278
- extra = dict (team_id = team .id , team_name = team .name ),
279
- )
326
+ team_sources = sources_by_team_id [team .id ]
327
+ if any (source .type != "gitlab" for source in team_sources ):
328
+ logger .warning (
329
+ f"Cannot delete team { team .name } , it has sources not in gitlab"
330
+ )
331
+ else :
332
+ CONFIG .client .delete_team (team .id )
333
+ logger .warning (
334
+ f"Successfully deleted team { team .name } " ,
335
+ )
280
336
281
337
282
338
def create_new_teams (teams : set [str ]) -> list [Team ]:
@@ -290,8 +346,7 @@ def create_new_teams(teams: set[str]) -> list[Team]:
290
346
new_teams .append (team )
291
347
292
348
logger .info (
293
- "Successfully created team" ,
294
- extra = dict (team_id = team .id , team_name = team .name ),
349
+ f"Successfully created team { team .name } " ,
295
350
)
296
351
297
352
return new_teams
@@ -308,14 +363,77 @@ def update_team_source(
308
363
response = CONFIG .client .update_team_source (payload )
309
364
310
365
if isinstance (response , Detail ):
311
- raise RuntimeError (f"Unable to update team source: { response .content } " )
366
+ raise RuntimeError (f"Unable to update team source: { response .detail } " )
367
+
368
+ logger .info (
369
+ f"Successfully updated sources for { team .name } " ,
370
+ )
371
+
372
+
373
+ def delete_invitation (invitation : Invitation ):
374
+ response = CONFIG .client .delete_invitation (invitation .id )
375
+
376
+ if isinstance (response , Detail ):
377
+ raise RuntimeError (f"Unable to delete invitation: { response .detail } " )
312
378
313
379
logger .info (
314
- "Successfully updated team sources" ,
315
- extra = dict (
316
- team_id = team .id ,
317
- team_name = team .name ,
318
- sources_added = sources_to_add ,
319
- sources_removed = sources_to_remove ,
320
- ),
380
+ f"Successfully deleted invitation for { invitation .email } " ,
321
381
)
382
+
383
+
384
+ def delete_invitations (invitations : Iterable [Invitation ]):
385
+ for invitation in invitations :
386
+ delete_invitation (invitation )
387
+
388
+
389
+ def delete_member (member : Member ):
390
+ """
391
+ Delete a member from the workspace
392
+ """
393
+
394
+ if not CONFIG .remove_members :
395
+ logger .debug ("Removing members is disabled, skipping..." )
396
+ return
397
+
398
+ payload = DeleteMemberParameters (member .id , send_email = CONFIG .send_email )
399
+ response = CONFIG .client .delete_member (payload )
400
+
401
+ if isinstance (response , Detail ):
402
+ logger .error (f"Unable to delete member : { member .email } " )
403
+ else :
404
+ logger .info (f"Successfully deleted member { member .email } " )
405
+
406
+
407
+ def deactivate_member (member : Member ):
408
+ """
409
+ Deactivate a member from the workspace
410
+ """
411
+
412
+ response = CONFIG .client .update_member (
413
+ UpdateMember (member .id , active = False , access_level = AccessLevel .RESTRICTED )
414
+ )
415
+
416
+ if isinstance (response , Detail ):
417
+ logger .error (f"Unable to deactivate member : { member .email } " )
418
+ else :
419
+ logger .info (f"Successfully deactivated member { member .email } " )
420
+
421
+
422
+ def remove_members (members_to_delete : Iterable [Member ]):
423
+ """
424
+ Delete or deactive members from the workspace depending on CONFIG
425
+ """
426
+
427
+ if not CONFIG .remove_members :
428
+ logger .debug ("Removing members is disabled, skipping..." )
429
+ return
430
+
431
+ for member in members_to_delete :
432
+ delete_member (member )
433
+
434
+
435
+ def list_team_invitations (team : Team ) -> list [TeamInvitation ]:
436
+ wrapper = lambda parameters : CONFIG .client .list_team_invitations (
437
+ team .id , parameters = parameters
438
+ )
439
+ return pagination_max_results (wrapper )
0 commit comments