diff --git a/sqlx-data.json b/sqlx-data.json index ee3f2634f..61d3fd908 100644 --- a/sqlx-data.json +++ b/sqlx-data.json @@ -4827,6 +4827,19 @@ }, "query": "\n DELETE FROM dependencies WHERE mod_dependency_id = NULL AND dependency_id = NULL AND dependency_file_name = NULL\n " }, + "af3f99ebd31aab8c911c9843dd8be814f302b41c9f7712944af14ab291fc0bdc": { + "describe": { + "columns": [], + "nullable": [], + "parameters": { + "Left": [ + "Int8", + "Text" + ] + } + }, + "query": "\n UPDATE users\n SET github_id = $1\n WHERE kratos_id = $2\n " + }, "b0c29c51bd3ae5b93d487471a98ee9bbb43a4df468ba781852b137dd315b9608": { "describe": { "columns": [], diff --git a/src/routes/v2/admin.rs b/src/routes/v2/admin.rs index 2ae66475a..be8208347 100644 --- a/src/routes/v2/admin.rs +++ b/src/routes/v2/admin.rs @@ -20,6 +20,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { web::scope("admin") .service(count_download) .service(add_minos_user) + .service(edit_github_id) .service(get_legacy_account) .service(process_payout), ); @@ -39,6 +40,44 @@ pub async fn add_minos_user( Ok(HttpResponse::Ok().finish()) } +// Add or update a user's GitHub ID by their kratos id +// OIDC ids should be kept in Minos, but Github is duplicated in Labrinth for legacy support +// This should not be directly useable by applications, only by the Minos backend +// user id is passed in path, github id is passed in body +#[derive(Deserialize)] +pub struct EditGithubId { + github_id: Option, +} +#[post("_edit_github_id/{kratos_id}", guard = "admin_key_guard")] +pub async fn edit_github_id( + pool: web::Data, + kratos_id: web::Path, + github_id: web::Json, +) -> Result { + let github_id = github_id.into_inner().github_id; + // Parse error if github inner id not a number + let github_id = github_id + .as_ref() + .map(|x| x.parse::()) + .transpose() + .map_err(|_| ApiError::InvalidInput("Github id must be a number".to_string()))?; + + let mut transaction = pool.begin().await?; + sqlx::query!( + " + UPDATE users + SET github_id = $1 + WHERE kratos_id = $2 + ", + github_id, + kratos_id.into_inner() + ) + .execute(&mut transaction) + .await?; + transaction.commit().await?; + Ok(HttpResponse::Ok().finish()) +} + #[get("_legacy_account/{github_id}", guard = "admin_key_guard")] pub async fn get_legacy_account( diff --git a/src/routes/v2/pats.rs b/src/routes/v2/pats.rs index 59636189d..04b4d875f 100644 --- a/src/routes/v2/pats.rs +++ b/src/routes/v2/pats.rs @@ -78,7 +78,7 @@ pub async fn get_pats(req: HttpRequest, pool: Data) -> Result get_user_from_pat(token, executor).await?, - ("ory_", _) => get_user_from_minos_session_token(token, executor).await?, + let possible_user = match token.split_once('_') { + Some(("modrinth", _)) => get_user_from_pat(token, executor).await?, + Some(("ory", _)) => get_user_from_minos_session_token(token, executor).await?, + Some(("github", _)) | Some(("gho", _)) | Some(("ghp", _)) => { + get_user_from_github_token(token, executor).await? + } _ => return Err(AuthenticationError::InvalidAuthMethod), }; Ok(possible_user) @@ -341,11 +344,36 @@ where ); let res = req.send().await?.error_for_status()?; let minos_user: MinosUser = res.json().await?; - let db_user = models::User::get_from_minos_kratos_id(minos_user.id.clone(), executor).await?; Ok(db_user) } +#[derive(Serialize, Deserialize, Debug)] +pub struct GitHubUser { + pub id: u64, +} +// Get a database user from a GitHub PAT +pub async fn get_user_from_github_token<'a, E>( + access_token: &str, + executor: E, +) -> Result, AuthenticationError> +where + E: sqlx::Executor<'a, Database = sqlx::Postgres>, +{ + let github_user: GitHubUser = reqwest::Client::new() + .get("https://api.github.com/user") + .header(reqwest::header::USER_AGENT, "Modrinth") + .header( + reqwest::header::AUTHORIZATION, + format!("token {access_token}"), + ) + .send() + .await? + .json() + .await?; + Ok(user_item::User::get_from_github_id(github_user.id, executor).await?) +} + pub async fn check_is_moderator_from_headers<'a, 'b, E>( headers: &HeaderMap, executor: E, diff --git a/src/util/guards.rs b/src/util/guards.rs index 8cc352d6e..f7b6b358a 100644 --- a/src/util/guards.rs +++ b/src/util/guards.rs @@ -4,7 +4,6 @@ pub const ADMIN_KEY_HEADER: &str = "Modrinth-Admin"; pub fn admin_key_guard(ctx: &GuardContext) -> bool { let admin_key = std::env::var("LABRINTH_ADMIN_KEY") .expect("No admin key provided, this should have been caught by check_env_vars"); - ctx.head() .headers() .get(ADMIN_KEY_HEADER) diff --git a/src/util/pat.rs b/src/util/pat.rs index f052d0375..a725f7331 100644 --- a/src/util/pat.rs +++ b/src/util/pat.rs @@ -75,7 +75,7 @@ where Ok(None) } -// Generate a new 128 char PAT token starting with 'mod_' +// Generate a new 128 char PAT token starting with 'modrinth_pat_' pub async fn generate_pat( con: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { @@ -86,9 +86,9 @@ pub async fn generate_pat( // First generate the PAT token as a random 128 char string. This may include uppercase and lowercase and numbers only. loop { let mut access_token = String::with_capacity(63); - access_token.push_str("mod_"); - for _ in 0..60 { - let c = rng.gen_range(0..60); + access_token.push_str("modrinth_pat_"); + for _ in 0..51 { + let c = rng.gen_range(0..62); if c < 10 { access_token.push(char::from_u32(c + 48).unwrap()); // 0-9 } else if c < 36 {