fix(registry): prevent token usage from unauthorized apps

This commit is contained in:
daimond113 2024-10-14 17:55:11 +02:00
parent 756b5c8257
commit 66a885b4e6
No known key found for this signature in database
GPG key ID: 3A8ECE51328B513C
6 changed files with 57 additions and 24 deletions

View file

@ -9,15 +9,20 @@ COMMITTER_GIT_EMAIL= # email of the committer used for index updates
# AUTHENTICATION CONFIGURATION # AUTHENTICATION CONFIGURATION
# Set the variables of the authentication you want to use in order to enable it # Set the variables of the authentication you want to use in order to enable it
READ_NEEDS_AUTH= # set to any value to require authentication for read requests
# Single Token # Single Token
ACCESS_TOKEN= # a single token that is used to authenticate all publish requests ACCESS_TOKEN= # a single token that is used to authenticate all publish requests
# Read/Write Tokens # Read/Write Tokens
# READ_NEEDS_AUTH isn't required for this
READ_ACCESS_TOKEN= # a token that is used to authenticate read requests READ_ACCESS_TOKEN= # a token that is used to authenticate read requests
WRITE_ACCESS_TOKEN= # a token that is used to authenticate write requests WRITE_ACCESS_TOKEN= # a token that is used to authenticate write requests
# GitHub # GitHub
GITHUB_AUTH= # set to any value to enable GitHub authentication GITHUB_AUTH= # set to any value to enable GitHub authentication
GITHUB_CLIENT_SECRET= # client secret of the GitHub OAuth app configured in the index's `config.toml`
# If none of the above is set, no authentication is required, even for write requests # If none of the above is set, no authentication is required, even for write requests

View file

@ -1,29 +1,51 @@
use crate::auth::{get_token_from_req, AuthImpl, UserId}; use crate::auth::{get_token_from_req, AuthImpl, UserId};
use actix_web::{dev::ServiceRequest, Error as ActixError}; use actix_web::{dev::ServiceRequest, Error as ActixError};
use serde::Deserialize; use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::fmt::Display; use std::fmt::Display;
#[derive(Debug)] #[derive(Debug)]
pub struct GitHubAuth { pub struct GitHubAuth {
pub reqwest_client: reqwest::Client, pub reqwest_client: reqwest::Client,
pub client_id: String,
pub client_secret: String,
}
#[derive(Debug, Serialize)]
struct TokenRequestBody {
access_token: String,
} }
impl AuthImpl for GitHubAuth { impl AuthImpl for GitHubAuth {
async fn for_write_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> { async fn for_write_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> {
let token = match get_token_from_req(req, true) { let token = match get_token_from_req(req) {
Some(token) => token, Some(token) => token,
None => return Ok(None), None => return Ok(None),
}; };
let response = match self let response = match self
.reqwest_client .reqwest_client
.get("https://api.github.com/user") .post(format!(
.header(reqwest::header::AUTHORIZATION, token) "https://api.github.com/applications/{}/token",
self.client_id
))
.basic_auth(&self.client_id, Some(&self.client_secret))
.json(&TokenRequestBody {
access_token: token,
})
.send() .send()
.await .await
.and_then(|res| res.error_for_status())
{ {
Ok(response) => response, Ok(response) => match response.error_for_status_ref() {
Ok(_) => response,
Err(e) if e.status().is_some_and(|s| s == StatusCode::UNAUTHORIZED) => {
return Ok(None);
}
Err(e) => {
log::error!("failed to get user: {e}");
return Ok(None);
}
},
Err(e) => { Err(e) => {
log::error!("failed to get user: {e}"); log::error!("failed to get user: {e}");
return Ok(None); return Ok(None);
@ -31,7 +53,7 @@ impl AuthImpl for GitHubAuth {
}; };
let user_id = match response.json::<UserResponse>().await { let user_id = match response.json::<UserResponse>().await {
Ok(user) => user.id, Ok(resp) => resp.user.id,
Err(e) => { Err(e) => {
log::error!("failed to get user: {e}"); log::error!("failed to get user: {e}");
return Ok(None); return Ok(None);
@ -49,6 +71,11 @@ impl Display for GitHubAuth {
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct UserResponse { struct User {
id: u64, id: u64,
} }
#[derive(Debug, Deserialize)]
struct UserResponse {
user: User,
}

View file

@ -13,6 +13,7 @@ use actix_web::{
middleware::Next, middleware::Next,
web, HttpMessage, HttpResponse, web, HttpMessage, HttpResponse,
}; };
use pesde::{source::pesde::PesdePackageSource, Project};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::fmt::Display; use std::fmt::Display;
@ -55,7 +56,7 @@ pub trait AuthImpl: Display {
} }
fn read_needs_auth(&self) -> bool { fn read_needs_auth(&self) -> bool {
false benv!("READ_NEEDS_AUTH").is_ok()
} }
} }
@ -141,14 +142,20 @@ pub async fn read_mw(
next.call(req).await.map(|res| res.map_into_left_body()) next.call(req).await.map(|res| res.map_into_left_body())
} }
pub fn get_auth_from_env() -> Auth { pub fn get_auth_from_env(index: &PesdePackageSource, project: &Project) -> Auth {
if let Ok(token) = benv!("ACCESS_TOKEN") { if let Ok(token) = benv!("ACCESS_TOKEN") {
Auth::Token(token::TokenAuth { Auth::Token(token::TokenAuth {
token: *Sha256::digest(token.as_bytes()).as_ref(), token: *Sha256::digest(token.as_bytes()).as_ref(),
}) })
} else if benv!("GITHUB_AUTH").is_ok() { } else if benv!("GITHUB_AUTH").is_ok() {
let config = index.config(project).expect("failed to get index config");
Auth::GitHub(github::GitHubAuth { Auth::GitHub(github::GitHubAuth {
reqwest_client: make_reqwest(), reqwest_client: make_reqwest(),
client_id: config
.github_oauth_client_id
.expect("index isn't configured for GitHub"),
client_secret: benv!(required "GITHUB_CLIENT_SECRET"),
}) })
} else if let Ok((r, w)) = } else if let Ok((r, w)) =
benv!("READ_ACCESS_TOKEN").and_then(|r| benv!("WRITE_ACCESS_TOKEN").map(|w| (r, w))) benv!("READ_ACCESS_TOKEN").and_then(|r| benv!("WRITE_ACCESS_TOKEN").map(|w| (r, w)))
@ -162,7 +169,7 @@ pub fn get_auth_from_env() -> Auth {
} }
} }
pub fn get_token_from_req(req: &ServiceRequest, bearer: bool) -> Option<String> { pub fn get_token_from_req(req: &ServiceRequest) -> Option<String> {
let token = match req let token = match req
.headers() .headers()
.get(AUTHORIZATION) .get(AUTHORIZATION)
@ -172,13 +179,7 @@ pub fn get_token_from_req(req: &ServiceRequest, bearer: bool) -> Option<String>
None => return None, None => return None,
}; };
let token = if bearer { let token = if token.to_lowercase().starts_with("bearer ") {
if token.to_lowercase().starts_with("bearer ") {
token.to_string()
} else {
format!("Bearer {token}")
}
} else if token.to_lowercase().starts_with("bearer ") {
token[7..].to_string() token[7..].to_string()
} else { } else {
token.to_string() token.to_string()

View file

@ -12,7 +12,7 @@ pub struct RwTokenAuth {
impl AuthImpl for RwTokenAuth { impl AuthImpl for RwTokenAuth {
async fn for_write_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> { async fn for_write_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> {
let token = match get_token_from_req(req, false) { let token = match get_token_from_req(req) {
Some(token) => token, Some(token) => token,
None => return Ok(None), None => return Ok(None),
}; };
@ -27,7 +27,7 @@ impl AuthImpl for RwTokenAuth {
} }
async fn for_read_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> { async fn for_read_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> {
let token = match get_token_from_req(req, false) { let token = match get_token_from_req(req) {
Some(token) => token, Some(token) => token,
None => return Ok(None), None => return Ok(None),
}; };

View file

@ -12,7 +12,7 @@ pub struct TokenAuth {
impl AuthImpl for TokenAuth { impl AuthImpl for TokenAuth {
async fn for_write_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> { async fn for_write_request(&self, req: &ServiceRequest) -> Result<Option<UserId>, ActixError> {
let token = match get_token_from_req(req, false) { let token = match get_token_from_req(req) {
Some(token) => token, Some(token) => token,
None => return Ok(None), None => return Ok(None),
}; };

View file

@ -104,18 +104,18 @@ async fn run(with_sentry: bool) -> std::io::Result<()> {
let (search_reader, search_writer) = make_search(&project, &source); let (search_reader, search_writer) = make_search(&project, &source);
let app_data = web::Data::new(AppState { let app_data = web::Data::new(AppState {
source: Mutex::new(source),
project,
storage: { storage: {
let storage = get_storage_from_env(); let storage = get_storage_from_env();
info!("storage: {storage}"); info!("storage: {storage}");
storage storage
}, },
auth: { auth: {
let auth = get_auth_from_env(); let auth = get_auth_from_env(&source, &project);
info!("auth: {auth}"); info!("auth: {auth}");
auth auth
}, },
source: Mutex::new(source),
project,
search_reader, search_reader,
search_writer: Mutex::new(search_writer), search_writer: Mutex::new(search_writer),