|
1 |
| -use super::{cache::CachePolicy, error::AxumNope, headers::CanonicalUrl}; |
| 1 | +use super::{ |
| 2 | + cache::CachePolicy, |
| 3 | + error::{AxumNope, JsonAxumNope, JsonAxumResult}, |
| 4 | + headers::CanonicalUrl, |
| 5 | +}; |
2 | 6 | use crate::{
|
3 | 7 | db::types::BuildStatus,
|
4 | 8 | docbuilder::Limits,
|
5 | 9 | impl_axum_webpage,
|
| 10 | + utils::spawn_blocking, |
6 | 11 | web::{
|
| 12 | + crate_details::CrateDetails, |
7 | 13 | error::AxumResult,
|
8 | 14 | extractors::{DbConnection, Path},
|
9 | 15 | match_version, MetaData, ReqVersion,
|
10 | 16 | },
|
11 |
| - Config, |
| 17 | + BuildQueue, Config, |
12 | 18 | };
|
13 |
| -use anyhow::Result; |
| 19 | +use anyhow::{anyhow, Result}; |
14 | 20 | use axum::{
|
15 | 21 | extract::Extension, http::header::ACCESS_CONTROL_ALLOW_ORIGIN, response::IntoResponse, Json,
|
16 | 22 | };
|
| 23 | +use axum_extra::{ |
| 24 | + headers::{authorization::Bearer, Authorization}, |
| 25 | + TypedHeader, |
| 26 | +}; |
17 | 27 | use chrono::{DateTime, Utc};
|
18 | 28 | use semver::Version;
|
19 | 29 | use serde::Serialize;
|
| 30 | +use serde_json::json; |
20 | 31 | use std::sync::Arc;
|
21 | 32 |
|
22 | 33 | #[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
@@ -111,6 +122,81 @@ pub(crate) async fn build_list_json_handler(
|
111 | 122 | .into_response())
|
112 | 123 | }
|
113 | 124 |
|
| 125 | +async fn build_trigger_check( |
| 126 | + mut conn: DbConnection, |
| 127 | + name: &String, |
| 128 | + version: &Version, |
| 129 | + build_queue: &Arc<BuildQueue>, |
| 130 | +) -> AxumResult<impl IntoResponse> { |
| 131 | + let _ = CrateDetails::new(&mut *conn, &name, &version, None, vec![]) |
| 132 | + .await? |
| 133 | + .ok_or(AxumNope::VersionNotFound)?; |
| 134 | + |
| 135 | + let crate_version_is_in_queue = spawn_blocking({ |
| 136 | + let name = name.clone(); |
| 137 | + let version_string = version.to_string(); |
| 138 | + let build_queue = build_queue.clone(); |
| 139 | + move || build_queue.has_build_queued(&name, &version_string) |
| 140 | + }) |
| 141 | + .await?; |
| 142 | + if crate_version_is_in_queue { |
| 143 | + return Err(AxumNope::BadRequest(anyhow!( |
| 144 | + "crate {name} {version} already queued for rebuild" |
| 145 | + ))); |
| 146 | + } |
| 147 | + |
| 148 | + Ok(()) |
| 149 | +} |
| 150 | + |
| 151 | +// Priority according to issue #2442; positive here as it's inverted. |
| 152 | +// FUTURE: move to a crate-global enum with all special priorities? |
| 153 | +const TRIGGERED_REBUILD_PRIORITY: i32 = 5; |
| 154 | + |
| 155 | +pub(crate) async fn build_trigger_rebuild_handler( |
| 156 | + Path((name, version)): Path<(String, Version)>, |
| 157 | + conn: DbConnection, |
| 158 | + Extension(build_queue): Extension<Arc<BuildQueue>>, |
| 159 | + Extension(config): Extension<Arc<Config>>, |
| 160 | + opt_auth_header: Option<TypedHeader<Authorization<Bearer>>>, |
| 161 | +) -> JsonAxumResult<impl IntoResponse> { |
| 162 | + let expected_token = |
| 163 | + config |
| 164 | + .trigger_rebuild_token |
| 165 | + .as_ref() |
| 166 | + .ok_or(JsonAxumNope(AxumNope::InternalError(anyhow!( |
| 167 | + "access token `trigger_rebuild_token` \ |
| 168 | + is not configured" |
| 169 | + ))))?; |
| 170 | + |
| 171 | + // (Future: would it be better to have standard middleware handle auth?) |
| 172 | + let TypedHeader(auth_header) = |
| 173 | + opt_auth_header.ok_or(JsonAxumNope(AxumNope::MissingAuthenticationToken))?; |
| 174 | + if auth_header.token() != expected_token { |
| 175 | + return Err(JsonAxumNope(AxumNope::InvalidAuthenticationToken)); |
| 176 | + } |
| 177 | + |
| 178 | + build_trigger_check(conn, &name, &version, &build_queue) |
| 179 | + .await |
| 180 | + .map_err(JsonAxumNope)?; |
| 181 | + |
| 182 | + spawn_blocking({ |
| 183 | + let name = name.clone(); |
| 184 | + let version_string = version.to_string(); |
| 185 | + move || { |
| 186 | + build_queue.add_crate( |
| 187 | + &name, |
| 188 | + &version_string, |
| 189 | + TRIGGERED_REBUILD_PRIORITY, |
| 190 | + None, /* because crates.io is the only service that calls this endpoint */ |
| 191 | + ) |
| 192 | + } |
| 193 | + }) |
| 194 | + .await |
| 195 | + .map_err(|e| JsonAxumNope(e.into()))?; |
| 196 | + |
| 197 | + Ok(Json(json!({}))) |
| 198 | +} |
| 199 | + |
114 | 200 | async fn get_builds(
|
115 | 201 | conn: &mut sqlx::PgConnection,
|
116 | 202 | name: &str,
|
@@ -276,6 +362,119 @@ mod tests {
|
276 | 362 | });
|
277 | 363 | }
|
278 | 364 |
|
| 365 | + #[test] |
| 366 | + fn build_trigger_rebuild_missing_config() { |
| 367 | + wrapper(|env| { |
| 368 | + env.fake_release().name("foo").version("0.1.0").create()?; |
| 369 | + |
| 370 | + { |
| 371 | + let response = env.frontend().get("/crate/regex/1.3.1/rebuild").send()?; |
| 372 | + // Needs POST |
| 373 | + assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); |
| 374 | + } |
| 375 | + |
| 376 | + { |
| 377 | + let response = env.frontend().post("/crate/regex/1.3.1/rebuild").send()?; |
| 378 | + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); |
| 379 | + let text = response.text()?; |
| 380 | + assert!(text.contains("access token `trigger_rebuild_token` is not configured")); |
| 381 | + let json: serde_json::Value = serde_json::from_str(&text)?; |
| 382 | + assert_eq!( |
| 383 | + json, |
| 384 | + serde_json::json!({ |
| 385 | + "title": "Internal Server Error", |
| 386 | + "message": "access token `trigger_rebuild_token` is not configured" |
| 387 | + }) |
| 388 | + ); |
| 389 | + } |
| 390 | + |
| 391 | + Ok(()) |
| 392 | + }) |
| 393 | + } |
| 394 | + |
| 395 | + #[test] |
| 396 | + fn build_trigger_rebuild_with_config() { |
| 397 | + wrapper(|env| { |
| 398 | + let correct_token = "foo137"; |
| 399 | + env.override_config(|config| config.trigger_rebuild_token = Some(correct_token.into())); |
| 400 | + |
| 401 | + env.fake_release().name("foo").version("0.1.0").create()?; |
| 402 | + |
| 403 | + { |
| 404 | + let response = env.frontend().post("/crate/regex/1.3.1/rebuild").send()?; |
| 405 | + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); |
| 406 | + let text = response.text()?; |
| 407 | + let json: serde_json::Value = serde_json::from_str(&text)?; |
| 408 | + assert_eq!( |
| 409 | + json, |
| 410 | + serde_json::json!({ |
| 411 | + "title": "Missing authentication token", |
| 412 | + "message": "The token used for authentication is missing" |
| 413 | + }) |
| 414 | + ); |
| 415 | + } |
| 416 | + |
| 417 | + { |
| 418 | + let response = env |
| 419 | + .frontend() |
| 420 | + .post("/crate/regex/1.3.1/rebuild") |
| 421 | + .bearer_auth("someinvalidtoken") |
| 422 | + .send()?; |
| 423 | + assert_eq!(response.status(), StatusCode::UNAUTHORIZED); |
| 424 | + let text = response.text()?; |
| 425 | + let json: serde_json::Value = serde_json::from_str(&text)?; |
| 426 | + assert_eq!( |
| 427 | + json, |
| 428 | + serde_json::json!({ |
| 429 | + "title": "Invalid authentication token", |
| 430 | + "message": "The token used for authentication is not valid" |
| 431 | + }) |
| 432 | + ); |
| 433 | + } |
| 434 | + |
| 435 | + assert_eq!(env.build_queue().pending_count()?, 0); |
| 436 | + assert!(!env.build_queue().has_build_queued("foo", "0.1.0")?); |
| 437 | + |
| 438 | + { |
| 439 | + let response = env |
| 440 | + .frontend() |
| 441 | + .post("/crate/foo/0.1.0/rebuild") |
| 442 | + .bearer_auth(correct_token) |
| 443 | + .send()?; |
| 444 | + assert_eq!(response.status(), StatusCode::OK); |
| 445 | + let text = response.text()?; |
| 446 | + let json: serde_json::Value = serde_json::from_str(&text)?; |
| 447 | + assert_eq!(json, serde_json::json!({})); |
| 448 | + } |
| 449 | + |
| 450 | + assert_eq!(env.build_queue().pending_count()?, 1); |
| 451 | + assert!(env.build_queue().has_build_queued("foo", "0.1.0")?); |
| 452 | + |
| 453 | + { |
| 454 | + let response = env |
| 455 | + .frontend() |
| 456 | + .post("/crate/foo/0.1.0/rebuild") |
| 457 | + .bearer_auth(correct_token) |
| 458 | + .send()?; |
| 459 | + assert_eq!(response.status(), StatusCode::BAD_REQUEST); |
| 460 | + let text = response.text()?; |
| 461 | + let json: serde_json::Value = serde_json::from_str(&text)?; |
| 462 | + assert_eq!( |
| 463 | + json, |
| 464 | + serde_json::json!({ |
| 465 | + "title": "Bad request", |
| 466 | + "message": "crate foo 0.1.0 already queued for rebuild" |
| 467 | + }) |
| 468 | + ); |
| 469 | + } |
| 470 | + |
| 471 | + assert_eq!(env.build_queue().pending_count()?, 1); |
| 472 | + assert!(env.build_queue().has_build_queued("foo", "0.1.0")?); |
| 473 | + |
| 474 | + Ok(()) |
| 475 | + }); |
| 476 | + } |
| 477 | + |
279 | 478 | #[test]
|
280 | 479 | fn build_empty_list() {
|
281 | 480 | wrapper(|env| {
|
|
0 commit comments