about summary refs log tree commit diff
path: root/kittybox-rs/src/micropub
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2022-09-28 03:55:48 +0300
committerVika <vika@fireburn.ru>2022-09-28 03:55:48 +0300
commit6e20a3c51756c2e84290da6ec53b89a5fc58c0fc (patch)
treea360c40dce27b7804001038babd4631476232001 /kittybox-rs/src/micropub
parent2f02bf76a40c971b9404aa0913bc8baa7dfde24c (diff)
Use tokens from the auth backend to authenticate for Micropub
Diffstat (limited to 'kittybox-rs/src/micropub')
-rw-r--r--kittybox-rs/src/micropub/mod.rs148
-rw-r--r--kittybox-rs/src/micropub/util.rs52
2 files changed, 101 insertions, 99 deletions
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::<Vec<&serde_json::Value>>())
     })
 }
@@ -218,7 +217,7 @@ async fn background_processing<D: 'static + Storage>(
 
 // TODO actually save the post to the database and schedule post-processing
 pub(crate) async fn _post<D: 'static + Storage>(
-    user: User,
+    user: &TokenData,
     uid: String,
     mf2: serde_json::Value,
     db: D,
@@ -234,7 +233,7 @@ pub(crate) async fn _post<D: 'static + Storage>(
     //   - 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<D: 'static + Storage>(
             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<MicropubFormAction> for MicropubAction {
 }
 
 #[tracing::instrument(skip(db))]
-async fn post_action<D: Storage>(
+async fn post_action<D: Storage, A: AuthBackend>(
     action: MicropubAction,
     db: D,
-    user: User,
+    user: User<A>,
 ) -> Result<(), MicropubError> {
     let uri = if let Ok(uri) = action.url.parse::<hyper::Uri>() {
         uri
@@ -375,7 +374,7 @@ async fn post_action<D: Storage>(
 
     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<D: Storage>(
             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<D: Storage + 'static>(
+pub(crate) async fn post<D: Storage + 'static, A: AuthBackend>(
     Extension(db): Extension<D>,
     Extension(http): Extension<reqwest::Client>,
-    user: User,
+    user: User<A>,
     body: BodyStream,
     TypedHeader(content_type): TypedHeader<ContentType>,
 ) -> axum::response::Response {
@@ -482,7 +481,7 @@ pub async fn post<D: Storage + 'static>(
         },
         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<D: Storage + 'static>(
 }
 
 #[tracing::instrument(skip(db))]
-pub async fn query<D: Storage>(
+pub(crate) async fn query<D: Storage, A: AuthBackend>(
     Extension(db): Extension<D>,
     query: Option<Query<MicropubQuery>>,
-    user: User,
+    Host(host): Host,
+    user: User<A>,
 ) -> 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<MicropubChannel> = match db.get_channels(host.as_str()).await {
+            let channels: Vec<MicropubChannel> = 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<D: Storage>(
                     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<D: Storage>(
                 }
             }
         }
-        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<D: Storage>(
     }
 }
 
-pub fn router<S: Storage + 'static>(storage: S, http: reqwest::Client) -> axum::routing::MethodRouter {
-    axum::routing::get(query::<S>)
-        .post(post::<S>)
+pub fn router<S, A>(
+    storage: S,
+    http: reqwest::Client,
+    auth: A
+) -> axum::routing::MethodRouter
+where
+    S: Storage + 'static,
+    A: AuthBackend
+{
+    axum::routing::get(query::<S, A>)
+        .post(post::<S, A>)
         .layer(tower_http::cors::CorsLayer::new()
                .allow_methods([
                    axum::http::Method::GET,
@@ -607,6 +613,7 @@ pub fn router<S: Storage + 'static>(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::<crate::indieauth::backend::fs::FileBackend>(
+                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;
 
diff --git a/kittybox-rs/src/micropub/util.rs b/kittybox-rs/src/micropub/util.rs
index 7c6a0b1..5097878 100644
--- a/kittybox-rs/src/micropub/util.rs
+++ b/kittybox-rs/src/micropub/util.rs
@@ -1,5 +1,5 @@
 use crate::database::Storage;
-use crate::tokenauth::User;
+use kittybox_indieauth::TokenData;
 use chrono::prelude::*;
 use core::iter::Iterator;
 use newbase60::num_to_sxg;
@@ -33,7 +33,7 @@ fn reset_dt(post: &mut serde_json::Value) -> DateTime<FixedOffset> {
     chrono::DateTime::from(curtime)
 }
 
-pub fn normalize_mf2(mut body: serde_json::Value, user: &User) -> (String, serde_json::Value) {
+pub fn normalize_mf2(mut body: serde_json::Value, user: &TokenData) -> (String, serde_json::Value) {
     // Normalize the MF2 object here.
     let me = &user.me;
     let folder = get_folder_from_type(body["type"][0].as_str().unwrap());
@@ -190,7 +190,7 @@ pub(crate) async fn create_feed(
     storage: &impl Storage,
     uid: &str,
     channel: &str,
-    user: &User,
+    user: &TokenData,
 ) -> crate::database::Result<()> {
     let path = url::Url::parse(channel).unwrap().path().to_string();
 
@@ -220,6 +220,16 @@ mod tests {
     use super::*;
     use serde_json::json;
 
+    fn token_data() -> TokenData {
+        TokenData {
+            me: "https://fireburn.ru/".parse().unwrap(),
+            client_id: "https://quill.p3k.io/".parse().unwrap(),
+            scope: kittybox_indieauth::Scopes::new(vec![kittybox_indieauth::Scope::Create]),
+            exp: Some(u64::MAX),
+            iat: Some(0)
+        }
+    }
+
     #[test]
     fn test_form_to_mf2() {
         assert_eq!(
@@ -248,11 +258,7 @@ mod tests {
 
         let (uid, normalized) = normalize_mf2(
             mf2.clone(),
-            &User::new(
-                "https://fireburn.ru/",
-                "https://quill.p3k.io/",
-                "create update media",
-            ),
+            &token_data(),
         );
         assert_eq!(
             normalized["properties"]["uid"][0], mf2["properties"]["uid"][0],
@@ -277,11 +283,7 @@ mod tests {
 
         let (_, normalized) = normalize_mf2(
             mf2.clone(),
-            &User::new(
-                "https://fireburn.ru/",
-                "https://quill.p3k.io/",
-                "create update media",
-            ),
+            &token_data(),
         );
 
         assert_eq!(
@@ -303,11 +305,7 @@ mod tests {
 
         let (_, normalized) = normalize_mf2(
             mf2.clone(),
-            &User::new(
-                "https://fireburn.ru/",
-                "https://quill.p3k.io/",
-                "create update media",
-            ),
+            &token_data(),
         );
 
         assert_eq!(
@@ -327,11 +325,7 @@ mod tests {
 
         let (uid, post) = normalize_mf2(
             mf2,
-            &User::new(
-                "https://fireburn.ru/",
-                "https://quill.p3k.io/",
-                "create update media",
-            ),
+            &token_data(),
         );
         assert_eq!(
             post["properties"]["published"]
@@ -396,11 +390,7 @@ mod tests {
 
         let (_, post) = normalize_mf2(
             mf2,
-            &User::new(
-                "https://fireburn.ru/",
-                "https://quill.p3k.io/",
-                "create update media",
-            ),
+            &token_data(),
         );
         assert!(
             post["properties"]["url"]
@@ -429,11 +419,7 @@ mod tests {
 
         let (uid, post) = normalize_mf2(
             mf2,
-            &User::new(
-                "https://fireburn.ru/",
-                "https://quill.p3k.io/",
-                "create update media",
-            ),
+            &token_data(),
         );
         assert_eq!(
             post["properties"]["uid"][0], uid,