diff --git a/pubky-homeserver/src/routes/public.rs b/pubky-homeserver/src/routes/public.rs index cdfb0a9..2d417dd 100644 --- a/pubky-homeserver/src/routes/public.rs +++ b/pubky-homeserver/src/routes/public.rs @@ -26,8 +26,8 @@ pub async fn put( let public_key = pubky.public_key().clone(); let path = path.as_str(); - authorize(&mut state, cookies, &public_key, path)?; verify(path)?; + authorize(&mut state, cookies, &public_key, path)?; let mut stream = body.into_data_stream(); @@ -134,20 +134,32 @@ pub async fn delete( Ok(()) } +/// Authorize write (PUT or DELETE) for Public paths. fn authorize( state: &mut AppState, cookies: Cookies, public_key: &PublicKey, - _: &str, + path: &str, ) -> Result<()> { // TODO: can we move this logic to the extractor or a layer // to perform this validation? - let _ = state + let session = state .db .get_session(cookies, public_key)? .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; - Ok(()) + if session.pubky == *public_key + && session.capabilities.iter().any(|cap| { + path.starts_with(&cap.scope[1..]) + && cap + .abilities + .contains(&pubky_common::capabilities::Ability::Write) + }) + { + return Ok(()); + } + + Err(Error::with_status(StatusCode::FORBIDDEN)) } fn verify(path: &str) -> Result<()> { diff --git a/pubky/src/shared/auth.rs b/pubky/src/shared/auth.rs index 980ec52..ea35eb2 100644 --- a/pubky/src/shared/auth.rs +++ b/pubky/src/shared/auth.rs @@ -256,7 +256,7 @@ mod tests { use pkarr::{mainline::Testnet, Keypair}; use pubky_common::capabilities::{Capabilities, Capability}; use pubky_homeserver::Homeserver; - use url::Url; + use reqwest::StatusCode; #[tokio::test] async fn basic_authn() { @@ -294,6 +294,7 @@ mod tests { .unwrap() .unwrap(); + assert_eq!(session.pubky, keypair.public_key()); assert!(session.capabilities.contains(&Capability::root())); } } @@ -304,11 +305,11 @@ mod tests { let server = Homeserver::start_test(&testnet).await.unwrap(); let keypair = Keypair::random(); + let pubky = keypair.public_key(); // Third party app side - let capabilities: Capabilities = "/pub/pubky.app/:rw,/prv/foo.bar/file:rw" - .try_into() - .unwrap(); + let capabilities: Capabilities = + "/pub/pubky.app/:rw,/pub/foo.bar/file:r".try_into().unwrap(); let client = PubkyClient::test(&testnet); let (pubkyauth_url, pubkyauth_response) = client .auth_request("https://demo.httprelay.io/link", &capabilities) @@ -328,7 +329,36 @@ mod tests { let session = pubkyauth_response.await.unwrap().unwrap(); + assert_eq!(session.pubky, pubky); assert_eq!(session.capabilities, capabilities.0); - assert_eq!(session.pubky, keypair.public_key()); + + // Test access control enforcement + + client + .put(format!("pubky://{pubky}/pub/pubky.app/foo").as_str(), &[]) + .await + .unwrap(); + + assert_eq!( + client + .put(format!("pubky://{pubky}/pub/pubky.app").as_str(), &[]) + .await + .map_err(|e| match e { + crate::Error::Reqwest(e) => e.status(), + _ => None, + }), + Err(Some(StatusCode::FORBIDDEN)) + ); + + assert_eq!( + client + .put(format!("pubky://{pubky}/pub/foo.bar/file").as_str(), &[]) + .await + .map_err(|e| match e { + crate::Error::Reqwest(e) => e.status(), + _ => None, + }), + Err(Some(StatusCode::FORBIDDEN)) + ); } }