torrust_tracker/servers/http/v1/extractors/
authentication_key.rs1use std::panic::Location;
46
47use axum::extract::rejection::PathRejection;
48use axum::extract::{FromRequestParts, Path};
49use axum::http::request::Parts;
50use axum::response::{IntoResponse, Response};
51use futures::future::BoxFuture;
52use futures::FutureExt;
53use serde::Deserialize;
54
55use crate::core::auth::Key;
56use crate::servers::http::v1::handlers::common::auth;
57use crate::servers::http::v1::responses;
58
59pub struct Extract(pub Key);
61
62#[derive(Deserialize)]
63pub struct KeyParam(String);
64
65impl KeyParam {
66 #[must_use]
67 pub fn value(&self) -> String {
68 self.0.clone()
69 }
70}
71
72impl<S> FromRequestParts<S> for Extract
73where
74 S: Send + Sync,
75{
76 type Rejection = Response;
77
78 #[must_use]
79 fn from_request_parts<'life0, 'life1, 'async_trait>(
80 parts: &'life0 mut Parts,
81 state: &'life1 S,
82 ) -> BoxFuture<'async_trait, Result<Self, Self::Rejection>>
83 where
84 'life0: 'async_trait,
85 'life1: 'async_trait,
86 Self: 'async_trait,
87 {
88 async {
89 let maybe_path_with_key = Path::<KeyParam>::from_request_parts(parts, state).await;
91
92 match extract_key(maybe_path_with_key) {
93 Ok(key) => Ok(Extract(key)),
94 Err(error) => Err(error.into_response()),
95 }
96 }
97 .boxed()
98 }
99}
100
101fn extract_key(path_extractor_result: Result<Path<KeyParam>, PathRejection>) -> Result<Key, responses::error::Error> {
102 match path_extractor_result {
103 Ok(key_param) => match parse_key(&key_param.0.value()) {
104 Ok(key) => Ok(key),
105 Err(error) => Err(error),
106 },
107 Err(path_rejection) => Err(custom_error(&path_rejection)),
108 }
109}
110
111fn parse_key(key: &str) -> Result<Key, responses::error::Error> {
112 let key = key.parse::<Key>();
113
114 match key {
115 Ok(key) => Ok(key),
116 Err(_parse_key_error) => Err(responses::error::Error::from(auth::Error::InvalidKeyFormat {
117 location: Location::caller(),
118 })),
119 }
120}
121
122fn custom_error(rejection: &PathRejection) -> responses::error::Error {
123 match rejection {
124 axum::extract::rejection::PathRejection::FailedToDeserializePathParams(_) => {
125 responses::error::Error::from(auth::Error::InvalidKeyFormat {
126 location: Location::caller(),
127 })
128 }
129 axum::extract::rejection::PathRejection::MissingPathParams(_) => {
130 responses::error::Error::from(auth::Error::MissingAuthKey {
131 location: Location::caller(),
132 })
133 }
134 _ => responses::error::Error::from(auth::Error::CannotExtractKeyParam {
135 location: Location::caller(),
136 }),
137 }
138}
139
140#[cfg(test)]
141mod tests {
142
143 use super::parse_key;
144 use crate::servers::http::v1::responses::error::Error;
145
146 fn assert_error_response(error: &Error, error_message: &str) {
147 assert!(
148 error.failure_reason.contains(error_message),
149 "Error response does not contain message: '{error_message}'. Error: {error:?}"
150 );
151 }
152
153 #[test]
154 fn it_should_return_an_authentication_error_if_the_key_cannot_be_parsed() {
155 let invalid_key = "invalid_key";
156
157 let response = parse_key(invalid_key).unwrap_err();
158
159 assert_error_response(&response, "Authentication error: Invalid format for authentication key param");
160 }
161}