From 09beb52d8fe8d6833f881bc6d2365268564e84d8 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Mon, 29 Jul 2024 16:56:48 +0100 Subject: [PATCH 1/3] feat: [#974] new API endpoint to upload pre-existing keys You can test it with: ```console curl -X POST http://localhost:1212/api/v1/keys?token=MyAccessToken \ -H "Content-Type: application/json" \ -d '{ "key": "Xc1L4PbQJSFGlrgSRZl8wxSFAuMa21z7", "seconds_valid": 7200 }' ``` The `key` field is optional. If it's not provided a random key will be generated. --- src/core/auth.rs | 2 +- src/core/mod.rs | 38 +++++++++++-- src/servers/apis/v1/context/auth_key/forms.rs | 8 +++ .../apis/v1/context/auth_key/handlers.rs | 53 ++++++++++++++++++- src/servers/apis/v1/context/auth_key/mod.rs | 1 + .../apis/v1/context/auth_key/responses.rs | 21 +++++++- .../apis/v1/context/auth_key/routes.rs | 8 ++- src/servers/apis/v1/responses.rs | 3 +- 8 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 src/servers/apis/v1/context/auth_key/forms.rs diff --git a/src/core/auth.rs b/src/core/auth.rs index 94d455d7e..00ded71ef 100644 --- a/src/core/auth.rs +++ b/src/core/auth.rs @@ -152,7 +152,7 @@ pub struct Key(String); /// ``` /// /// If the string does not contains a valid key, the parser function will return this error. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Display)] pub struct ParseKeyError; impl FromStr for Key { diff --git a/src/core/mod.rs b/src/core/mod.rs index 64d5e2c9a..f0853ec27 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -453,6 +453,7 @@ use std::panic::Location; use std::sync::Arc; use std::time::Duration; +use auth::ExpiringKey; use databases::driver::Driver; use derive_more::Constructor; use tokio::sync::mpsc::error::SendError; @@ -460,9 +461,9 @@ use torrust_tracker_clock::clock::Time; use torrust_tracker_configuration::v2::database; use torrust_tracker_configuration::{AnnouncePolicy, Core, TORRENT_PEERS_LIMIT}; use torrust_tracker_primitives::info_hash::InfoHash; -use torrust_tracker_primitives::peer; use torrust_tracker_primitives::swarm_metadata::SwarmMetadata; use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics; +use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch}; use torrust_tracker_torrent_repository::entry::EntrySync; use torrust_tracker_torrent_repository::repository::Repository; use tracing::debug; @@ -804,6 +805,37 @@ impl Tracker { /// Will return a `database::Error` if unable to add the `auth_key` to the database. pub async fn generate_auth_key(&self, lifetime: Duration) -> Result { let auth_key = auth::generate(lifetime); + + self.database.add_key_to_keys(&auth_key)?; + self.keys.write().await.insert(auth_key.key.clone(), auth_key.clone()); + Ok(auth_key) + } + + /// It adds a pre-generated authentication key. + /// + /// Authentication keys are used by HTTP trackers. + /// + /// # Context: Authentication + /// + /// # Errors + /// + /// Will return a `database::Error` if unable to add the `auth_key` to the + /// database. For example, if the key already exist. + /// + /// # Arguments + /// + /// * `lifetime` - The duration in seconds for the new key. The key will be + /// no longer valid after `lifetime` seconds. + pub async fn add_auth_key( + &self, + key: Key, + valid_until: DurationSinceUnixEpoch, + ) -> Result { + let auth_key = ExpiringKey { key, valid_until }; + + // code-review: should we return a friendly error instead of the DB + // constrain error when the key already exist? For now, it's returning + // the specif error for each DB driver when a UNIQUE constrain fails. self.database.add_key_to_keys(&auth_key)?; self.keys.write().await.insert(auth_key.key.clone(), auth_key.clone()); Ok(auth_key) @@ -816,10 +848,6 @@ impl Tracker { /// # Errors /// /// Will return a `database::Error` if unable to remove the `key` to the database. - /// - /// # Panics - /// - /// Will panic if key cannot be converted into a valid `Key`. pub async fn remove_auth_key(&self, key: &Key) -> Result<(), databases::error::Error> { self.database.remove_key_from_keys(key)?; self.keys.write().await.remove(key); diff --git a/src/servers/apis/v1/context/auth_key/forms.rs b/src/servers/apis/v1/context/auth_key/forms.rs new file mode 100644 index 000000000..9c023ab72 --- /dev/null +++ b/src/servers/apis/v1/context/auth_key/forms.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct AddKeyForm { + #[serde(rename = "key")] + pub opt_key: Option, + pub seconds_valid: u64, +} diff --git a/src/servers/apis/v1/context/auth_key/handlers.rs b/src/servers/apis/v1/context/auth_key/handlers.rs index 792d9507e..3f85089ec 100644 --- a/src/servers/apis/v1/context/auth_key/handlers.rs +++ b/src/servers/apis/v1/context/auth_key/handlers.rs @@ -3,17 +3,66 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use axum::extract::{Path, State}; +use axum::extract::{self, Path, State}; use axum::response::Response; use serde::Deserialize; +use torrust_tracker_clock::clock::Time; +use super::forms::AddKeyForm; use super::responses::{ - auth_key_response, failed_to_delete_key_response, failed_to_generate_key_response, failed_to_reload_keys_response, + auth_key_response, failed_to_add_key_response, failed_to_delete_key_response, failed_to_generate_key_response, + failed_to_reload_keys_response, invalid_auth_key_duration_response, invalid_auth_key_response, }; use crate::core::auth::Key; use crate::core::Tracker; use crate::servers::apis::v1::context::auth_key::resources::AuthKey; use crate::servers::apis::v1::responses::{invalid_auth_key_param_response, ok_response}; +use crate::CurrentClock; + +/// It handles the request to add a new authentication key. +/// +/// It returns these types of responses: +/// +/// - `200` with a json [`AuthKey`] +/// resource. If the key was generated successfully. +/// - `400` with an error if the key couldn't been added because of an invalid +/// request. +/// - `500` with serialized error in debug format. If the key couldn't be +/// generated. +/// +/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::auth_key#generate-a-new-authentication-key) +/// for more information about this endpoint. +pub async fn add_auth_key_handler( + State(tracker): State>, + extract::Json(add_key_form): extract::Json, +) -> Response { + match add_key_form.opt_key { + Some(pre_existing_key) => { + let Some(valid_until) = CurrentClock::now_add(&Duration::from_secs(add_key_form.seconds_valid)) else { + return invalid_auth_key_duration_response(add_key_form.seconds_valid); + }; + + let key = pre_existing_key.parse::(); + + match key { + Ok(key) => match tracker.add_auth_key(key, valid_until).await { + Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)), + Err(e) => failed_to_add_key_response(e), + }, + Err(e) => invalid_auth_key_response(&pre_existing_key, &e), + } + } + None => { + match tracker + .generate_auth_key(Duration::from_secs(add_key_form.seconds_valid)) + .await + { + Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)), + Err(e) => failed_to_generate_key_response(e), + } + } + } +} /// It handles the request to generate a new authentication key. /// diff --git a/src/servers/apis/v1/context/auth_key/mod.rs b/src/servers/apis/v1/context/auth_key/mod.rs index 330249b58..b00d7a2cb 100644 --- a/src/servers/apis/v1/context/auth_key/mod.rs +++ b/src/servers/apis/v1/context/auth_key/mod.rs @@ -119,6 +119,7 @@ //! "status": "ok" //! } //! ``` +pub mod forms; pub mod handlers; pub mod resources; pub mod responses; diff --git a/src/servers/apis/v1/context/auth_key/responses.rs b/src/servers/apis/v1/context/auth_key/responses.rs index 51be162c5..dfe449b46 100644 --- a/src/servers/apis/v1/context/auth_key/responses.rs +++ b/src/servers/apis/v1/context/auth_key/responses.rs @@ -4,8 +4,9 @@ use std::error::Error; use axum::http::{header, StatusCode}; use axum::response::{IntoResponse, Response}; +use crate::core::auth::ParseKeyError; use crate::servers::apis::v1::context::auth_key::resources::AuthKey; -use crate::servers::apis::v1::responses::unhandled_rejection_response; +use crate::servers::apis::v1::responses::{bad_request_response, unhandled_rejection_response}; /// `200` response that contains the `AuthKey` resource as json. /// @@ -22,12 +23,20 @@ pub fn auth_key_response(auth_key: &AuthKey) -> Response { .into_response() } +// Error responses + /// `500` error response when a new authentication key cannot be generated. #[must_use] pub fn failed_to_generate_key_response(e: E) -> Response { unhandled_rejection_response(format!("failed to generate key: {e}")) } +/// `500` error response when the provide key cannot be added. +#[must_use] +pub fn failed_to_add_key_response(e: E) -> Response { + unhandled_rejection_response(format!("failed to add key: {e}")) +} + /// `500` error response when an authentication key cannot be deleted. #[must_use] pub fn failed_to_delete_key_response(e: E) -> Response { @@ -40,3 +49,13 @@ pub fn failed_to_delete_key_response(e: E) -> Response { pub fn failed_to_reload_keys_response(e: E) -> Response { unhandled_rejection_response(format!("failed to reload keys: {e}")) } + +#[must_use] +pub fn invalid_auth_key_response(auth_key: &str, error: &ParseKeyError) -> Response { + bad_request_response(&format!("Invalid URL: invalid auth key: string \"{auth_key}\", {error}")) +} + +#[must_use] +pub fn invalid_auth_key_duration_response(duration: u64) -> Response { + bad_request_response(&format!("Invalid URL: invalid auth key duration: \"{duration}\"")) +} diff --git a/src/servers/apis/v1/context/auth_key/routes.rs b/src/servers/apis/v1/context/auth_key/routes.rs index 003ee5af4..9452f2c0f 100644 --- a/src/servers/apis/v1/context/auth_key/routes.rs +++ b/src/servers/apis/v1/context/auth_key/routes.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use axum::routing::{get, post}; use axum::Router; -use super::handlers::{delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler}; +use super::handlers::{add_auth_key_handler, delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler}; use crate::core::Tracker; /// It adds the routes to the router for the [`auth_key`](crate::servers::apis::v1::context::auth_key) API context. @@ -30,5 +30,9 @@ pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { .with_state(tracker.clone()), ) // Keys command - .route(&format!("{prefix}/keys/reload"), get(reload_keys_handler).with_state(tracker)) + .route( + &format!("{prefix}/keys/reload"), + get(reload_keys_handler).with_state(tracker.clone()), + ) + .route(&format!("{prefix}/keys"), post(add_auth_key_handler).with_state(tracker)) } diff --git a/src/servers/apis/v1/responses.rs b/src/servers/apis/v1/responses.rs index ecaf90098..d2c52ac40 100644 --- a/src/servers/apis/v1/responses.rs +++ b/src/servers/apis/v1/responses.rs @@ -61,7 +61,8 @@ pub fn invalid_auth_key_param_response(invalid_key: &str) -> Response { bad_request_response(&format!("Invalid auth key id param \"{invalid_key}\"")) } -fn bad_request_response(body: &str) -> Response { +#[must_use] +pub fn bad_request_response(body: &str) -> Response { ( StatusCode::BAD_REQUEST, [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], From 583b305ed32a310f5897be7de943df8e4975e751 Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 30 Jul 2024 10:55:34 +0100 Subject: [PATCH 2/3] test: [#874] new key generation endpoint --- tests/servers/api/v1/asserts.rs | 35 ++- tests/servers/api/v1/client.rs | 28 +- .../api/v1/contract/context/auth_key.rs | 240 +++++++++++++++--- 3 files changed, 267 insertions(+), 36 deletions(-) diff --git a/tests/servers/api/v1/asserts.rs b/tests/servers/api/v1/asserts.rs index 955293db1..ba906f65f 100644 --- a/tests/servers/api/v1/asserts.rs +++ b/tests/servers/api/v1/asserts.rs @@ -61,6 +61,12 @@ pub async fn assert_bad_request(response: Response, body: &str) { assert_eq!(response.text().await.unwrap(), body); } +pub async fn assert_unprocessable_content(response: Response, text: &str) { + assert_eq!(response.status(), 422); + assert_eq!(response.headers().get("content-type").unwrap(), "text/plain; charset=utf-8"); + assert!(response.text().await.unwrap().contains(text)); +} + pub async fn assert_not_found(response: Response) { assert_eq!(response.status(), 404); // todo: missing header in the response @@ -82,10 +88,37 @@ pub async fn assert_invalid_infohash_param(response: Response, invalid_infohash: .await; } -pub async fn assert_invalid_auth_key_param(response: Response, invalid_auth_key: &str) { +pub async fn assert_invalid_auth_key_get_param(response: Response, invalid_auth_key: &str) { assert_bad_request(response, &format!("Invalid auth key id param \"{}\"", &invalid_auth_key)).await; } +pub async fn assert_invalid_auth_key_post_param(response: Response, invalid_auth_key: &str) { + assert_bad_request( + response, + &format!( + "Invalid URL: invalid auth key: string \"{}\", ParseKeyError", + &invalid_auth_key + ), + ) + .await; +} + +pub async fn _assert_unprocessable_auth_key_param(response: Response, _invalid_value: &str) { + assert_unprocessable_content( + response, + "Failed to deserialize the JSON body into the target type: seconds_valid: invalid type", + ) + .await; +} + +pub async fn assert_unprocessable_auth_key_duration_param(response: Response, _invalid_value: &str) { + assert_unprocessable_content( + response, + "Failed to deserialize the JSON body into the target type: seconds_valid: invalid type", + ) + .await; +} + pub async fn assert_invalid_key_duration_param(response: Response, invalid_key_duration: &str) { assert_bad_request( response, diff --git a/tests/servers/api/v1/client.rs b/tests/servers/api/v1/client.rs index 61e98e742..91f18acac 100644 --- a/tests/servers/api/v1/client.rs +++ b/tests/servers/api/v1/client.rs @@ -1,4 +1,5 @@ use reqwest::Response; +use serde::Serialize; use crate::common::http::{Query, QueryParam, ReqwestQuery}; use crate::servers::api::connection_info::ConnectionInfo; @@ -18,7 +19,11 @@ impl Client { } pub async fn generate_auth_key(&self, seconds_valid: i32) -> Response { - self.post(&format!("key/{}", &seconds_valid)).await + self.post_empty(&format!("key/{}", &seconds_valid)).await + } + + pub async fn add_auth_key(&self, add_key_form: AddKeyForm) -> Response { + self.post_form("keys", &add_key_form).await } pub async fn delete_auth_key(&self, key: &str) -> Response { @@ -30,7 +35,7 @@ impl Client { } pub async fn whitelist_a_torrent(&self, info_hash: &str) -> Response { - self.post(&format!("whitelist/{}", &info_hash)).await + self.post_empty(&format!("whitelist/{}", &info_hash)).await } pub async fn remove_torrent_from_whitelist(&self, info_hash: &str) -> Response { @@ -63,10 +68,20 @@ impl Client { self.get_request_with_query(path, query).await } - pub async fn post(&self, path: &str) -> Response { + pub async fn post_empty(&self, path: &str) -> Response { + reqwest::Client::new() + .post(self.base_url(path).clone()) + .query(&ReqwestQuery::from(self.query_with_token())) + .send() + .await + .unwrap() + } + + pub async fn post_form(&self, path: &str, form: &T) -> Response { reqwest::Client::new() .post(self.base_url(path).clone()) .query(&ReqwestQuery::from(self.query_with_token())) + .json(&form) .send() .await .unwrap() @@ -114,3 +129,10 @@ pub async fn get(path: &str, query: Option) -> Response { None => reqwest::Client::builder().build().unwrap().get(path).send().await.unwrap(), } } + +#[derive(Serialize, Debug)] +pub struct AddKeyForm { + #[serde(rename = "key")] + pub opt_key: Option, + pub seconds_valid: u64, +} diff --git a/tests/servers/api/v1/contract/context/auth_key.rs b/tests/servers/api/v1/contract/context/auth_key.rs index f9630bafe..f02267b8b 100644 --- a/tests/servers/api/v1/contract/context/auth_key.rs +++ b/tests/servers/api/v1/contract/context/auth_key.rs @@ -1,23 +1,28 @@ use std::time::Duration; +use serde::Serialize; use torrust_tracker::core::auth::Key; use torrust_tracker_test_helpers::configuration; use crate::servers::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; use crate::servers::api::v1::asserts::{ assert_auth_key_utf8, assert_failed_to_delete_key, assert_failed_to_generate_key, assert_failed_to_reload_keys, - assert_invalid_auth_key_param, assert_invalid_key_duration_param, assert_ok, assert_token_not_valid, assert_unauthorized, + assert_invalid_auth_key_get_param, assert_invalid_auth_key_post_param, assert_ok, assert_token_not_valid, + assert_unauthorized, assert_unprocessable_auth_key_duration_param, }; -use crate::servers::api::v1::client::Client; +use crate::servers::api::v1::client::{AddKeyForm, Client}; use crate::servers::api::{force_database_error, Started}; #[tokio::test] -async fn should_allow_generating_a_new_auth_key() { +async fn should_allow_generating_a_new_random_auth_key() { let env = Started::new(&configuration::ephemeral().into()).await; - let seconds_valid = 60; - - let response = Client::new(env.get_connection_info()).generate_auth_key(seconds_valid).await; + let response = Client::new(env.get_connection_info()) + .add_auth_key(AddKeyForm { + opt_key: None, + seconds_valid: 60, + }) + .await; let auth_key_resource = assert_auth_key_utf8(response).await; @@ -32,43 +37,49 @@ async fn should_allow_generating_a_new_auth_key() { } #[tokio::test] -async fn should_not_allow_generating_a_new_auth_key_for_unauthenticated_users() { +async fn should_allow_uploading_a_preexisting_auth_key() { let env = Started::new(&configuration::ephemeral().into()).await; - let seconds_valid = 60; - - let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str())) - .generate_auth_key(seconds_valid) + let response = Client::new(env.get_connection_info()) + .add_auth_key(AddKeyForm { + opt_key: Some("Xc1L4PbQJSFGlrgSRZl8wxSFAuMa21z5".to_string()), + seconds_valid: 60, + }) .await; - assert_token_not_valid(response).await; - - let response = Client::new(connection_with_no_token(env.get_connection_info().bind_address.as_str())) - .generate_auth_key(seconds_valid) - .await; + let auth_key_resource = assert_auth_key_utf8(response).await; - assert_unauthorized(response).await; + // Verify the key with the tracker + assert!(env + .tracker + .verify_auth_key(&auth_key_resource.key.parse::().unwrap()) + .await + .is_ok()); env.stop().await; } #[tokio::test] -async fn should_fail_generating_a_new_auth_key_when_the_key_duration_is_invalid() { +async fn should_not_allow_generating_a_new_auth_key_for_unauthenticated_users() { let env = Started::new(&configuration::ephemeral().into()).await; - let invalid_key_durations = [ - // "", it returns 404 - // " ", it returns 404 - "-1", "text", - ]; + let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str())) + .add_auth_key(AddKeyForm { + opt_key: None, + seconds_valid: 60, + }) + .await; - for invalid_key_duration in invalid_key_durations { - let response = Client::new(env.get_connection_info()) - .post(&format!("key/{invalid_key_duration}")) - .await; + assert_token_not_valid(response).await; - assert_invalid_key_duration_param(response, invalid_key_duration).await; - } + let response = Client::new(connection_with_no_token(env.get_connection_info().bind_address.as_str())) + .add_auth_key(AddKeyForm { + opt_key: None, + seconds_valid: 60, + }) + .await; + + assert_unauthorized(response).await; env.stop().await; } @@ -79,8 +90,12 @@ async fn should_fail_when_the_auth_key_cannot_be_generated() { force_database_error(&env.tracker); - let seconds_valid = 60; - let response = Client::new(env.get_connection_info()).generate_auth_key(seconds_valid).await; + let response = Client::new(env.get_connection_info()) + .add_auth_key(AddKeyForm { + opt_key: None, + seconds_valid: 60, + }) + .await; assert_failed_to_generate_key(response).await; @@ -107,6 +122,77 @@ async fn should_allow_deleting_an_auth_key() { env.stop().await; } +#[tokio::test] +async fn should_fail_generating_a_new_auth_key_when_the_provided_key_is_invalid() { + #[derive(Serialize, Debug)] + pub struct InvalidAddKeyForm { + #[serde(rename = "key")] + pub opt_key: Option, + pub seconds_valid: u64, + } + + let env = Started::new(&configuration::ephemeral().into()).await; + + let invalid_keys = [ + // "", it returns 404 + // " ", it returns 404 + "-1", // Not a string + "invalid", // Invalid string + "GQEs2ZNcCm9cwEV9dBpcPB5OwNFWFiR", // Not a 32-char string + // "%QEs2ZNcCm9cwEV9dBpcPB5OwNFWFiRd", // Invalid char. todo: this doesn't fail + ]; + + for invalid_key in invalid_keys { + let response = Client::new(env.get_connection_info()) + .post_form( + "keys", + &InvalidAddKeyForm { + opt_key: Some(invalid_key.to_string()), + seconds_valid: 60, + }, + ) + .await; + + assert_invalid_auth_key_post_param(response, invalid_key).await; + } + + env.stop().await; +} + +#[tokio::test] +async fn should_fail_generating_a_new_auth_key_when_the_key_duration_is_invalid() { + #[derive(Serialize, Debug)] + pub struct InvalidAddKeyForm { + #[serde(rename = "key")] + pub opt_key: Option, + pub seconds_valid: String, + } + + let env = Started::new(&configuration::ephemeral().into()).await; + + let invalid_key_durations = [ + // "", it returns 404 + // " ", it returns 404 + "-1", "text", + ]; + + for invalid_key_duration in invalid_key_durations { + let response = Client::new(env.get_connection_info()) + .post_form( + "keys", + &InvalidAddKeyForm { + opt_key: None, + seconds_valid: invalid_key_duration.to_string(), + }, + ) + .await; + + assert_unprocessable_auth_key_duration_param(response, invalid_key_duration).await; + } + + env.stop().await; +} + #[tokio::test] async fn should_fail_deleting_an_auth_key_when_the_key_id_is_invalid() { let env = Started::new(&configuration::ephemeral().into()).await; @@ -124,7 +210,7 @@ async fn should_fail_deleting_an_auth_key_when_the_key_id_is_invalid() { for invalid_auth_key in &invalid_auth_keys { let response = Client::new(env.get_connection_info()).delete_auth_key(invalid_auth_key).await; - assert_invalid_auth_key_param(response, invalid_auth_key).await; + assert_invalid_auth_key_get_param(response, invalid_auth_key).await; } env.stop().await; @@ -247,3 +333,93 @@ async fn should_not_allow_reloading_keys_for_unauthenticated_users() { env.stop().await; } + +mod deprecated_generate_key_endpoint { + + use torrust_tracker::core::auth::Key; + use torrust_tracker_test_helpers::configuration; + + use crate::servers::api::connection_info::{connection_with_invalid_token, connection_with_no_token}; + use crate::servers::api::v1::asserts::{ + assert_auth_key_utf8, assert_failed_to_generate_key, assert_invalid_key_duration_param, assert_token_not_valid, + assert_unauthorized, + }; + use crate::servers::api::v1::client::Client; + use crate::servers::api::{force_database_error, Started}; + + #[tokio::test] + async fn should_allow_generating_a_new_auth_key() { + let env = Started::new(&configuration::ephemeral().into()).await; + + let seconds_valid = 60; + + let response = Client::new(env.get_connection_info()).generate_auth_key(seconds_valid).await; + + let auth_key_resource = assert_auth_key_utf8(response).await; + + // Verify the key with the tracker + assert!(env + .tracker + .verify_auth_key(&auth_key_resource.key.parse::().unwrap()) + .await + .is_ok()); + + env.stop().await; + } + + #[tokio::test] + async fn should_not_allow_generating_a_new_auth_key_for_unauthenticated_users() { + let env = Started::new(&configuration::ephemeral().into()).await; + + let seconds_valid = 60; + + let response = Client::new(connection_with_invalid_token(env.get_connection_info().bind_address.as_str())) + .generate_auth_key(seconds_valid) + .await; + + assert_token_not_valid(response).await; + + let response = Client::new(connection_with_no_token(env.get_connection_info().bind_address.as_str())) + .generate_auth_key(seconds_valid) + .await; + + assert_unauthorized(response).await; + + env.stop().await; + } + + #[tokio::test] + async fn should_fail_generating_a_new_auth_key_when_the_key_duration_is_invalid() { + let env = Started::new(&configuration::ephemeral().into()).await; + + let invalid_key_durations = [ + // "", it returns 404 + // " ", it returns 404 + "-1", "text", + ]; + + for invalid_key_duration in invalid_key_durations { + let response = Client::new(env.get_connection_info()) + .post_empty(&format!("key/{invalid_key_duration}")) + .await; + + assert_invalid_key_duration_param(response, invalid_key_duration).await; + } + + env.stop().await; + } + + #[tokio::test] + async fn should_fail_when_the_auth_key_cannot_be_generated() { + let env = Started::new(&configuration::ephemeral().into()).await; + + force_database_error(&env.tracker); + + let seconds_valid = 60; + let response = Client::new(env.get_connection_info()).generate_auth_key(seconds_valid).await; + + assert_failed_to_generate_key(response).await; + + env.stop().await; + } +} From 04f50e454e6e6c0c047a798af410ff3b19ad228d Mon Sep 17 00:00:00 2001 From: Jose Celano Date: Tue, 30 Jul 2024 11:22:30 +0100 Subject: [PATCH 3/3] docs: [#974] update add key endpoint doc --- .../apis/v1/context/auth_key/handlers.rs | 2 ++ src/servers/apis/v1/context/auth_key/mod.rs | 21 +++++++++++++------ .../apis/v1/context/auth_key/routes.rs | 8 +++++-- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/servers/apis/v1/context/auth_key/handlers.rs b/src/servers/apis/v1/context/auth_key/handlers.rs index 3f85089ec..6d2d99150 100644 --- a/src/servers/apis/v1/context/auth_key/handlers.rs +++ b/src/servers/apis/v1/context/auth_key/handlers.rs @@ -75,6 +75,8 @@ pub async fn add_auth_key_handler( /// /// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::auth_key#generate-a-new-authentication-key) /// for more information about this endpoint. +/// +/// This endpoint has been deprecated. Use [`add_auth_key_handler`]. pub async fn generate_auth_key_handler(State(tracker): State>, Path(seconds_valid_or_key): Path) -> Response { let seconds_valid = seconds_valid_or_key; match tracker.generate_auth_key(Duration::from_secs(seconds_valid)).await { diff --git a/src/servers/apis/v1/context/auth_key/mod.rs b/src/servers/apis/v1/context/auth_key/mod.rs index b00d7a2cb..f6762b26e 100644 --- a/src/servers/apis/v1/context/auth_key/mod.rs +++ b/src/servers/apis/v1/context/auth_key/mod.rs @@ -3,8 +3,8 @@ //! Authentication keys are used to authenticate HTTP tracker `announce` and //! `scrape` requests. //! -//! When the tracker is running in `private` or `private_listed` mode, the -//! authentication keys are required to announce and scrape torrents. +//! When the tracker is running in `private` mode, the authentication keys are +//! required to announce and scrape torrents. //! //! A sample `announce` request **without** authentication key: //! @@ -22,22 +22,31 @@ //! //! # Generate a new authentication key //! -//! `POST /key/:seconds_valid` +//! `POST /keys` //! -//! It generates a new authentication key. +//! It generates a new authentication key or upload a pre-generated key. //! //! > **NOTICE**: keys expire after a certain amount of time. //! -//! **Path parameters** +//! **POST parameters** //! //! Name | Type | Description | Required | Example //! ---|---|---|---|--- +//! `key` | 32-char string (0-9, a-z, A-Z) | The optional pre-generated key. | No | `Xc1L4PbQJSFGlrgSRZl8wxSFAuMa21z7` //! `seconds_valid` | positive integer | The number of seconds the key will be valid. | Yes | `3600` //! +//! > **NOTICE**: the `key` field is optional. If is not provided the tracker +//! > will generated a random one. +//! //! **Example request** //! //! ```bash -//! curl -X POST "http://127.0.0.1:1212/api/v1/key/120?token=MyAccessToken" +//! curl -X POST http://localhost:1212/api/v1/keys?token=MyAccessToken \ +//! -H "Content-Type: application/json" \ +//! -d '{ +//! "key": "xqD6NWH9TcKrOCwDmqcdH5hF5RrbL0A6", +//! "seconds_valid": 7200 +//! }' //! ``` //! //! **Example response** `200` diff --git a/src/servers/apis/v1/context/auth_key/routes.rs b/src/servers/apis/v1/context/auth_key/routes.rs index 9452f2c0f..60ccd77ab 100644 --- a/src/servers/apis/v1/context/auth_key/routes.rs +++ b/src/servers/apis/v1/context/auth_key/routes.rs @@ -21,8 +21,12 @@ pub fn add(prefix: &str, router: Router, tracker: Arc) -> Router { .route( // code-review: Axum does not allow two routes with the same path but different path variable name. // In the new major API version, `seconds_valid` should be a POST form field so that we will have two paths: - // POST /key - // DELETE /key/:key + // + // POST /keys + // DELETE /keys/:key + // + // The POST /key/:seconds_valid has been deprecated and it will removed in the future. + // Use POST /keys &format!("{prefix}/key/:seconds_valid_or_key"), post(generate_auth_key_handler) .with_state(tracker.clone())