Skip to content

Commit 09beb52

Browse files
committed
feat: [torrust#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.
1 parent a964659 commit 09beb52

File tree

8 files changed

+122
-12
lines changed

8 files changed

+122
-12
lines changed

src/core/auth.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ pub struct Key(String);
152152
/// ```
153153
///
154154
/// If the string does not contains a valid key, the parser function will return this error.
155-
#[derive(Debug, PartialEq, Eq)]
155+
#[derive(Debug, PartialEq, Eq, Display)]
156156
pub struct ParseKeyError;
157157

158158
impl FromStr for Key {

src/core/mod.rs

+33-5
Original file line numberDiff line numberDiff line change
@@ -453,16 +453,17 @@ use std::panic::Location;
453453
use std::sync::Arc;
454454
use std::time::Duration;
455455

456+
use auth::ExpiringKey;
456457
use databases::driver::Driver;
457458
use derive_more::Constructor;
458459
use tokio::sync::mpsc::error::SendError;
459460
use torrust_tracker_clock::clock::Time;
460461
use torrust_tracker_configuration::v2::database;
461462
use torrust_tracker_configuration::{AnnouncePolicy, Core, TORRENT_PEERS_LIMIT};
462463
use torrust_tracker_primitives::info_hash::InfoHash;
463-
use torrust_tracker_primitives::peer;
464464
use torrust_tracker_primitives::swarm_metadata::SwarmMetadata;
465465
use torrust_tracker_primitives::torrent_metrics::TorrentsMetrics;
466+
use torrust_tracker_primitives::{peer, DurationSinceUnixEpoch};
466467
use torrust_tracker_torrent_repository::entry::EntrySync;
467468
use torrust_tracker_torrent_repository::repository::Repository;
468469
use tracing::debug;
@@ -804,6 +805,37 @@ impl Tracker {
804805
/// Will return a `database::Error` if unable to add the `auth_key` to the database.
805806
pub async fn generate_auth_key(&self, lifetime: Duration) -> Result<auth::ExpiringKey, databases::error::Error> {
806807
let auth_key = auth::generate(lifetime);
808+
809+
self.database.add_key_to_keys(&auth_key)?;
810+
self.keys.write().await.insert(auth_key.key.clone(), auth_key.clone());
811+
Ok(auth_key)
812+
}
813+
814+
/// It adds a pre-generated authentication key.
815+
///
816+
/// Authentication keys are used by HTTP trackers.
817+
///
818+
/// # Context: Authentication
819+
///
820+
/// # Errors
821+
///
822+
/// Will return a `database::Error` if unable to add the `auth_key` to the
823+
/// database. For example, if the key already exist.
824+
///
825+
/// # Arguments
826+
///
827+
/// * `lifetime` - The duration in seconds for the new key. The key will be
828+
/// no longer valid after `lifetime` seconds.
829+
pub async fn add_auth_key(
830+
&self,
831+
key: Key,
832+
valid_until: DurationSinceUnixEpoch,
833+
) -> Result<auth::ExpiringKey, databases::error::Error> {
834+
let auth_key = ExpiringKey { key, valid_until };
835+
836+
// code-review: should we return a friendly error instead of the DB
837+
// constrain error when the key already exist? For now, it's returning
838+
// the specif error for each DB driver when a UNIQUE constrain fails.
807839
self.database.add_key_to_keys(&auth_key)?;
808840
self.keys.write().await.insert(auth_key.key.clone(), auth_key.clone());
809841
Ok(auth_key)
@@ -816,10 +848,6 @@ impl Tracker {
816848
/// # Errors
817849
///
818850
/// Will return a `database::Error` if unable to remove the `key` to the database.
819-
///
820-
/// # Panics
821-
///
822-
/// Will panic if key cannot be converted into a valid `Key`.
823851
pub async fn remove_auth_key(&self, key: &Key) -> Result<(), databases::error::Error> {
824852
self.database.remove_key_from_keys(key)?;
825853
self.keys.write().await.remove(key);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
use serde::{Deserialize, Serialize};
2+
3+
#[derive(Serialize, Deserialize, Debug)]
4+
pub struct AddKeyForm {
5+
#[serde(rename = "key")]
6+
pub opt_key: Option<String>,
7+
pub seconds_valid: u64,
8+
}

src/servers/apis/v1/context/auth_key/handlers.rs

+51-2
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,66 @@ use std::str::FromStr;
33
use std::sync::Arc;
44
use std::time::Duration;
55

6-
use axum::extract::{Path, State};
6+
use axum::extract::{self, Path, State};
77
use axum::response::Response;
88
use serde::Deserialize;
9+
use torrust_tracker_clock::clock::Time;
910

11+
use super::forms::AddKeyForm;
1012
use super::responses::{
11-
auth_key_response, failed_to_delete_key_response, failed_to_generate_key_response, failed_to_reload_keys_response,
13+
auth_key_response, failed_to_add_key_response, failed_to_delete_key_response, failed_to_generate_key_response,
14+
failed_to_reload_keys_response, invalid_auth_key_duration_response, invalid_auth_key_response,
1215
};
1316
use crate::core::auth::Key;
1417
use crate::core::Tracker;
1518
use crate::servers::apis::v1::context::auth_key::resources::AuthKey;
1619
use crate::servers::apis::v1::responses::{invalid_auth_key_param_response, ok_response};
20+
use crate::CurrentClock;
21+
22+
/// It handles the request to add a new authentication key.
23+
///
24+
/// It returns these types of responses:
25+
///
26+
/// - `200` with a json [`AuthKey`]
27+
/// resource. If the key was generated successfully.
28+
/// - `400` with an error if the key couldn't been added because of an invalid
29+
/// request.
30+
/// - `500` with serialized error in debug format. If the key couldn't be
31+
/// generated.
32+
///
33+
/// Refer to the [API endpoint documentation](crate::servers::apis::v1::context::auth_key#generate-a-new-authentication-key)
34+
/// for more information about this endpoint.
35+
pub async fn add_auth_key_handler(
36+
State(tracker): State<Arc<Tracker>>,
37+
extract::Json(add_key_form): extract::Json<AddKeyForm>,
38+
) -> Response {
39+
match add_key_form.opt_key {
40+
Some(pre_existing_key) => {
41+
let Some(valid_until) = CurrentClock::now_add(&Duration::from_secs(add_key_form.seconds_valid)) else {
42+
return invalid_auth_key_duration_response(add_key_form.seconds_valid);
43+
};
44+
45+
let key = pre_existing_key.parse::<Key>();
46+
47+
match key {
48+
Ok(key) => match tracker.add_auth_key(key, valid_until).await {
49+
Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)),
50+
Err(e) => failed_to_add_key_response(e),
51+
},
52+
Err(e) => invalid_auth_key_response(&pre_existing_key, &e),
53+
}
54+
}
55+
None => {
56+
match tracker
57+
.generate_auth_key(Duration::from_secs(add_key_form.seconds_valid))
58+
.await
59+
{
60+
Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)),
61+
Err(e) => failed_to_generate_key_response(e),
62+
}
63+
}
64+
}
65+
}
1766

1867
/// It handles the request to generate a new authentication key.
1968
///

src/servers/apis/v1/context/auth_key/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
//! "status": "ok"
120120
//! }
121121
//! ```
122+
pub mod forms;
122123
pub mod handlers;
123124
pub mod resources;
124125
pub mod responses;

src/servers/apis/v1/context/auth_key/responses.rs

+20-1
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ use std::error::Error;
44
use axum::http::{header, StatusCode};
55
use axum::response::{IntoResponse, Response};
66

7+
use crate::core::auth::ParseKeyError;
78
use crate::servers::apis::v1::context::auth_key::resources::AuthKey;
8-
use crate::servers::apis::v1::responses::unhandled_rejection_response;
9+
use crate::servers::apis::v1::responses::{bad_request_response, unhandled_rejection_response};
910

1011
/// `200` response that contains the `AuthKey` resource as json.
1112
///
@@ -22,12 +23,20 @@ pub fn auth_key_response(auth_key: &AuthKey) -> Response {
2223
.into_response()
2324
}
2425

26+
// Error responses
27+
2528
/// `500` error response when a new authentication key cannot be generated.
2629
#[must_use]
2730
pub fn failed_to_generate_key_response<E: Error>(e: E) -> Response {
2831
unhandled_rejection_response(format!("failed to generate key: {e}"))
2932
}
3033

34+
/// `500` error response when the provide key cannot be added.
35+
#[must_use]
36+
pub fn failed_to_add_key_response<E: Error>(e: E) -> Response {
37+
unhandled_rejection_response(format!("failed to add key: {e}"))
38+
}
39+
3140
/// `500` error response when an authentication key cannot be deleted.
3241
#[must_use]
3342
pub fn failed_to_delete_key_response<E: Error>(e: E) -> Response {
@@ -40,3 +49,13 @@ pub fn failed_to_delete_key_response<E: Error>(e: E) -> Response {
4049
pub fn failed_to_reload_keys_response<E: Error>(e: E) -> Response {
4150
unhandled_rejection_response(format!("failed to reload keys: {e}"))
4251
}
52+
53+
#[must_use]
54+
pub fn invalid_auth_key_response(auth_key: &str, error: &ParseKeyError) -> Response {
55+
bad_request_response(&format!("Invalid URL: invalid auth key: string \"{auth_key}\", {error}"))
56+
}
57+
58+
#[must_use]
59+
pub fn invalid_auth_key_duration_response(duration: u64) -> Response {
60+
bad_request_response(&format!("Invalid URL: invalid auth key duration: \"{duration}\""))
61+
}

src/servers/apis/v1/context/auth_key/routes.rs

+6-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::sync::Arc;
1111
use axum::routing::{get, post};
1212
use axum::Router;
1313

14-
use super::handlers::{delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler};
14+
use super::handlers::{add_auth_key_handler, delete_auth_key_handler, generate_auth_key_handler, reload_keys_handler};
1515
use crate::core::Tracker;
1616

1717
/// 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<Tracker>) -> Router {
3030
.with_state(tracker.clone()),
3131
)
3232
// Keys command
33-
.route(&format!("{prefix}/keys/reload"), get(reload_keys_handler).with_state(tracker))
33+
.route(
34+
&format!("{prefix}/keys/reload"),
35+
get(reload_keys_handler).with_state(tracker.clone()),
36+
)
37+
.route(&format!("{prefix}/keys"), post(add_auth_key_handler).with_state(tracker))
3438
}

src/servers/apis/v1/responses.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ pub fn invalid_auth_key_param_response(invalid_key: &str) -> Response {
6161
bad_request_response(&format!("Invalid auth key id param \"{invalid_key}\""))
6262
}
6363

64-
fn bad_request_response(body: &str) -> Response {
64+
#[must_use]
65+
pub fn bad_request_response(body: &str) -> Response {
6566
(
6667
StatusCode::BAD_REQUEST,
6768
[(header::CONTENT_TYPE, "text/plain; charset=utf-8")],

0 commit comments

Comments
 (0)