From 6e20a3c51756c2e84290da6ec53b89a5fc58c0fc Mon Sep 17 00:00:00 2001 From: Vika Date: Wed, 28 Sep 2022 03:55:48 +0300 Subject: Use tokens from the auth backend to authenticate for Micropub --- kittybox-rs/src/micropub/mod.rs | 148 ++++++++++++++++++++++------------------ 1 file changed, 82 insertions(+), 66 deletions(-) (limited to 'kittybox-rs/src/micropub/mod.rs') diff --git a/kittybox-rs/src/micropub/mod.rs b/kittybox-rs/src/micropub/mod.rs index d0aeae0..0f7441e 100644 --- a/kittybox-rs/src/micropub/mod.rs +++ b/kittybox-rs/src/micropub/mod.rs @@ -1,16 +1,16 @@ use crate::database::{MicropubChannel, Storage, StorageError}; -use crate::tokenauth::User; +use crate::indieauth::backend::AuthBackend; +use crate::indieauth::User; use crate::micropub::util::form_to_mf2_json; -use axum::extract::{BodyStream, Query}; +use axum::extract::{BodyStream, Query, Host}; use axum::headers::ContentType; use axum::response::{IntoResponse, Response}; use axum::TypedHeader; use axum::{http::StatusCode, Extension}; use serde::{Deserialize, Serialize}; use serde_json::json; -use std::fmt::Display; use tracing::{debug, error, info, warn}; - +use kittybox_indieauth::{Scope, TokenData}; use kittybox_util::{MicropubError, ErrorType}; #[derive(Serialize, Deserialize, Debug, PartialEq)] @@ -65,8 +65,7 @@ fn populate_reply_context( .iter() .find(|ctx| Some(ctx.url.as_str()) == i.as_str()) .and_then(|ctx| ctx.mf2["items"].get(0)) - .or(Some(i)) - .unwrap()) + .unwrap_or(i)) .collect::>()) }) } @@ -218,7 +217,7 @@ async fn background_processing( // TODO actually save the post to the database and schedule post-processing pub(crate) async fn _post( - user: User, + user: &TokenData, uid: String, mf2: serde_json::Value, db: D, @@ -234,7 +233,7 @@ pub(crate) async fn _post( // - The MF2-JSON document's author is set // Security check! Do we have an OAuth2 scope to proceed? - if !user.check_scope("create") { + if !user.check_scope(&Scope::Create) { return Err(MicropubError { error: ErrorType::InvalidScope, error_description: "Not enough privileges - try acquiring the \"create\" scope." @@ -292,7 +291,7 @@ pub(crate) async fn _post( db.update_post(chan, json!({"add": {"children": [uid]}})) .await?; } else if default_channels.iter().any(|i| chan == i) { - util::create_feed(&db, &uid, chan, &user).await?; + util::create_feed(&db, &uid, chan, user).await?; } else { warn!("Ignoring non-existent channel: {}", chan); } @@ -344,10 +343,10 @@ impl From for MicropubAction { } #[tracing::instrument(skip(db))] -async fn post_action( +async fn post_action( action: MicropubAction, db: D, - user: User, + user: User, ) -> Result<(), MicropubError> { let uri = if let Ok(uri) = action.url.parse::() { uri @@ -375,7 +374,7 @@ async fn post_action( match action.action { ActionType::Delete => { - if !user.check_scope("delete") { + if !user.check_scope(&Scope::Delete) { return Err(MicropubError { error: ErrorType::InvalidScope, error_description: "You need a \"delete\" scope for this.".to_owned(), @@ -385,7 +384,7 @@ async fn post_action( db.delete_post(&action.url).await? } ActionType::Update => { - if !user.check_scope("update") { + if !user.check_scope(&Scope::Update) { return Err(MicropubError { error: ErrorType::InvalidScope, error_description: "You need an \"update\" scope for this.".to_owned(), @@ -468,10 +467,10 @@ async fn dispatch_body( } #[tracing::instrument(skip(db, http))] -pub async fn post( +pub(crate) async fn post( Extension(db): Extension, Extension(http): Extension, - user: User, + user: User, body: BodyStream, TypedHeader(content_type): TypedHeader, ) -> axum::response::Response { @@ -482,7 +481,7 @@ pub async fn post( }, Ok(PostBody::MF2(mf2)) => { let (uid, mf2) = normalize_mf2(mf2, &user); - match _post(user, uid, mf2, db, http).await { + match _post(&user, uid, mf2, db, http).await { Ok(response) => response, Err(err) => err.into_response(), } @@ -492,30 +491,40 @@ pub async fn post( } #[tracing::instrument(skip(db))] -pub async fn query( +pub(crate) async fn query( Extension(db): Extension, query: Option>, - user: User, + Host(host): Host, + user: User, ) -> axum::response::Response { // We handle the invalid query case manually to return a // MicropubError instead of HTTP 422 - if query.is_none() { + let query = if let Some(Query(query)) = query { + query + } else { return MicropubError::new( ErrorType::InvalidRequest, "Invalid query provided. Try ?q=config to see what you can do." ).into_response(); - } - let query: MicropubQuery = query.unwrap().0; + }; - let host = axum::http::Uri::try_from(user.me.as_str()) + if axum::http::Uri::try_from(user.me.as_str()) .unwrap() .authority() .unwrap() - .clone(); + != &host + { + return MicropubError::new( + ErrorType::NotAuthorized, + "This website doesn't belong to you.", + ) + .into_response(); + } + match query.q { QueryType::Config => { - let channels: Vec = match db.get_channels(host.as_str()).await { + let channels: Vec = match db.get_channels(user.me.as_str()).await { Ok(chans) => chans, Err(err) => { return MicropubError::new( @@ -534,26 +543,15 @@ pub async fn query( QueryType::SyndicateTo ], "channels": channels, - "_kittybox_authority": host.as_str(), - "syndicate-to": [] + "_kittybox_authority": user.me.as_str(), + "syndicate-to": [], + "media_endpoint": user.me.join("/.kittybox/media").unwrap().as_str() })) .into_response() } QueryType::Source => { match query.url { Some(url) => { - if axum::http::Uri::try_from(&url) - .unwrap() - .authority() - .unwrap() - != &host - { - return MicropubError::new( - ErrorType::NotAuthorized, - "You are requesting a post from a website that doesn't belong to you.", - ) - .into_response(); - } match db.get_post(&url).await { Ok(some) => match some { Some(post) => axum::response::Json(&post).into_response(), @@ -582,7 +580,7 @@ pub async fn query( } } } - QueryType::Channel => match db.get_channels(host.as_str()).await { + QueryType::Channel => match db.get_channels(user.me.as_str()).await { Ok(chans) => axum::response::Json(json!({ "channels": chans })).into_response(), Err(err) => MicropubError::new( ErrorType::InternalServerError, @@ -596,9 +594,17 @@ pub async fn query( } } -pub fn router(storage: S, http: reqwest::Client) -> axum::routing::MethodRouter { - axum::routing::get(query::) - .post(post::) +pub fn router( + storage: S, + http: reqwest::Client, + auth: A +) -> axum::routing::MethodRouter +where + S: Storage + 'static, + A: AuthBackend +{ + axum::routing::get(query::) + .post(post::) .layer(tower_http::cors::CorsLayer::new() .allow_methods([ axum::http::Method::GET, @@ -607,6 +613,7 @@ pub fn router(storage: S, http: reqwest::Client) -> axum:: .allow_origin(tower_http::cors::Any)) .layer(axum::Extension(storage)) .layer(axum::Extension(http)) + .layer(axum::Extension(auth)) } #[cfg(test)] @@ -629,11 +636,13 @@ impl MicropubQuery { #[cfg(test)] mod tests { - use crate::{database::Storage, micropub::MicropubError, tokenauth::User}; + use crate::{database::Storage, micropub::MicropubError}; use hyper::body::HttpBody; use serde_json::json; use super::FetchedPostContext; + use kittybox_indieauth::{Scopes, Scope, TokenData}; + use axum::extract::Host; #[test] fn test_populate_reply_context() { @@ -682,14 +691,15 @@ mod tests { "content": ["Hello world!"] } }); - let user = User::new( - "https://localhost:8080/", - "https://kittybox.fireburn.ru/", - "profile", - ); + let user = TokenData { + me: "https://localhost:8080/".parse().unwrap(), + client_id: "https://kittybox.fireburn.ru/".parse().unwrap(), + scope: Scopes::new(vec![Scope::Profile]), + iat: None, exp: None + }; let (uid, mf2) = super::normalize_mf2(post, &user); - let err = super::_post(user, uid, mf2, db.clone(), reqwest::Client::new()) + let err = super::_post(&user, uid, mf2, db.clone(), reqwest::Client::new()) .await .unwrap_err(); @@ -711,14 +721,15 @@ mod tests { "url": ["https://fireburn.ru/posts/hello"] } }); - let user = User::new( - "https://aaronparecki.com/", - "https://kittybox.fireburn.ru/", - "create update media", - ); + let user = TokenData { + me: "https://aaronparecki.com/".parse().unwrap(), + client_id: "https://kittybox.fireburn.ru/".parse().unwrap(), + scope: Scopes::new(vec![Scope::Profile, Scope::Create, Scope::Update, Scope::Media]), + iat: None, exp: None + }; let (uid, mf2) = super::normalize_mf2(post, &user); - let err = super::_post(user, uid, mf2, db.clone(), reqwest::Client::new()) + let err = super::_post(&user, uid, mf2, db.clone(), reqwest::Client::new()) .await .unwrap_err(); @@ -738,14 +749,15 @@ mod tests { "content": ["Hello world!"] } }); - let user = User::new( - "https://localhost:8080/", - "https://kittybox.fireburn.ru/", - "create", - ); + let user = TokenData { + me: "https://localhost:8080/".parse().unwrap(), + client_id: "https://kittybox.fireburn.ru/".parse().unwrap(), + scope: Scopes::new(vec![Scope::Profile, Scope::Create]), + iat: None, exp: None + }; let (uid, mf2) = super::normalize_mf2(post, &user); - let res = super::_post(user, uid, mf2, db.clone(), reqwest::Client::new()) + let res = super::_post(&user, uid, mf2, db.clone(), reqwest::Client::new()) .await .unwrap(); @@ -765,11 +777,15 @@ mod tests { Some(axum::extract::Query(super::MicropubQuery::source( "https://aaronparecki.com/feeds/main", ))), - User::new( - "https://fireburn.ru/", - "https://quill.p3k.io/", - "create update media", - ), + Host("aaronparecki.com".to_owned()), + crate::indieauth::User::( + TokenData { + me: "https://fireburn.ru/".parse().unwrap(), + client_id: "https://kittybox.fireburn.ru/".parse().unwrap(), + scope: Scopes::new(vec![Scope::Profile, Scope::Create, Scope::Update, Scope::Media]), + iat: None, exp: None + }, std::marker::PhantomData + ) ) .await; -- cgit 1.4.1