Skip to content

Commit

Permalink
feat: [#974] new API endpoint to upload pre-existing keys
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
josecelano committed Jul 29, 2024
1 parent a964659 commit 09beb52
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 12 deletions.
2 changes: 1 addition & 1 deletion src/core/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]

Check warning on line 155 in src/core/auth.rs

View check run for this annotation

Codecov / codecov/patch

src/core/auth.rs#L155

Added line #L155 was not covered by tests
pub struct ParseKeyError;

impl FromStr for Key {
Expand Down
38 changes: 33 additions & 5 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -453,16 +453,17 @@ 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;
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;
Expand Down Expand Up @@ -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<auth::ExpiringKey, databases::error::Error> {
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<auth::ExpiringKey, databases::error::Error> {
let auth_key = ExpiringKey { key, valid_until };

Check warning on line 834 in src/core/mod.rs

View check run for this annotation

Codecov / codecov/patch

src/core/mod.rs#L829-L834

Added lines #L829 - L834 were not covered by tests

// 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)
Expand All @@ -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);
Expand Down
8 changes: 8 additions & 0 deletions src/servers/apis/v1/context/auth_key/forms.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]

Check warning on line 3 in src/servers/apis/v1/context/auth_key/forms.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/apis/v1/context/auth_key/forms.rs#L3

Added line #L3 was not covered by tests
pub struct AddKeyForm {
#[serde(rename = "key")]
pub opt_key: Option<String>,
pub seconds_valid: u64,

Check warning on line 7 in src/servers/apis/v1/context/auth_key/forms.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/apis/v1/context/auth_key/forms.rs#L7

Added line #L7 was not covered by tests
}
53 changes: 51 additions & 2 deletions src/servers/apis/v1/context/auth_key/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<Tracker>>,
extract::Json(add_key_form): extract::Json<AddKeyForm>,
) -> 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);

Check warning on line 42 in src/servers/apis/v1/context/auth_key/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/apis/v1/context/auth_key/handlers.rs#L35-L42

Added lines #L35 - L42 were not covered by tests
};

let key = pre_existing_key.parse::<Key>();

Check warning on line 45 in src/servers/apis/v1/context/auth_key/handlers.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L45 was not covered by tests

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),

Check warning on line 50 in src/servers/apis/v1/context/auth_key/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/apis/v1/context/auth_key/handlers.rs#L47-L50

Added lines #L47 - L50 were not covered by tests
},
Err(e) => invalid_auth_key_response(&pre_existing_key, &e),

Check warning on line 52 in src/servers/apis/v1/context/auth_key/handlers.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L52 was not covered by tests
}
}

Check warning on line 54 in src/servers/apis/v1/context/auth_key/handlers.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L54 was not covered by tests
None => {
match tracker
.generate_auth_key(Duration::from_secs(add_key_form.seconds_valid))
.await

Check warning on line 58 in src/servers/apis/v1/context/auth_key/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/apis/v1/context/auth_key/handlers.rs#L56-L58

Added lines #L56 - L58 were not covered by tests
{
Ok(auth_key) => auth_key_response(&AuthKey::from(auth_key)),
Err(e) => failed_to_generate_key_response(e),

Check warning on line 61 in src/servers/apis/v1/context/auth_key/handlers.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/apis/v1/context/auth_key/handlers.rs#L60-L61

Added lines #L60 - L61 were not covered by tests
}
}
}
}

Check warning on line 65 in src/servers/apis/v1/context/auth_key/handlers.rs

View check run for this annotation

Codecov / codecov/patch

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

Added line #L65 was not covered by tests

/// It handles the request to generate a new authentication key.
///
Expand Down
1 change: 1 addition & 0 deletions src/servers/apis/v1/context/auth_key/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
//! "status": "ok"
//! }
//! ```
pub mod forms;
pub mod handlers;
pub mod resources;
pub mod responses;
Expand Down
21 changes: 20 additions & 1 deletion src/servers/apis/v1/context/auth_key/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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: Error>(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: Error>(e: E) -> Response {
unhandled_rejection_response(format!("failed to add key: {e}"))
}

Check warning on line 38 in src/servers/apis/v1/context/auth_key/responses.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/apis/v1/context/auth_key/responses.rs#L36-L38

Added lines #L36 - L38 were not covered by tests

/// `500` error response when an authentication key cannot be deleted.
#[must_use]
pub fn failed_to_delete_key_response<E: Error>(e: E) -> Response {
Expand All @@ -40,3 +49,13 @@ pub fn failed_to_delete_key_response<E: Error>(e: E) -> Response {
pub fn failed_to_reload_keys_response<E: Error>(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}"))
}

Check warning on line 56 in src/servers/apis/v1/context/auth_key/responses.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/apis/v1/context/auth_key/responses.rs#L54-L56

Added lines #L54 - L56 were not covered by tests

#[must_use]
pub fn invalid_auth_key_duration_response(duration: u64) -> Response {
bad_request_response(&format!("Invalid URL: invalid auth key duration: \"{duration}\""))
}

Check warning on line 61 in src/servers/apis/v1/context/auth_key/responses.rs

View check run for this annotation

Codecov / codecov/patch

src/servers/apis/v1/context/auth_key/responses.rs#L59-L61

Added lines #L59 - L61 were not covered by tests
8 changes: 6 additions & 2 deletions src/servers/apis/v1/context/auth_key/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -30,5 +30,9 @@ pub fn add(prefix: &str, router: Router, tracker: Arc<Tracker>) -> 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))
}
3 changes: 2 additions & 1 deletion src/servers/apis/v1/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")],
Expand Down

0 comments on commit 09beb52

Please sign in to comment.