Skip to content

Commit a6b1e64

Browse files
committed
disable token auth, move docs.rs call to async job
1 parent 345f3ea commit a6b1e64

File tree

7 files changed

+155
-40
lines changed

7 files changed

+155
-40
lines changed

src/controllers/version/docs.rs

+17-40
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
//! Endpoint for triggering a docs.rs rebuild
22
33
use super::CrateVersionPath;
4-
use super::update::authenticate;
54
use crate::app::AppState;
6-
use crate::rate_limiter::LimitedAction;
7-
use crate::util::HeaderMapExt;
5+
use crate::auth::AuthCheck;
86
use crate::util::errors::{AppResult, forbidden};
9-
use axum::body::Body;
10-
use axum::response::Response;
11-
use http::HeaderValue;
7+
use crate::worker::jobs;
8+
use axum::response::{IntoResponse as _, Response};
9+
use crates_io_worker::BackgroundJob as _;
10+
use http::StatusCode;
1211
use http::request::Parts;
1312

1413
/// Trigger a rebuild for the crate documentation on docs.rs.
@@ -17,7 +16,6 @@ use http::request::Parts;
1716
path = "/api/v1/crates/{name}/{version}/rebuild_docs",
1817
params(CrateVersionPath),
1918
security(
20-
("api_token" = []),
2119
("cookie" = []),
2220
),
2321
tag = "versions",
@@ -28,43 +26,22 @@ pub async fn rebuild_version_docs(
2826
path: CrateVersionPath,
2927
req: Parts,
3028
) -> AppResult<Response> {
31-
let Some(ref docs_rs_api_token) = app.config.docs_rs_api_token else {
29+
if app.config.docs_rs_api_token.is_none() {
3230
return Err(forbidden("docs.rs integration is not configured"));
3331
};
3432

35-
let mut conn = app.db_read().await?;
36-
// FIXME: which scope to use?
37-
let auth = authenticate(&req, &mut conn, &path.name).await?;
33+
let mut conn = app.db_write().await?;
34+
AuthCheck::only_cookie().check(&req, &mut conn).await?;
3835

39-
// FIXME: rate limiting needed here? which Action?
40-
app.rate_limiter
41-
.check_rate_limit(auth.user_id(), LimitedAction::YankUnyank, &mut conn)
42-
.await?;
36+
// validate if version & crate exist
37+
path.load_version_and_crate(&mut conn).await?;
4338

44-
let target_url = app
45-
.config
46-
.docs_rs_url
47-
.join(&format!("/crate/{}/{}/rebuild", path.name, path.version))
48-
.unwrap(); // FIXME: handle error
39+
if let Err(error) = jobs::DocsRsQueueRebuild::new(path.name, path.version)
40+
.enqueue(&mut conn)
41+
.await
42+
{
43+
warn!("docs_rs_queue_rebuild: Failed to enqueue background job: {error}");
44+
}
4945

50-
let client = reqwest::Client::new();
51-
let response = client
52-
.post(target_url.as_str())
53-
.bearer_auth(docs_rs_api_token)
54-
.send()
55-
.await?;
56-
57-
const APPLICATION_JSON: HeaderValue = HeaderValue::from_static("application/json");
58-
59-
Ok(Response::builder()
60-
.status(response.status())
61-
.header(
62-
"content-type",
63-
response
64-
.headers()
65-
.get("content-type")
66-
.unwrap_or(&APPLICATION_JSON),
67-
)
68-
.body(Body::from_stream(response.bytes_stream()))
69-
.unwrap())
46+
Ok(StatusCode::CREATED.into_response())
7047
}

src/docs_rs.rs

+100
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use async_trait::async_trait;
2+
use http::StatusCode;
3+
use mockall::automock;
4+
use url::Url;
5+
6+
#[derive(Debug, thiserror::Error)]
7+
pub enum DocsRsError {
8+
/// The rebuild couldn't be triggered.
9+
/// The reason is passed in the given error message.
10+
#[error("Bad request: {0}")]
11+
BadRequest(String),
12+
/// The request was rate limited by the server.
13+
/// This is the NGINX level rate limit for requests coming from a single IP.
14+
/// This is _not_ the rate limit that docs.rs might apply for rebuilds of the same crate
15+
/// (AKA: "rebuild too often").
16+
#[error("rate limited")]
17+
RateLimited,
18+
#[error(transparent)]
19+
Permission(anyhow::Error),
20+
/// crate or version not found on docs.rs.
21+
/// This can be temporary directly after a release until the docs.rs registry watcher
22+
/// queued the build for the release.
23+
#[error("crate or version not found on docs.rs")]
24+
NotFound,
25+
#[error(transparent)]
26+
Other(anyhow::Error),
27+
}
28+
29+
#[automock]
30+
#[async_trait]
31+
pub trait DocsRsClient: Send + Sync {
32+
async fn rebuild_docs(&self, name: &str, version: &str) -> Result<(), DocsRsError>;
33+
}
34+
35+
pub(crate) struct RealDocsRsClient {
36+
client: reqwest::Client,
37+
base_url: Url,
38+
api_token: String,
39+
}
40+
41+
impl RealDocsRsClient {
42+
pub fn new(base_url: impl Into<Url>, api_token: impl Into<String>) -> Self {
43+
Self {
44+
client: reqwest::Client::new(),
45+
base_url: base_url.into(),
46+
api_token: api_token.into(),
47+
}
48+
}
49+
}
50+
51+
#[async_trait]
52+
impl DocsRsClient for RealDocsRsClient {
53+
async fn rebuild_docs(&self, name: &str, version: &str) -> Result<(), DocsRsError> {
54+
let target_url = self
55+
.base_url
56+
.join(&format!("/crate/{name}/{version}/rebuild"))
57+
.map_err(|err| DocsRsError::Other(err.into()))?;
58+
59+
let response = self
60+
.client
61+
.post(target_url)
62+
.bearer_auth(&self.api_token)
63+
.send()
64+
.await
65+
.map_err(|err| DocsRsError::Other(err.into()))?;
66+
67+
match response.status() {
68+
StatusCode::CREATED => Ok(()),
69+
StatusCode::NOT_FOUND => Err(DocsRsError::NotFound),
70+
StatusCode::TOO_MANY_REQUESTS => Err(DocsRsError::RateLimited),
71+
StatusCode::BAD_REQUEST => {
72+
#[derive(Deserialize)]
73+
struct BadRequestResponse {
74+
message: String,
75+
}
76+
77+
let error_response: BadRequestResponse = response
78+
.json()
79+
.await
80+
.map_err(|err| DocsRsError::Other(err.into()))?;
81+
82+
Err(DocsRsError::BadRequest(error_response.message))
83+
}
84+
_ => Err(DocsRsError::Other(anyhow::anyhow!(
85+
"Unexpected response from docs.rs: {}\n{}",
86+
response.status(),
87+
response.text().await.unwrap_or_default()
88+
))),
89+
}
90+
}
91+
}
92+
93+
/// Builds an [DocsRsClient] implementation based on the [config::Server]
94+
pub fn docs_rs_client(config: &crate::config::Server) -> Box<dyn DocsRsClient + Send + Sync> {
95+
if let Some(api_token) = &config.docs_rs_api_token {
96+
Box::new(RealDocsRsClient::new(config.docs_rs_url.clone(), api_token))
97+
} else {
98+
Box::new(MockDocsRsClient::new())
99+
}
100+
}

src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub mod cloudfront;
3333
pub mod config;
3434
pub mod controllers;
3535
pub mod db;
36+
pub mod docs_rs;
3637
pub mod email;
3738
pub mod external_urls;
3839
pub mod fastly;

src/tests/util/test_app.rs

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use std::sync::LazyLock;
2626
use std::{rc::Rc, sync::Arc, time::Duration};
2727
use tokio::runtime::Handle;
2828
use tokio::task::block_in_place;
29+
use url::Url;
2930

3031
struct TestAppInner {
3132
app: Arc<App>,
@@ -477,6 +478,8 @@ fn simple_config() -> config::Server {
477478
og_image_base_url: None,
478479
html_render_cache_max_capacity: 1024,
479480
content_security_policy: None,
481+
docs_rs_url: Url::parse("https://docs.rs").unwrap(),
482+
docs_rs_api_token: None,
480483
}
481484
}
482485

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
use crate::docs_rs::docs_rs_client;
2+
use crate::worker::Environment;
3+
use crates_io_worker::BackgroundJob;
4+
use std::sync::Arc;
5+
6+
/// A background job that queues a docs rebuild for a specific release
7+
#[derive(Serialize, Deserialize)]
8+
pub struct DocsRsQueueRebuild {
9+
name: String,
10+
version: String,
11+
}
12+
13+
impl DocsRsQueueRebuild {
14+
pub fn new(name: String, version: String) -> Self {
15+
Self { name, version }
16+
}
17+
}
18+
19+
impl BackgroundJob for DocsRsQueueRebuild {
20+
const JOB_NAME: &'static str = "docs_rs_queue_rebuild";
21+
const DEDUPLICATED: bool = true;
22+
23+
type Context = Arc<Environment>;
24+
25+
async fn run(&self, ctx: Self::Context) -> anyhow::Result<()> {
26+
let client = docs_rs_client(&ctx.config);
27+
client.rebuild_docs(&self.name, &self.version).await?;
28+
29+
Ok(())
30+
}
31+
}

src/worker/jobs/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod archive_version_downloads;
22
mod daily_db_maintenance;
33
mod delete_crate;
4+
mod docs_rs_queue_rebuild;
45
mod downloads;
56
pub mod dump_db;
67
mod expiry_notification;
@@ -17,6 +18,7 @@ mod update_default_version;
1718
pub use self::archive_version_downloads::ArchiveVersionDownloads;
1819
pub use self::daily_db_maintenance::DailyDbMaintenance;
1920
pub use self::delete_crate::DeleteCrateFromStorage;
21+
pub use self::docs_rs_queue_rebuild::DocsRsQueueRebuild;
2022
pub use self::downloads::{
2123
CleanProcessedLogFiles, ProcessCdnLog, ProcessCdnLogQueue, UpdateDownloads,
2224
};

src/worker/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ impl RunnerExt for Runner<Arc<Environment>> {
2424
.register_job_type::<jobs::CleanProcessedLogFiles>()
2525
.register_job_type::<jobs::DailyDbMaintenance>()
2626
.register_job_type::<jobs::DeleteCrateFromStorage>()
27+
.register_job_type::<jobs::DocsRsQueueRebuild>()
2728
.register_job_type::<jobs::DumpDb>()
2829
.register_job_type::<jobs::IndexVersionDownloadsArchive>()
2930
.register_job_type::<jobs::InvalidateCdns>()

0 commit comments

Comments
 (0)