use log::{error,info}; use std::future::Future; use std::pin::Pin; use url::Url; use tide::prelude::*; use tide::{Request, Response, Next, Result}; use crate::database; use crate::ApplicationState; #[derive(Deserialize, Serialize, Debug, PartialEq)] pub struct User { pub me: Url, pub client_id: Url, scope: String } impl User { pub fn check_scope(&self, scope: &str) -> bool { self.scopes().any(|i| i == scope) } pub fn scopes(&self) -> std::str::SplitAsciiWhitespace<'_> { self.scope.split_ascii_whitespace() } #[cfg(test)] pub fn new(me: &str, client_id: &str, scope: &str) -> Self { Self { me: Url::parse(me).unwrap(), client_id: Url::parse(client_id).unwrap(), scope: scope.to_string() } } } async fn get_token_data(token: String, token_endpoint: &http_types::Url, http_client: &surf::Client) -> (http_types::StatusCode, Option) { match http_client.get(token_endpoint).header("Authorization", token).header("Accept", "application/json").send().await { Ok(mut resp) => { if resp.status() == 200 { match resp.body_json::().await { Ok(user) => { info!("Token endpoint request successful. Validated user: {}", user.me); (resp.status(), Some(user)) }, Err(err) => { error!("Token endpoint parsing error (HTTP status {}): {}", resp.status(), err); (http_types::StatusCode::InternalServerError, None) } } } else { error!("Token endpoint returned non-200: {}", resp.status()); (resp.status(), None) } } Err(err) => { error!("Token endpoint connection error: {}", err); (http_types::StatusCode::InternalServerError, None) } } } // TODO: Figure out how to cache these authorization values - they can potentially take a lot of processing time pub fn check_auth<'a, Backend>(mut req: Request>, next: Next<'a, ApplicationState>) -> Pin + Send + 'a>> where Backend: database::Storage + Send + Sync + Clone { Box::pin(async { let header = req.header("Authorization"); match header { None => { Ok(Response::builder(401).body(json!({ "error": "unauthorized", "error_description": "Please provide an access token." })).build()) }, Some(value) => { let endpoint = &req.state().token_endpoint; let http_client = &req.state().http_client; match get_token_data(value.last().to_string(), endpoint, http_client).await { (http_types::StatusCode::Ok, Some(user)) => { req.set_ext(user); Ok(next.run(req).await) }, (http_types::StatusCode::InternalServerError, None) => { Ok(Response::builder(500).body(json!({ "error": "token_endpoint_fail", "error_description": "Token endpoint made a boo-boo and refused to answer." })).build()) }, (_, None) => { Ok(Response::builder(401).body(json!({ "error": "unauthorized", "error_description": "The token endpoint refused to accept your token." })).build()) }, (_, Some(_)) => { // This shouldn't happen. panic!("The token validation function has caught rabies and returns malformed responses. Aborting."); } } } } }) } #[cfg(test)] mod tests { use super::*; #[test] fn user_scopes_are_checkable() { let user = User::new("https://fireburn.ru/", "https://quill.p3k.io/", "create update media"); assert!(user.check_scope("create")); assert!(!user.check_scope("delete")); } }