From 738bff1ae1ae3a841b32e211f132c6d65286dc80 Mon Sep 17 00:00:00 2001 From: nazeh Date: Fri, 20 Dec 2024 11:30:58 +0300 Subject: [PATCH] feat(homeserver): add PubkyHost and Authz layers --- .../src/core/database/tables/sessions.rs | 66 ++----- pubky-homeserver/src/core/extractors.rs | 46 ++--- pubky-homeserver/src/core/layers/authz.rs | 172 ++++++++++++++++++ pubky-homeserver/src/core/layers/mod.rs | 2 + .../src/core/layers/pubky_host.rs | 64 +++++++ pubky-homeserver/src/core/mod.rs | 70 +++---- pubky-homeserver/src/core/routes/auth.rs | 25 ++- pubky-homeserver/src/core/routes/mod.rs | 24 ++- .../src/core/routes/public/mod.rs | 61 ++----- .../src/core/routes/public/read.rs | 27 ++- .../src/core/routes/public/write.rs | 17 +- pubky/pkg/test/auth.js | 31 +++- pubky/src/native/cookies.rs | 1 - pubky/src/shared/public.rs | 7 +- 14 files changed, 393 insertions(+), 220 deletions(-) create mode 100644 pubky-homeserver/src/core/layers/authz.rs create mode 100644 pubky-homeserver/src/core/layers/mod.rs create mode 100644 pubky-homeserver/src/core/layers/pubky_host.rs diff --git a/pubky-homeserver/src/core/database/tables/sessions.rs b/pubky-homeserver/src/core/database/tables/sessions.rs index 3ffe916..c2fa2fb 100644 --- a/pubky-homeserver/src/core/database/tables/sessions.rs +++ b/pubky-homeserver/src/core/database/tables/sessions.rs @@ -2,9 +2,7 @@ use heed::{ types::{Bytes, Str}, Database, }; -use pkarr::PublicKey; use pubky_common::session::Session; -use tower_cookies::Cookies; use crate::core::database::DB; @@ -14,61 +12,31 @@ pub type SessionsTable = Database; pub const SESSIONS_TABLE: &str = "sessions"; impl DB { - pub fn get_session( - &mut self, - cookies: Cookies, - public_key: &PublicKey, - ) -> anyhow::Result> { - if let Some(bytes) = self.get_session_bytes(cookies, public_key)? { + pub fn get_session(&self, session_secret: &str) -> anyhow::Result> { + let rtxn = self.env.read_txn()?; + + let session = self + .tables + .sessions + .get(&rtxn, session_secret)? + .map(|s| s.to_vec()); + + rtxn.commit()?; + + if let Some(bytes) = session { return Ok(Some(Session::deserialize(&bytes)?)); }; Ok(None) } - pub fn get_session_bytes( - &mut self, - cookies: Cookies, - public_key: &PublicKey, - ) -> anyhow::Result>> { - if let Some(cookie) = - cookies.get(&public_key.to_string().chars().take(8).collect::()) - { - let rtxn = self.env.read_txn()?; + pub fn delete_session(&mut self, secret: &str) -> anyhow::Result { + let mut wtxn = self.env.write_txn()?; - let session = self - .tables - .sessions - .get(&rtxn, cookie.value())? - .map(|s| s.to_vec()); + let deleted = self.tables.sessions.delete(&mut wtxn, secret)?; - rtxn.commit()?; + wtxn.commit()?; - return Ok(session); - }; - - Ok(None) - } - - pub fn delete_session( - &mut self, - cookies: Cookies, - public_key: &PublicKey, - ) -> anyhow::Result { - // TODO: Set expired cookie to delete the cookie on client side. - - if let Some(cookie) = - cookies.get(&public_key.to_string().chars().take(8).collect::()) - { - let mut wtxn = self.env.write_txn()?; - - let deleted = self.tables.sessions.delete(&mut wtxn, cookie.value())?; - - wtxn.commit()?; - - return Ok(deleted); - }; - - Ok(false) + Ok(deleted) } } diff --git a/pubky-homeserver/src/core/extractors.rs b/pubky-homeserver/src/core/extractors.rs index c4b090e..0d4f3f9 100644 --- a/pubky-homeserver/src/core/extractors.rs +++ b/pubky-homeserver/src/core/extractors.rs @@ -10,48 +10,38 @@ use axum::{ use pkarr::PublicKey; -use crate::core::error::{Error, Result}; +use crate::core::error::Result; -#[derive(Debug)] -pub enum Pubky { - Host(PublicKey), - PubkyHost(PublicKey), -} +#[derive(Debug, Clone)] +pub struct PubkyHost(pub(crate) PublicKey); -impl Pubky { +impl PubkyHost { pub fn public_key(&self) -> &PublicKey { - match self { - Pubky::Host(p) => p, - Pubky::PubkyHost(p) => p, - } + &self.0 } } #[async_trait] -impl FromRequestParts for Pubky +impl FromRequestParts for PubkyHost where - S: Send + Sync, + S: Sync + Send, { type Rejection = Response; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - let headers_to_check = ["host", "pubky-host"]; + let pubky_host = parts + .extensions + .get::() + .cloned() + .ok_or(( + StatusCode::INTERNAL_SERVER_ERROR, + "Can't extract PubkyHost. Is `PubkyHostLayer` enabled?", + )) + .map_err(|e| e.into_response())?; - for header in headers_to_check { - if let Some(Ok(pubky_host)) = parts.headers.get(header).map(|h| h.to_str()) { - if let Ok(public_key) = PublicKey::try_from(pubky_host) { - tracing::debug!(?pubky_host); + tracing::debug!(?pubky_host); - if header == "host" { - return Ok(Pubky::Host(public_key)); - } - - return Ok(Pubky::PubkyHost(public_key)); - } - } - } - - Err(Error::new(StatusCode::NOT_FOUND, "Pubky host not found".into()).into_response()) + Ok(pubky_host) } } diff --git a/pubky-homeserver/src/core/layers/authz.rs b/pubky-homeserver/src/core/layers/authz.rs new file mode 100644 index 0000000..d8cc5dd --- /dev/null +++ b/pubky-homeserver/src/core/layers/authz.rs @@ -0,0 +1,172 @@ +use axum::http::{header, HeaderMap, Method}; +use axum::response::IntoResponse; +use axum::{ + body::Body, + http::{Request, StatusCode}, +}; +use futures_util::future::BoxFuture; +use pkarr::PublicKey; +use std::{convert::Infallible, task::Poll}; +use tower::{Layer, Service}; +use tower_cookies::Cookies; + +use crate::core::{ + error::{Error, Result}, + extractors::PubkyHost, + AppState, +}; + +/// A Tower Layer to handle authorization for write operations. +#[derive(Debug, Clone)] +pub struct AuthorizationLayer { + state: AppState, +} + +impl AuthorizationLayer { + pub fn new(state: AppState) -> Self { + Self { state } + } +} + +impl Layer for AuthorizationLayer { + type Service = AuthorizationMiddleware; + + fn layer(&self, inner: S) -> Self::Service { + AuthorizationMiddleware { + inner, + state: self.state.clone(), + } + } +} + +/// Middleware that performs authorization checks for write operations. +#[derive(Debug, Clone)] +pub struct AuthorizationMiddleware { + inner: S, + state: AppState, +} + +impl Service> for AuthorizationMiddleware +where + S: Service, Response = axum::response::Response, Error = Infallible> + + Send + + 'static + + Clone, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(|_| unreachable!()) // `Infallible` conversion + } + + fn call(&mut self, req: Request) -> Self::Future { + let state = self.state.clone(); + let mut inner = self.inner.clone(); + + Box::pin(async move { + let path = req.uri().path(); + + // Verify the path + if let Err(e) = verify(path) { + return Ok(e.into_response()); + } + + let pubky = match req.extensions().get::() { + Some(pk) => pk, + None => { + return Ok( + Error::new(StatusCode::NOT_FOUND, "Pubky Host is missing".into()) + .into_response(), + ) + } + }; + + // Authorize the request + if let Err(e) = authorize( + &state, + req.method(), + req.headers(), + pubky.public_key(), + path, + ) { + return Ok(e.into_response()); + } + + // If authorized, proceed to the inner service + inner.call(req).await.map_err(|_| unreachable!()) + }) + } +} + +/// Verifies the path. +fn verify(path: &str) -> Result<()> { + if !path.starts_with("/pub/") { + return Err(Error::new( + StatusCode::FORBIDDEN, + "Writing to directories other than '/pub/' is forbidden".into(), + )); + } + Ok(()) +} + +/// Authorize write (PUT or DELETE) for Public paths. +fn authorize( + state: &AppState, + method: &Method, + headers: &HeaderMap, + public_key: &PublicKey, + path: &str, +) -> Result<()> { + if path.starts_with("/pub/") && method == Method::GET { + return Ok(()); + } + + let session_secret = session_secret_from_headers(headers, public_key) + .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; + + let session = state + .db + .get_session(&session_secret)? + .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; + + if session.pubky() == public_key + && session.capabilities().iter().any(|cap| { + path.starts_with(&cap.scope) + && cap + .actions + .contains(&pubky_common::capabilities::Action::Write) + }) + { + return Ok(()); + } + + Err(Error::with_status(StatusCode::FORBIDDEN)) +} + +fn cookie_name(public_key: &PublicKey) -> String { + public_key.to_string().chars().take(8).collect::() +} + +pub fn session_secret_from_cookies(cookies: Cookies, public_key: &PublicKey) -> Option { + cookies + .get(&cookie_name(public_key)) + .map(|c| c.value().to_string()) +} + +// TODO: unit test this +fn session_secret_from_headers(headers: &HeaderMap, public_key: &PublicKey) -> Option { + headers + .get_all(header::COOKIE) + .iter() + .filter_map(|h| h.to_str().ok()) + .find(|h| h.starts_with(&cookie_name(public_key))) + .and_then(|h| { + h.split(';') + .next() + .and_then(|key_value| key_value.split('=').last()) + }) + .map(|s| s.to_string()) +} diff --git a/pubky-homeserver/src/core/layers/mod.rs b/pubky-homeserver/src/core/layers/mod.rs new file mode 100644 index 0000000..585095b --- /dev/null +++ b/pubky-homeserver/src/core/layers/mod.rs @@ -0,0 +1,2 @@ +pub mod authz; +pub mod pubky_host; diff --git a/pubky-homeserver/src/core/layers/pubky_host.rs b/pubky-homeserver/src/core/layers/pubky_host.rs new file mode 100644 index 0000000..1af6d1e --- /dev/null +++ b/pubky-homeserver/src/core/layers/pubky_host.rs @@ -0,0 +1,64 @@ +use pkarr::PublicKey; + +use crate::core::extractors::PubkyHost; + +use axum::{body::Body, http::Request}; +use futures_util::future::BoxFuture; +use std::{convert::Infallible, task::Poll}; +use tower::{Layer, Service}; + +use crate::core::error::Result; + +/// A Tower Layer to handle authorization for write operations. +#[derive(Debug, Clone)] +pub struct PubkyHostLayer; + +impl Layer for PubkyHostLayer { + type Service = PubkyHostLayerMiddleware; + + fn layer(&self, inner: S) -> Self::Service { + PubkyHostLayerMiddleware { inner } + } +} + +/// Middleware that performs authorization checks for write operations. +#[derive(Debug, Clone)] +pub struct PubkyHostLayerMiddleware { + inner: S, +} + +impl Service> for PubkyHostLayerMiddleware +where + S: Service, Response = axum::response::Response, Error = Infallible> + + Send + + 'static + + Clone, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = Infallible; + type Future = BoxFuture<'static, Result>; + + fn poll_ready(&mut self, cx: &mut std::task::Context<'_>) -> Poll> { + self.inner.poll_ready(cx).map_err(|_| unreachable!()) // `Infallible` conversion + } + + fn call(&mut self, req: Request) -> Self::Future { + let mut inner = self.inner.clone(); + let mut req = req; + + Box::pin(async move { + let headers_to_check = ["host", "pubky-host"]; + + for header in headers_to_check { + if let Some(Ok(pubky_host)) = req.headers().get(header).map(|h| h.to_str()) { + if let Ok(public_key) = PublicKey::try_from(pubky_host) { + req.extensions_mut().insert(PubkyHost(public_key)); + } + } + } + + inner.call(req).await.map_err(|_| unreachable!()) + }) + } +} diff --git a/pubky-homeserver/src/core/mod.rs b/pubky-homeserver/src/core/mod.rs index ce33985..5ab88e7 100644 --- a/pubky-homeserver/src/core/mod.rs +++ b/pubky-homeserver/src/core/mod.rs @@ -1,20 +1,26 @@ use anyhow::Result; -use axum::{extract::Request, response::Response, Router}; +use axum::{ + body::Body, + extract::Request, + http::{header, Method}, + response::Response, + Router, +}; use pkarr::{Keypair, PublicKey}; use pubky_common::{ - auth::AuthVerifier, capabilities::Capability, crypto::random_bytes, session::Session, - timestamp::Timestamp, + auth::{AuthToken, AuthVerifier}, + capabilities::Capability, }; use tower::ServiceExt; -use tower_cookies::{cookie::SameSite, Cookie}; mod config; mod database; mod error; mod extractors; +mod layers; mod routes; -use database::{tables::users::User, DB}; +use database::DB; pub use config::Config; @@ -28,7 +34,6 @@ pub(crate) struct AppState { /// A side-effect-free Core of the [Homeserver]. pub struct HomeserverCore { config: Config, - pub(crate) state: AppState, pub(crate) router: Router, } @@ -49,7 +54,6 @@ impl HomeserverCore { let router = routes::create_app(state.clone()); Ok(Self { - state, router, config: config.clone(), }) @@ -79,42 +83,28 @@ impl HomeserverCore { // === Public Methods === - // TODO: move this logic to a common place. - pub fn create_user(&mut self, public_key: &PublicKey) -> Result { - let mut wtxn = self.state.db.env.write_txn()?; + pub async fn create_root_user(&mut self, keypair: &Keypair) -> Result { + let auth_token = AuthToken::sign(keypair, vec![Capability::root()]); - self.state.db.tables.users.put( - &mut wtxn, - public_key, - &User { - created_at: Timestamp::now().as_u64(), - }, - )?; + let response = self + .call( + Request::builder() + .uri("/signup") + .header("host", keypair.public_key().to_string()) + .method(Method::POST) + .body(Body::from(auth_token.serialize())) + .unwrap(), + ) + .await?; - let session_secret = base32::encode(base32::Alphabet::Crockford, &random_bytes::<16>()); + let header_value = response + .headers() + .get(header::SET_COOKIE) + .and_then(|h| h.to_str().ok()) + .expect("should return a set-cookie header") + .to_string(); - let session = Session::new(public_key, &[Capability::root()], None).serialize(); - - self.state - .db - .tables - .sessions - .put(&mut wtxn, &session_secret, &session)?; - - wtxn.commit()?; - - let mut cookie = Cookie::new( - public_key.to_string().chars().take(8).collect::(), - session_secret, - ); - - cookie.set_path("/"); - - cookie.set_secure(true); - cookie.set_same_site(SameSite::None); - cookie.set_http_only(true); - - Ok(cookie) + Ok(header_value) } pub async fn call(&self, request: Request) -> Result { diff --git a/pubky-homeserver/src/core/routes/auth.rs b/pubky-homeserver/src/core/routes/auth.rs index f1efdb2..8cffc88 100644 --- a/pubky-homeserver/src/core/routes/auth.rs +++ b/pubky-homeserver/src/core/routes/auth.rs @@ -12,7 +12,8 @@ use pubky_common::{crypto::random_bytes, session::Session, timestamp::Timestamp} use crate::core::{ database::tables::users::User, error::{Error, Result}, - extractors::Pubky, + extractors::PubkyHost, + layers::authz::session_secret_from_cookies, AppState, }; @@ -29,14 +30,16 @@ pub async fn signup( } pub async fn session( - State(mut state): State, + State(state): State, cookies: Cookies, - pubky: Pubky, + pubky: PubkyHost, ) -> Result { - if let Some(session) = state.db.get_session(cookies, pubky.public_key())? { - // TODO: add content-type - return Ok(session.serialize()); - }; + if let Some(secret) = session_secret_from_cookies(cookies, pubky.public_key()) { + if let Some(session) = state.db.get_session(&secret)? { + // TODO: add content-type + return Ok(session.serialize()); + }; + } Err(Error::with_status(StatusCode::NOT_FOUND)) } @@ -44,9 +47,13 @@ pub async fn session( pub async fn signout( State(mut state): State, cookies: Cookies, - pubky: Pubky, + pubky: PubkyHost, ) -> Result { - state.db.delete_session(cookies, pubky.public_key())?; + // TODO: Set expired cookie to delete the cookie on client side. + + if let Some(secret) = session_secret_from_cookies(cookies, pubky.public_key()) { + state.db.delete_session(&secret)?; + } // Idempotent Success Response (200 OK) Ok(()) diff --git a/pubky-homeserver/src/core/routes/mod.rs b/pubky-homeserver/src/core/routes/mod.rs index a01b172..9f485f1 100644 --- a/pubky-homeserver/src/core/routes/mod.rs +++ b/pubky-homeserver/src/core/routes/mod.rs @@ -1,8 +1,7 @@ //! The controller part of the [crate::HomeserverCore] use axum::{ - extract::DefaultBodyLimit, - routing::{delete, get, head, post, put}, + routing::{delete, get, post}, Router, }; use tower_cookies::CookieManagerLayer; @@ -10,12 +9,14 @@ use tower_http::{cors::CorsLayer, trace::TraceLayer}; use crate::core::AppState; +use super::layers::pubky_host::PubkyHostLayer; + mod auth; mod feed; mod public; mod root; -fn base(state: AppState) -> Router { +fn base() -> Router { Router::new() .route("/", get(root::handler)) .route("/signup", post(auth::signup)) @@ -27,22 +28,19 @@ fn base(state: AppState) -> Router { .route("/session", get(auth::session)) .route("/session", delete(auth::signout)) // - Data routes - .route("/pub/", get(public::read::list_root)) - .route("/pub/*path", get(public::read::get)) - .route("/pub/*path", head(public::read::head)) - .route("/pub/*path", put(public::write::put)) - .route("/pub/*path", delete(public::write::delete)) // Events .route("/events/", get(feed::feed)) .layer(CookieManagerLayer::new()) - // TODO: revisit if we enable streaming big payloads - // TODO: maybe add to a separate router (drive router?). - .layer(DefaultBodyLimit::max(100 * 1024 * 1024)) - .with_state(state) + // TODO: add size limit + // TODO: revisit if we enable streaming big payloads + // TODO: maybe add to a separate router (drive router?). } pub fn create_app(state: AppState) -> Router { - base(state.clone()) + base() + .merge(public::data_store_router(state.clone())) .layer(CorsLayer::very_permissive()) .layer(TraceLayer::new_for_http()) + .layer(PubkyHostLayer) + .with_state(state) } diff --git a/pubky-homeserver/src/core/routes/public/mod.rs b/pubky-homeserver/src/core/routes/public/mod.rs index f19abef..dc37314 100644 --- a/pubky-homeserver/src/core/routes/public/mod.rs +++ b/pubky-homeserver/src/core/routes/public/mod.rs @@ -1,52 +1,21 @@ -use axum::http::StatusCode; -use pkarr::PublicKey; -use tower_cookies::Cookies; - -use crate::core::{ - error::{Error, Result}, - AppState, +use axum::{ + extract::DefaultBodyLimit, + routing::{delete, get, head, put}, + Router, }; +use crate::core::{layers::authz::AuthorizationLayer, AppState}; + pub mod read; pub mod write; -/// Authorize write (PUT or DELETE) for Public paths. -fn authorize( - state: &mut AppState, - cookies: Cookies, - public_key: &PublicKey, - path: &str, -) -> Result<()> { - // TODO: can we move this logic to the extractor or a layer - // to perform this validation? - let session = state - .db - .get_session(cookies, public_key)? - .ok_or(Error::with_status(StatusCode::UNAUTHORIZED))?; - - if session.pubky() == public_key - && session.capabilities().iter().any(|cap| { - path.starts_with(&cap.scope[1..]) - && cap - .actions - .contains(&pubky_common::capabilities::Action::Write) - }) - { - return Ok(()); - } - - Err(Error::with_status(StatusCode::FORBIDDEN)) -} - -fn verify(path: &str) -> Result<()> { - if !path.starts_with("pub/") { - return Err(Error::new( - StatusCode::FORBIDDEN, - "Writing to directories other than '/pub/' is forbidden".into(), - )); - } - - // TODO: should we forbid paths ending with `/`? - - Ok(()) +pub fn data_store_router(state: AppState) -> Router { + Router::new() + .route("/pub/", get(read::list_root)) + .route("/pub/*path", get(read::get)) + .route("/pub/*path", head(read::head)) + .route("/pub/*path", put(write::put)) + .route("/pub/*path", delete(write::delete)) + .layer(DefaultBodyLimit::max(100 * 1024 * 1024)) + .layer(AuthorizationLayer::new(state.clone())) } diff --git a/pubky-homeserver/src/core/routes/public/read.rs b/pubky-homeserver/src/core/routes/public/read.rs index 532c100..6ffb379 100644 --- a/pubky-homeserver/src/core/routes/public/read.rs +++ b/pubky-homeserver/src/core/routes/public/read.rs @@ -11,20 +11,16 @@ use std::str::FromStr; use crate::core::{ database::tables::entries::Entry, error::{Error, Result}, - extractors::{EntryPath, ListQueryParams, Pubky}, + extractors::{EntryPath, ListQueryParams, PubkyHost}, AppState, }; -use super::verify; - pub async fn head( State(state): State, - pubky: Pubky, + pubky: PubkyHost, headers: HeaderMap, path: EntryPath, ) -> Result { - verify(path.as_str())?; - let rtxn = state.db.env.read_txn()?; get_entry( @@ -38,7 +34,7 @@ pub async fn head( pub async fn list_root( State(state): State, - pubky: Pubky, + pubky: PubkyHost, params: ListQueryParams, ) -> Result { list(state, pubky.public_key(), "pub/", params) @@ -47,12 +43,10 @@ pub async fn list_root( pub async fn get( State(state): State, headers: HeaderMap, - pubky: Pubky, + pubky: PubkyHost, path: EntryPath, params: ListQueryParams, ) -> Result { - verify(&path)?; - let public_key = pubky.public_key().clone(); if path.as_str().ends_with('/') { @@ -215,9 +209,9 @@ mod tests { async fn if_last_modified() { let mut server = HomeserverCore::test().unwrap(); - let public_key = Keypair::random().public_key(); - let cookie = server.create_user(&public_key).unwrap(); - let cookie = cookie.to_string(); + let keypair = Keypair::random(); + let public_key = keypair.public_key(); + let cookie = server.create_root_user(&keypair).await.unwrap().to_string(); let data = vec![1_u8, 2, 3, 4, 5]; @@ -271,9 +265,10 @@ mod tests { async fn if_none_match() { let mut server = HomeserverCore::test().unwrap(); - let public_key = Keypair::random().public_key(); - let cookie = server.create_user(&public_key).unwrap(); - let cookie = cookie.to_string(); + let keypair = Keypair::random(); + let public_key = keypair.public_key(); + + let cookie = server.create_root_user(&keypair).await.unwrap().to_string(); let data = vec![1_u8, 2, 3, 4, 5]; diff --git a/pubky-homeserver/src/core/routes/public/write.rs b/pubky-homeserver/src/core/routes/public/write.rs index 4c78b0a..2f2d3f2 100644 --- a/pubky-homeserver/src/core/routes/public/write.rs +++ b/pubky-homeserver/src/core/routes/public/write.rs @@ -3,27 +3,20 @@ use std::io::Write; use futures_util::stream::StreamExt; use axum::{body::Body, extract::State, http::StatusCode, response::IntoResponse}; -use tower_cookies::Cookies; use crate::core::{ error::{Error, Result}, - extractors::{EntryPath, Pubky}, + extractors::{EntryPath, PubkyHost}, AppState, }; -use super::{authorize, verify}; - pub async fn delete( State(mut state): State, - pubky: Pubky, + pubky: PubkyHost, path: EntryPath, - cookies: Cookies, ) -> Result { let public_key = pubky.public_key().clone(); - verify(&path)?; - authorize(&mut state, cookies, &public_key, &path)?; - // TODO: should we wrap this with `tokio::task::spawn_blocking` in case it takes too long? let deleted = state.db.delete_entry(&public_key, &path)?; @@ -37,16 +30,12 @@ pub async fn delete( pub async fn put( State(mut state): State, - pubky: Pubky, + pubky: PubkyHost, path: EntryPath, - cookies: Cookies, body: Body, ) -> Result { let public_key = pubky.public_key().clone(); - verify(&path)?; - authorize(&mut state, cookies, &public_key, &path)?; - let mut entry_writer = state.db.write_entry(&public_key, &path)?; let mut stream = body.into_data_stream(); diff --git a/pubky/pkg/test/auth.js b/pubky/pkg/test/auth.js index e9961ca..73179f0 100644 --- a/pubky/pkg/test/auth.js +++ b/pubky/pkg/test/auth.js @@ -7,7 +7,7 @@ const TESTNET_HTTP_RELAY = "http://localhost:15412/link"; // TODO: test multiple users in wasm -test('auth', async (t) => { +test('Auth: basic', async (t) => { const client = Client.testnet(); const keypair = Keypair.random() @@ -33,7 +33,34 @@ test('auth', async (t) => { } }) -test("3rd party signin", async (t) => { +test("Auth: multi-user (cookies)", async (t) => { + const client = Client.testnet(); + + const alice = Keypair.random() + const bob = Keypair.random() + + await client.signup(alice, HOMESERVER_PUBLICKEY ) + + let session = await client.session(alice.publicKey()) + t.ok(session, "signup") + + { + await client.signup(bob, HOMESERVER_PUBLICKEY ) + + const session = await client.session(bob.publicKey()) + t.ok(session, "signup") + } + + session = await client.session(alice.publicKey()); + t.is(session.pubky().z32(), alice.publicKey().z32(), "alice is still signed in") + + await client.signout(bob.publicKey()); + + session = await client.session(alice.publicKey()); + t.is(session.pubky().z32(), alice.publicKey().z32(), "alice is still signed in after signout of bob") +}) + +test("Auth: 3rd party signin", async (t) => { let keypair = Keypair.random(); let pubky = keypair.publicKey().z32(); diff --git a/pubky/src/native/cookies.rs b/pubky/src/native/cookies.rs index d4a2adb..03ca907 100644 --- a/pubky/src/native/cookies.rs +++ b/pubky/src/native/cookies.rs @@ -66,7 +66,6 @@ impl CookieStore for CookieJar { .collect::>() .join("; "); - // TODO: should we return if empty or just push? if s.is_empty() { let host = url.host_str().unwrap_or(""); diff --git a/pubky/src/shared/public.rs b/pubky/src/shared/public.rs index 74e8b7e..53636a1 100644 --- a/pubky/src/shared/public.rs +++ b/pubky/src/shared/public.rs @@ -677,9 +677,12 @@ mod tests { ); } - let get = client.get(url).send().await.unwrap().bytes().await.unwrap(); + let response = client.get(url).send().await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); - assert_eq!(get.as_ref(), &[0]); + let body = response.bytes().await.unwrap(); + + assert_eq!(body.as_ref(), &[0]); } #[tokio::test]