Skip to content

Commit 5b92a2e

Browse files
authored
Merge pull request #10596 from Turbo87/models-deps
models: Remove remaining `GitHubClient` usage
2 parents 5b5fd9d + bf4dca8 commit 5b92a2e

File tree

11 files changed

+177
-228
lines changed

11 files changed

+177
-228
lines changed

src/controllers/crate_owner_invitation.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use crate::app::AppState;
22
use crate::auth::AuthCheck;
33
use crate::auth::Authentication;
4+
use crate::controllers::helpers::authorization::Rights;
45
use crate::controllers::helpers::pagination::{Page, PaginationOptions, PaginationQueryParams};
56
use crate::models::crate_owner_invitation::AcceptError;
6-
use crate::models::{Crate, CrateOwnerInvitation, Rights, User};
7+
use crate::models::{Crate, CrateOwnerInvitation, User};
78
use crate::schema::{crate_owner_invitations, crates, users};
89
use crate::util::errors::{bad_request, custom, forbidden, internal, AppResult, BoxedAppError};
910
use crate::util::RequestUtils;
@@ -157,7 +158,7 @@ async fn prepare_list(
157158
// Only allow crate owners to query pending invitations for their crate.
158159
let krate: Crate = Crate::by_name(&crate_name).first(conn).await?;
159160
let owners = krate.owners(conn).await?;
160-
if user.rights(&*state.github, &owners).await? != Rights::Full {
161+
if Rights::get(user, &*state.github, &owners).await? != Rights::Full {
161162
let detail = "only crate owners can query pending invitations for their crate";
162163
return Err(forbidden(detail));
163164
}

src/controllers/helpers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::util::errors::AppResult;
22
use axum::response::{IntoResponse, Response};
33
use axum_extra::json;
44

5+
pub mod authorization;
56
pub(crate) mod pagination;
67

78
pub(crate) use self::pagination::Paginate;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use crate::models::{Owner, User};
2+
use crates_io_github::{GitHubClient, GitHubError};
3+
use oauth2::AccessToken;
4+
use secrecy::ExposeSecret;
5+
6+
/// Access rights to the crate (publishing and ownership management)
7+
/// NOTE: The order of these variants matters!
8+
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug, Clone, Copy)]
9+
pub enum Rights {
10+
None,
11+
Publish,
12+
Full,
13+
}
14+
15+
impl Rights {
16+
/// Given this set of owners, determines the strongest rights the
17+
/// user has.
18+
///
19+
/// Short-circuits on `Full` because you can't beat it. In practice, we'll always
20+
/// see `[user, user, user, ..., team, team, team]`, so we could shortcircuit on
21+
/// `Publish` as well, but this is a non-obvious invariant so we don't bother.
22+
/// Sweet free optimization if teams are proving burdensome to check.
23+
/// More than one team isn't really expected, though.
24+
pub async fn get(
25+
user: &User,
26+
gh_client: &dyn GitHubClient,
27+
owners: &[Owner],
28+
) -> Result<Self, GitHubError> {
29+
let token = AccessToken::new(user.gh_access_token.expose_secret().to_string());
30+
31+
let mut best = Self::None;
32+
for owner in owners {
33+
match *owner {
34+
Owner::User(ref other_user) => {
35+
if other_user.id == user.id {
36+
return Ok(Self::Full);
37+
}
38+
}
39+
Owner::Team(ref team) => {
40+
// Phones home to GitHub to ask if this User is a member of the given team.
41+
// Note that we're assuming that the given user is the one interested in
42+
// the answer. If this is not the case, then we could accidentally leak
43+
// private membership information here.
44+
let is_team_member = gh_client
45+
.team_membership(team.org_id, team.github_id, &user.gh_login, &token)
46+
.await?
47+
.is_some_and(|m| m.is_active());
48+
49+
if is_team_member {
50+
best = Self::Publish;
51+
}
52+
}
53+
}
54+
}
55+
Ok(best)
56+
}
57+
}

src/controllers/krate/delete.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use crate::app::AppState;
22
use crate::auth::AuthCheck;
3+
use crate::controllers::helpers::authorization::Rights;
34
use crate::controllers::krate::CratePath;
45
use crate::email::Email;
5-
use crate::models::{NewDeletedCrate, Rights};
6+
use crate::models::NewDeletedCrate;
67
use crate::schema::{crate_downloads, crates, dependencies};
78
use crate::util::errors::{custom, AppResult, BoxedAppError};
89
use crate::worker::jobs;
@@ -68,7 +69,7 @@ pub async fn delete_crate(
6869
// Check that the user is an owner of the crate (team owners are not allowed to delete crates)
6970
let user = auth.user();
7071
let owners = krate.owners(&mut conn).await?;
71-
match user.rights(&*app.github, &owners).await? {
72+
match Rights::get(user, &*app.github, &owners).await? {
7273
Rights::Full => {}
7374
Rights::Publish => {
7475
let msg = "team members don't have permission to delete crates";

src/controllers/krate/owners.rs

Lines changed: 103 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
//! All routes related to managing owners of a crate
22
3+
use crate::controllers::helpers::authorization::Rights;
34
use crate::controllers::krate::CratePath;
45
use crate::models::krate::OwnerRemoveError;
56
use crate::models::{
67
krate::NewOwnerInvite, token::EndpointScope, CrateOwner, NewCrateOwnerInvitation,
7-
NewCrateOwnerInvitationOutcome,
8+
NewCrateOwnerInvitationOutcome, NewTeam,
89
};
9-
use crate::models::{Crate, Owner, Rights, Team, User};
10+
use crate::models::{Crate, Owner, Team, User};
1011
use crate::util::errors::{bad_request, crate_not_found, custom, AppResult, BoxedAppError};
1112
use crate::views::EncodableOwner;
1213
use crate::{app::AppState, App};
@@ -15,12 +16,13 @@ use axum::Json;
1516
use axum_extra::json;
1617
use axum_extra::response::ErasedJson;
1718
use chrono::Utc;
18-
use crates_io_github::GitHubClient;
19+
use crates_io_github::{GitHubClient, GitHubError};
1920
use diesel::prelude::*;
2021
use diesel_async::scoped_futures::ScopedFutureExt;
2122
use diesel_async::{AsyncConnection, AsyncPgConnection, RunQueryDsl};
2223
use http::request::Parts;
2324
use http::StatusCode;
25+
use oauth2::AccessToken;
2426
use secrecy::{ExposeSecret, SecretString};
2527
use thiserror::Error;
2628

@@ -176,7 +178,7 @@ async fn modify_owners(
176178

177179
let owners = krate.owners(conn).await?;
178180

179-
match user.rights(&*app.github, &owners).await? {
181+
match Rights::get(user, &*app.github, &owners).await? {
180182
Rights::Full => {}
181183
// Yes!
182184
Rights::Publish => {
@@ -336,8 +338,26 @@ async fn add_team_owner(
336338
krate: &Crate,
337339
login: &str,
338340
) -> Result<NewOwnerInvite, OwnerAddError> {
341+
// github:rust-lang:owners
342+
let mut chunks = login.split(':');
343+
344+
let team_system = chunks.next().unwrap();
345+
if team_system != "github" {
346+
let error = "unknown organization handler, only 'github:org:team' is supported";
347+
return Err(bad_request(error).into());
348+
}
349+
350+
// unwrap is documented above as part of the calling contract
351+
let org = chunks.next().unwrap();
352+
let team = chunks.next().ok_or_else(|| {
353+
let error = "missing github team argument; format is github:org:team";
354+
bad_request(error)
355+
})?;
356+
339357
// Always recreate teams to get the most up-to-date GitHub ID
340-
let team = Team::create_or_update(gh_client, conn, login, req_user).await?;
358+
let team =
359+
create_or_update_github_team(gh_client, conn, &login.to_lowercase(), org, team, req_user)
360+
.await?;
341361

342362
// Teams are added as owners immediately, since the above call ensures
343363
// the user is a team member.
@@ -352,6 +372,84 @@ async fn add_team_owner(
352372
Ok(NewOwnerInvite::Team(team))
353373
}
354374

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+
let is_team_member = gh_client
415+
.team_membership(org_id, team.id, gh_login, &token)
416+
.await?
417+
.is_some_and(|m| m.is_active());
418+
419+
let can_add_team =
420+
is_team_member || is_gh_org_owner(gh_client, org_id, gh_login, &token).await?;
421+
422+
if !can_add_team {
423+
return Err(custom(
424+
StatusCode::FORBIDDEN,
425+
"only members of a team or organization owners can add it as an owner",
426+
));
427+
}
428+
429+
let org = gh_client.org_by_name(org_name, &token).await?;
430+
431+
NewTeam::builder()
432+
.login(&login.to_lowercase())
433+
.org_id(org_id)
434+
.github_id(team.id)
435+
.maybe_name(team.name.as_deref())
436+
.maybe_avatar(org.avatar_url.as_deref())
437+
.build()
438+
.create_or_update(conn)
439+
.await
440+
.map_err(Into::into)
441+
}
442+
443+
async fn is_gh_org_owner(
444+
gh_client: &dyn GitHubClient,
445+
org_id: i32,
446+
gh_login: &str,
447+
token: &AccessToken,
448+
) -> Result<bool, GitHubError> {
449+
let membership = gh_client.org_membership(org_id, gh_login, token).await?;
450+
Ok(membership.is_some_and(|m| m.is_active_admin()))
451+
}
452+
355453
/// Error results from a [`add_owner()`] model call.
356454
#[derive(Debug, Error)]
357455
enum OwnerAddError {

src/controllers/krate/publish.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ use url::Url;
2828

2929
use crate::models::{
3030
default_versions::Version as DefaultVersion, Category, Crate, DependencyKind, Keyword,
31-
NewCrate, NewVersion, NewVersionOwnerAction, Rights, VersionAction,
31+
NewCrate, NewVersion, NewVersionOwnerAction, VersionAction,
3232
};
3333

34+
use crate::controllers::helpers::authorization::Rights;
3435
use crate::licenses::parse_license_expr;
3536
use crate::middleware::log_request::RequestLogExt;
3637
use crate::models::token::EndpointScope;
@@ -356,7 +357,7 @@ pub async fn publish(app: AppState, req: Parts, body: Body) -> AppResult<Json<Go
356357
};
357358

358359
let owners = krate.owners(conn).await?;
359-
if user.rights(&*app.github, &owners).await? < Rights::Publish {
360+
if Rights::get(user, &*app.github, &owners).await? < Rights::Publish {
360361
return Err(custom(StatusCode::FORBIDDEN, MISSING_RIGHTS_ERROR_MESSAGE));
361362
}
362363

src/controllers/version/update.rs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
use super::CrateVersionPath;
22
use crate::app::AppState;
33
use crate::auth::{AuthCheck, Authentication};
4+
use crate::controllers::helpers::authorization::Rights;
45
use crate::models::token::EndpointScope;
5-
use crate::models::{
6-
Crate, NewVersionOwnerAction, Rights, Version, VersionAction, VersionOwnerAction,
7-
};
6+
use crate::models::{Crate, NewVersionOwnerAction, Version, VersionAction, VersionOwnerAction};
87
use crate::rate_limiter::LimitedAction;
98
use crate::schema::versions;
109
use crate::util::errors::{bad_request, custom, AppResult};
@@ -122,7 +121,7 @@ pub async fn perform_version_yank_update(
122121

123122
let yanked = yanked.unwrap_or(version.yanked);
124123

125-
if user.rights(&*state.github, &owners).await? < Rights::Publish {
124+
if Rights::get(user, &*state.github, &owners).await? < Rights::Publish {
126125
if user.is_admin {
127126
let action = if yanked { "yanking" } else { "unyanking" };
128127
warn!(

src/models.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ pub use self::follow::Follow;
1212
pub use self::keyword::{CrateKeyword, Keyword};
1313
pub use self::krate::{Crate, CrateName, NewCrate, RecentCrateDownloads};
1414
pub use self::owner::{CrateOwner, Owner, OwnerKind};
15-
pub use self::rights::Rights;
1615
pub use self::team::{NewTeam, Team};
1716
pub use self::token::ApiToken;
1817
pub use self::user::{NewUser, User};
@@ -32,8 +31,7 @@ mod follow;
3231
mod keyword;
3332
pub mod krate;
3433
mod owner;
35-
mod rights;
36-
mod team;
34+
pub mod team;
3735
pub mod token;
3836
pub mod user;
3937
pub mod version;

src/models/rights.rs

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)