feat(homeserver): remove EntryPath extractor and add Authz and PubkyHost layers

This commit is contained in:
nazeh
2024-12-22 12:03:06 +03:00
parent 96be5818a7
commit bcb8d39088
8 changed files with 86 additions and 129 deletions

View File

@@ -28,6 +28,9 @@ pub type EntriesTable = Database<Str, Bytes>;
pub const ENTRIES_TABLE: &str = "entries";
impl DB {
/// Write an entry by an author at a given path.
///
/// The path has to start with a forward slash `/`
pub fn write_entry(
&mut self,
public_key: &PublicKey,
@@ -36,10 +39,13 @@ impl DB {
EntryWriter::new(self, public_key, path)
}
/// Delete an entry by an author at a given path.
///
/// The path has to start with a forward slash `/`
pub fn delete_entry(&mut self, public_key: &PublicKey, path: &str) -> anyhow::Result<bool> {
let mut wtxn = self.env.write_txn()?;
let key = format!("{public_key}/{path}");
let key = format!("{public_key}{path}");
let deleted = if let Some(bytes) = self.tables.entries.get(&wtxn, &key)? {
let entry = Entry::deserialize(bytes)?;
@@ -62,7 +68,7 @@ impl DB {
let deleted_entry = self.tables.entries.delete(&mut wtxn, &key)?;
// create DELETE event
if path.starts_with("pub/") {
if path.starts_with("/pub/") {
let url = format!("pubky://{key}");
let event = Event::delete(&url);
@@ -92,7 +98,7 @@ impl DB {
public_key: &PublicKey,
path: &str,
) -> anyhow::Result<Option<Entry>> {
let key = format!("{public_key}/{path}");
let key = format!("{public_key}{path}");
if let Some(bytes) = self.tables.entries.get(txn, &key)? {
return Ok(Some(Entry::deserialize(bytes)?));
@@ -336,7 +342,7 @@ impl<'db> EntryWriter<'db> {
let buffer = File::create(&buffer_path)?;
let entry_key = format!("{public_key}/{path}");
let entry_key = format!("{public_key}{path}");
Ok(Self {
db,
@@ -345,7 +351,7 @@ impl<'db> EntryWriter<'db> {
buffer_path,
entry_key,
timestamp,
is_public: path.starts_with("pub/"),
is_public: path.starts_with("/pub/"),
})
}

View File

@@ -1,8 +1,8 @@
use std::{collections::HashMap, ops::Deref};
use std::collections::HashMap;
use axum::{
async_trait,
extract::{FromRequestParts, Path, Query},
extract::{FromRequestParts, Query},
http::{request::Parts, StatusCode},
response::{IntoResponse, Response},
RequestPartsExt,
@@ -39,60 +39,12 @@ where
))
.map_err(|e| e.into_response())?;
tracing::debug!(?pubky_host);
tracing::debug!(pubky_host = ?pubky_host.public_key().to_string());
Ok(pubky_host)
}
}
#[derive(Debug)]
pub struct EntryPath(pub(crate) String);
impl EntryPath {
pub fn as_str(&self) -> &str {
self.as_ref()
}
}
impl std::fmt::Display for EntryPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Deref for EntryPath {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[async_trait]
impl<S> FromRequestParts<S> for EntryPath
where
S: Send + Sync,
{
type Rejection = Response;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let params: Path<HashMap<String, String>> =
parts.extract().await.map_err(IntoResponse::into_response)?;
// TODO: enforce path limits like no trailing '/'
let path = params
.get("path")
.ok_or_else(|| (StatusCode::NOT_FOUND, "entry path missing").into_response())?;
if parts.uri.to_string().starts_with("/pub/") {
Ok(EntryPath(format!("pub/{}", path)))
} else {
Ok(EntryPath(path.to_string()))
}
}
}
#[derive(Debug)]
pub struct ListQueryParams {
pub limit: Option<u16>,

View File

@@ -69,11 +69,6 @@ where
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::<PubkyHost>() {
Some(pk) => pk,
None => {
@@ -101,17 +96,6 @@ where
}
}
/// 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,
@@ -120,8 +104,18 @@ fn authorize(
public_key: &PublicKey,
path: &str,
) -> Result<()> {
if path.starts_with("/pub/") && method == Method::GET {
if path == "/session" {
// Checking (or deleting) one's session is ok for everyone
return Ok(());
} else if path.starts_with("/pub/") {
if method == Method::GET {
return Ok(());
}
} else {
return Err(Error::new(
StatusCode::FORBIDDEN,
"Writing to directories other than '/pub/' is forbidden".into(),
));
}
let session_secret = session_secret_from_headers(headers, public_key)

View File

@@ -1,7 +1,7 @@
//! The controller part of the [crate::HomeserverCore]
use axum::{
routing::{delete, get, post},
routing::{get, post},
Router,
};
use tower_cookies::CookieManagerLayer;
@@ -9,28 +9,18 @@ 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;
mod tenants;
fn base() -> Router<AppState> {
Router::new()
.route("/", get(root::handler))
.route("/signup", post(auth::signup))
.route("/session", post(auth::signin))
// Routes for Pubky in the Hostname.
//
// The default and wortks with native Pubky client.
// - Session routes
.route("/session", get(auth::session))
.route("/session", delete(auth::signout))
// - Data routes
// Events
.route("/events/", get(feed::feed))
.layer(CookieManagerLayer::new())
// TODO: add size limit
// TODO: revisit if we enable streaming big payloads
// TODO: maybe add to a separate router (drive router?).
@@ -38,9 +28,9 @@ fn base() -> Router<AppState> {
pub fn create_app(state: AppState) -> Router {
base()
.merge(public::data_store_router(state.clone()))
.merge(tenants::router(state.clone()))
.layer(CookieManagerLayer::new())
.layer(CorsLayer::very_permissive())
.layer(TraceLayer::new_for_http())
.layer(PubkyHostLayer)
.with_state(state)
}

View File

@@ -1,21 +0,0 @@
use axum::{
extract::DefaultBodyLimit,
routing::{delete, get, head, put},
Router,
};
use crate::core::{layers::authz::AuthorizationLayer, AppState};
pub mod read;
pub mod write;
pub fn data_store_router(state: AppState) -> Router<AppState> {
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()))
}

View File

@@ -0,0 +1,38 @@
//! Per Tenant (user / Pubky) routes.
//!
//! Every route here is relative to a tenant's Pubky host,
//! as opposed to routes relative to the Homeserver's owner.
use axum::{
extract::DefaultBodyLimit,
routing::{delete, get, head, put},
Router,
};
use crate::core::{
layers::{authz::AuthorizationLayer, pubky_host::PubkyHostLayer},
AppState,
};
use super::auth;
pub mod read;
pub mod write;
pub fn router(state: AppState) -> Router<AppState> {
Router::new()
// - Datastore routes
.route("/pub/", get(read::get))
.route("/pub/*path", get(read::get))
.route("/pub/*path", head(read::head))
.route("/pub/*path", put(write::put))
.route("/pub/*path", delete(write::delete))
// - Session routes
.route("/session", get(auth::session))
.route("/session", delete(auth::signout))
// Layers
// TODO: different max size for sessions and other routes?
.layer(DefaultBodyLimit::max(100 * 1024 * 1024))
.layer(AuthorizationLayer::new(state.clone()))
.layer(PubkyHostLayer)
}

View File

@@ -1,6 +1,6 @@
use axum::{
body::Body,
extract::State,
extract::{OriginalUri, State},
http::{header, HeaderMap, HeaderValue, Response, StatusCode},
response::IntoResponse,
};
@@ -11,7 +11,7 @@ use std::str::FromStr;
use crate::core::{
database::tables::entries::Entry,
error::{Error, Result},
extractors::{EntryPath, ListQueryParams, PubkyHost},
extractors::{ListQueryParams, PubkyHost},
AppState,
};
@@ -19,7 +19,7 @@ pub async fn head(
State(state): State<AppState>,
pubky: PubkyHost,
headers: HeaderMap,
path: EntryPath,
path: OriginalUri,
) -> Result<impl IntoResponse> {
let rtxn = state.db.env.read_txn()?;
@@ -27,29 +27,22 @@ pub async fn head(
headers,
state
.db
.get_entry(&rtxn, pubky.public_key(), path.as_str())?,
.get_entry(&rtxn, pubky.public_key(), path.0.path())?,
None,
)
}
pub async fn list_root(
State(state): State<AppState>,
pubky: PubkyHost,
params: ListQueryParams,
) -> Result<impl IntoResponse> {
list(state, pubky.public_key(), "pub/", params)
}
pub async fn get(
State(state): State<AppState>,
headers: HeaderMap,
pubky: PubkyHost,
path: EntryPath,
path: OriginalUri,
params: ListQueryParams,
) -> Result<impl IntoResponse> {
let public_key = pubky.public_key().clone();
let path = path.0.path().to_string();
if path.as_str().ends_with('/') {
if path.ends_with('/') {
return list(state, &public_key, &path, params);
}
@@ -91,7 +84,7 @@ pub fn list(
) -> Result<Response<Body>> {
let txn = state.db.env.read_txn()?;
let path = format!("{public_key}/{path}");
let path = format!("{public_key}{path}");
if !state.db.contains_directory(&txn, &path)? {
return Err(Error::new(

View File

@@ -2,23 +2,28 @@ use std::io::Write;
use futures_util::stream::StreamExt;
use axum::{body::Body, extract::State, http::StatusCode, response::IntoResponse};
use axum::{
body::Body,
extract::{OriginalUri, State},
http::StatusCode,
response::IntoResponse,
};
use crate::core::{
error::{Error, Result},
extractors::{EntryPath, PubkyHost},
extractors::PubkyHost,
AppState,
};
pub async fn delete(
State(mut state): State<AppState>,
pubky: PubkyHost,
path: EntryPath,
path: OriginalUri,
) -> Result<impl IntoResponse> {
let public_key = pubky.public_key().clone();
// 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)?;
let deleted = state.db.delete_entry(&public_key, path.0.path())?;
if !deleted {
// TODO: if the path ends with `/` return a `CONFLICT` error?
@@ -31,12 +36,12 @@ pub async fn delete(
pub async fn put(
State(mut state): State<AppState>,
pubky: PubkyHost,
path: EntryPath,
path: OriginalUri,
body: Body,
) -> Result<impl IntoResponse> {
let public_key = pubky.public_key().clone();
let mut entry_writer = state.db.write_entry(&public_key, &path)?;
let mut entry_writer = state.db.write_entry(&public_key, path.0.path())?;
let mut stream = body.into_data_stream();
while let Some(next) = stream.next().await {