use crate::db::{queries::api_tokens as api_tokens_queries, DbConnectionPool};
use crate::v0::{context::SharedContext, errors::HandleError};
use warp::{Filter, Rejection};
pub const API_TOKEN_HEADER: &str = "API-Token";
#[derive(PartialEq, Eq, Debug, Clone)]
pub struct ApiToken(Vec<u8>);
pub struct ApiTokenManager {
connection_pool: DbConnectionPool,
}
impl From<&[u8]> for ApiToken {
fn from(data: &[u8]) -> Self {
Self(data.to_vec())
}
}
impl AsRef<[u8]> for ApiToken {
fn as_ref(&self) -> &[u8] {
self.0.as_slice()
}
}
impl ApiToken {
pub fn new(data: Vec<u8>) -> Self {
Self(data)
}
}
impl ApiTokenManager {
fn new(connection_pool: DbConnectionPool) -> Self {
Self { connection_pool }
}
async fn is_token_valid(&self, token: ApiToken) -> Result<bool, HandleError> {
match api_tokens_queries::query_token(token, &self.connection_pool).await {
Ok(Some(_)) => Ok(true),
Ok(None) => Ok(false),
Err(e) => Err(HandleError::InternalError(format!(
"Error retrieving token: {}",
e
))),
}
}
#[allow(dead_code)]
async fn revoke_token(&self, _token: ApiToken) -> Result<(), ()> {
Ok(())
}
}
async fn authorize_token(token: String, context: SharedContext) -> Result<(), Rejection> {
let manager = ApiTokenManager::new(context.read().await.db_connection_pool.clone());
let mut token_vec: Vec<u8> = Vec::new();
base64::decode_config_buf(token.clone(), base64::URL_SAFE, &mut token_vec).map_err(|_err| {
warp::reject::custom(HandleError::InvalidHeader(
API_TOKEN_HEADER,
"header should be base64 url safe decodable",
))
})?;
let api_token = ApiToken(token_vec);
match manager.is_token_valid(api_token).await {
Ok(true) => Ok(()),
Ok(false) => {
tracing::event!(
tracing::Level::INFO,
"Unauthorized token received: {}",
token
);
Err(warp::reject::custom(HandleError::UnauthorizedToken))
}
Err(e) => Err(warp::reject::custom(e)),
}
}
pub async fn api_token_filter(
context: SharedContext,
) -> impl Filter<Extract = (), Error = Rejection> + Clone {
let with_context = warp::any().map(move || context.clone());
warp::header::header(API_TOKEN_HEADER)
.and(with_context)
.and_then(authorize_token)
.and(warp::any())
.untuple_one()
}
#[cfg(test)]
mod test {
use crate::v0::api_token::{api_token_filter, API_TOKEN_HEADER};
use crate::v0::context::test::new_test_shared_context_from_url;
use vit_servicing_station_tests::common::startup::db::DbBuilder;
#[tokio::test]
async fn api_token_filter_reject() {
let db_url = DbBuilder::new().build_async().await.unwrap();
let shared_context = new_test_shared_context_from_url(&db_url);
let filter = api_token_filter(shared_context).await;
assert!(warp::test::request()
.header(API_TOKEN_HEADER, "foobar")
.filter(&filter)
.await
.is_err());
}
}