22
33use crate :: controllers:: krate:: CratePath ;
44use crate :: models:: krate:: OwnerRemoveError ;
5+ use crate :: models:: team:: can_add_team;
56use crate :: models:: {
67 krate:: NewOwnerInvite , token:: EndpointScope , CrateOwner , NewCrateOwnerInvitation ,
7- NewCrateOwnerInvitationOutcome ,
8+ NewCrateOwnerInvitationOutcome , NewTeam ,
89} ;
910use crate :: models:: { Crate , Owner , Rights , Team , User } ;
1011use crate :: util:: errors:: { bad_request, crate_not_found, custom, AppResult , BoxedAppError } ;
@@ -21,6 +22,7 @@ use diesel_async::scoped_futures::ScopedFutureExt;
2122use diesel_async:: { AsyncConnection , AsyncPgConnection , RunQueryDsl } ;
2223use http:: request:: Parts ;
2324use http:: StatusCode ;
25+ use oauth2:: AccessToken ;
2426use secrecy:: { ExposeSecret , SecretString } ;
2527use thiserror:: Error ;
2628
@@ -353,15 +355,9 @@ async fn add_team_owner(
353355 } ) ?;
354356
355357 // Always recreate teams to get the most up-to-date GitHub ID
356- let team = Team :: create_or_update_github_team (
357- gh_client,
358- conn,
359- & login. to_lowercase ( ) ,
360- org,
361- team,
362- req_user,
363- )
364- . await ?;
358+ let team =
359+ create_or_update_github_team ( gh_client, conn, & login. to_lowercase ( ) , org, team, req_user)
360+ . await ?;
365361
366362 // Teams are added as owners immediately, since the above call ensures
367363 // the user is a team member.
@@ -376,6 +372,66 @@ async fn add_team_owner(
376372 Ok ( NewOwnerInvite :: Team ( team) )
377373}
378374
375+ /// Tries to create or update a Github Team. Assumes `org` and `team` are
376+ /// correctly parsed out of the full `name`. `name` is passed as a
377+ /// convenience to avoid rebuilding it.
378+ pub async fn create_or_update_github_team (
379+ gh_client : & dyn GitHubClient ,
380+ conn : & mut AsyncPgConnection ,
381+ login : & str ,
382+ org_name : & str ,
383+ team_name : & str ,
384+ req_user : & User ,
385+ ) -> AppResult < Team > {
386+ // GET orgs/:org/teams
387+ // check that `team` is the `slug` in results, and grab its data
388+
389+ // "sanitization"
390+ fn is_allowed_char ( c : char ) -> bool {
391+ matches ! ( c, 'a' ..='z' | 'A' ..='Z' | '0' ..='9' | '-' | '_' )
392+ }
393+
394+ if let Some ( c) = org_name. chars ( ) . find ( |c| !is_allowed_char ( * c) ) {
395+ return Err ( bad_request ( format_args ! (
396+ "organization cannot contain special \
397+ characters like {c}"
398+ ) ) ) ;
399+ }
400+
401+ let token = AccessToken :: new ( req_user. gh_access_token . expose_secret ( ) . to_string ( ) ) ;
402+ let team = gh_client. team_by_name ( org_name, team_name, & token) . await
403+ . map_err ( |_| {
404+ bad_request ( format_args ! (
405+ "could not find the github team {org_name}/{team_name}. \
406+ Make sure that you have the right permissions in GitHub. \
407+ See https://doc.rust-lang.org/cargo/reference/publishing.html#github-permissions"
408+ ) )
409+ } ) ?;
410+
411+ let org_id = team. organization . id ;
412+ let gh_login = & req_user. gh_login ;
413+
414+ if !can_add_team ( gh_client, org_id, team. id , gh_login, & token) . await ? {
415+ return Err ( custom (
416+ StatusCode :: FORBIDDEN ,
417+ "only members of a team or organization owners can add it as an owner" ,
418+ ) ) ;
419+ }
420+
421+ let org = gh_client. org_by_name ( org_name, & token) . await ?;
422+
423+ NewTeam :: builder ( )
424+ . login ( & login. to_lowercase ( ) )
425+ . org_id ( org_id)
426+ . github_id ( team. id )
427+ . maybe_name ( team. name . as_deref ( ) )
428+ . maybe_avatar ( org. avatar_url . as_deref ( ) )
429+ . build ( )
430+ . create_or_update ( conn)
431+ . await
432+ . map_err ( Into :: into)
433+ }
434+
379435/// Error results from a [`add_owner()`] model call.
380436#[ derive( Debug , Error ) ]
381437enum OwnerAddError {
0 commit comments