backend: webdav tokens

This commit is contained in:
2025-10-29 00:12:58 +01:00
parent b4282c2b4b
commit d9b2297eca
12 changed files with 707 additions and 119 deletions
+52 -118
View File
@@ -8,6 +8,7 @@ use axum::Router;
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use diesel::prelude::*;
use diesel::OptionalExtension;
use diesel::PgConnection;
use futures_util::StreamExt;
use percent_encoding::{percent_decode_str, utf8_percent_encode, NON_ALPHANUMERIC};
@@ -15,7 +16,7 @@ use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
use quick_xml::Writer;
use uuid::Uuid;
use crate::auth::password;
use crate::auth::webdav_tokens::{find_active_token_by_secret, touch_webdav_token};
use crate::error::{AppError, AppResult};
use crate::models::{Document, DocumentVersion, Folder, User};
use crate::schema::{
@@ -29,17 +30,11 @@ use crate::utils::{http::inline_content_disposition, time::to_http_date};
const REALM: &str = "Papercrate WebDAV";
const DOWNLOAD_URL_TTL_SECONDS: u64 = 300;
#[derive(Clone, Debug)]
struct TenantEntry {
tenant_id: Uuid,
slug: String,
}
#[derive(Clone, Debug)]
struct WebDavContext {
tenant_id: Uuid,
_user_id: Uuid,
_username: String,
tenants: Vec<TenantEntry>,
}
pub fn create_router() -> Router<AppState> {
@@ -89,35 +84,20 @@ async fn handle_propfind(
let segments = parse_segments(path)?;
let resources = if segments.is_empty() {
build_account_root_resources(&context.tenants, depth)
} else {
let (requested_slug, remainder) = segments.split_first().unwrap();
let tenant_entry = match context
.tenants
.iter()
.find(|entry| entry.slug.eq_ignore_ascii_case(requested_slug))
{
Some(entry) => TenantEntry {
tenant_id: entry.tenant_id,
slug: entry.slug.clone(),
},
None => return Ok(not_found_response()),
};
let tenant_id = context.tenant_id;
let resolution = match resolve_path(state, &tenant_entry, remainder)? {
let resources = if segments.is_empty() {
let contents = fetch_folder_contents(state, tenant_id, None)?;
build_resources_for_folder(None, &[], &contents, depth)
} else {
let resolution = match resolve_path(state, tenant_id, &segments)? {
Some(resolved) => resolved,
None => return Ok(not_found_response()),
};
match resolution {
ResolvedPath::TenantRoot { chain } => {
let contents = fetch_folder_contents(state, tenant_entry.tenant_id, None)?;
build_resources_for_folder(None, &chain, &contents, depth)
}
ResolvedPath::Folder { folder, chain } => {
let contents =
fetch_folder_contents(state, tenant_entry.tenant_id, Some(folder.id))?;
let contents = fetch_folder_contents(state, tenant_id, Some(folder.id))?;
build_resources_for_folder(Some(&folder), &chain, &contents, depth)
}
ResolvedPath::Document {
@@ -151,29 +131,13 @@ async fn handle_get_or_head(
None => return Ok(unauthorized_response()),
};
let tenant_id = context.tenant_id;
let segments = parse_segments(path)?;
let (requested_slug, remainder) = match segments.split_first() {
Some(values) => values,
None => return Ok(method_not_allowed()),
};
let tenant_entry = match context
.tenants
.iter()
.find(|entry| entry.slug.eq_ignore_ascii_case(requested_slug))
{
Some(entry) => TenantEntry {
tenant_id: entry.tenant_id,
slug: entry.slug.clone(),
},
None => return Ok(not_found_response()),
};
if remainder.is_empty() {
if segments.is_empty() {
return Ok(method_not_allowed());
}
let resolution = match resolve_path(state, &tenant_entry, remainder)? {
let resolution = match resolve_path(state, tenant_id, &segments)? {
Some(resolved) => resolved,
None => return Ok(not_found_response()),
};
@@ -458,8 +422,8 @@ fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<Option<WebDavCo
}
};
let (username, password) = match credential_str.split_once(':') {
Some((username, password)) if !username.is_empty() => (username, password),
let (username, secret) = match credential_str.split_once(':') {
Some((username, secret)) if !username.is_empty() => (username, secret),
_ => return Ok(None),
};
@@ -478,35 +442,46 @@ fn authenticate(state: &AppState, headers: &HeaderMap) -> Result<Option<WebDavCo
Err(err) => return Err(AppError::from(err)),
};
let valid = password::verify_password(password, &user.password_hash)
.map_err(|_| AppError::internal("failed to verify password"))?;
let token = match find_active_token_by_secret(&mut conn, user.id, None, secret)? {
Some(token) => token,
None => {
tracing::warn!(%username, "webdav token invalid or expired");
return Ok(None);
}
};
if !valid {
tracing::warn!(%username, "webdav password invalid");
return Ok(None);
}
let tenant_rows: Vec<(Uuid, String)> = memberships_dsl::user_memberships
let tenant_row = memberships_dsl::user_memberships
.inner_join(tenant_dsl::tenants)
.filter(memberships_dsl::user_id.eq(user.id))
.filter(memberships_dsl::tenant_id.eq(token.tenant_id))
.select((tenant_dsl::id, tenant_dsl::slug))
.load(&mut conn)?;
.first::<(Uuid, String)>(&mut conn)
.optional()?;
if tenant_rows.is_empty() {
tracing::warn!(%username, "webdav user has no tenant memberships");
return Ok(None);
}
let (tenant_id, _slug) = match tenant_row {
Some(row) => row,
None => {
tracing::warn!(
%username,
tenant_id = %token.tenant_id,
"webdav token tenant membership missing"
);
return Ok(None);
}
};
let tenants: Vec<TenantEntry> = tenant_rows
.into_iter()
.map(|(tenant_id, slug)| TenantEntry { tenant_id, slug })
.collect();
touch_webdav_token(&mut conn, token.id)?;
tracing::debug!(%username, tenant_count = tenants.len(), "webdav login success");
tracing::debug!(
%username,
tenant_id = %tenant_id,
token_id = %token.id,
"webdav token login success"
);
Ok(Some(WebDavContext {
tenant_id,
_user_id: user.id,
_username: user.username,
tenants,
}))
}
@@ -564,37 +539,6 @@ fn build_resources_for_folder(
resources
}
fn build_account_root_resources(tenants: &[TenantEntry], depth: u8) -> Vec<DavResource> {
let mut resources = Vec::new();
resources.push(DavResource {
href: "/".to_string(),
display_name: "/".to_string(),
is_collection: true,
content_length: None,
content_type: None,
last_modified: None,
});
if depth == 0 {
return resources;
}
for tenant in tenants {
let href = build_href(&[tenant.slug.clone()], true);
resources.push(DavResource {
href,
display_name: tenant.slug.clone(),
is_collection: true,
content_length: None,
content_type: None,
last_modified: None,
});
}
resources
}
fn build_resources_for_document(
chain: &[String],
document: &Document,
@@ -717,9 +661,6 @@ struct DavResource {
last_modified: Option<String>,
}
enum ResolvedPath {
TenantRoot {
chain: Vec<String>,
},
Folder {
folder: Folder,
chain: Vec<String>,
@@ -733,23 +674,18 @@ enum ResolvedPath {
fn resolve_path(
state: &AppState,
tenant: &TenantEntry,
tenant_id: Uuid,
segments: &[String],
) -> AppResult<Option<ResolvedPath>> {
let mut conn = state.db_for_tenant(tenant.tenant_id)?;
let mut conn = state.db_for_tenant(tenant_id)?;
let mut parent_id: Option<Uuid> = None;
let mut chain: Vec<String> = vec![tenant.slug.clone()];
let mut chain: Vec<String> = Vec::new();
let mut current_folder: Option<Folder> = None;
if segments.is_empty() {
return Ok(Some(ResolvedPath::TenantRoot { chain }));
}
for (index, segment) in segments.iter().enumerate() {
let is_last = index == segments.len() - 1;
if let Some(folder) = find_folder_by_name(&mut conn, tenant.tenant_id, parent_id, segment)?
{
if let Some(folder) = find_folder_by_name(&mut conn, tenant_id, parent_id, segment)? {
chain.push(folder.name.clone());
if is_last {
return Ok(Some(ResolvedPath::Folder { folder, chain }));
@@ -761,7 +697,7 @@ fn resolve_path(
if is_last {
if let Some((document, version)) =
find_document_by_filename(&mut conn, tenant.tenant_id, parent_id, segment)?
find_document_by_filename(&mut conn, tenant_id, parent_id, segment)?
{
chain.push(document.filename.clone());
return Ok(Some(ResolvedPath::Document {
@@ -773,7 +709,7 @@ fn resolve_path(
}
if let Ok(uuid) = Uuid::parse_str(segment) {
if let Some(folder) = find_folder_by_id(&mut conn, tenant.tenant_id, uuid)? {
if let Some(folder) = find_folder_by_id(&mut conn, tenant_id, uuid)? {
if folder.parent_id != parent_id {
return Ok(None);
}
@@ -786,9 +722,7 @@ fn resolve_path(
continue;
}
if let Some((document, version)) =
find_document_by_id(&mut conn, tenant.tenant_id, uuid)?
{
if let Some((document, version)) = find_document_by_id(&mut conn, tenant_id, uuid)? {
if document.folder_id != parent_id {
return Ok(None);
}