WebDAV permissions and logging (closes #1362)

This commit is contained in:
mdecimus
2025-05-09 15:47:09 +02:00
parent fe7d646966
commit 095c501a66
50 changed files with 1031 additions and 273 deletions

View File

@@ -4,6 +4,7 @@ version = "0.11.7"
edition = "2021"
[dependencies]
trc = { path = "../trc" }
hashify = "0.2.6"
quick-xml = "0.37.2"
calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] }
@@ -11,6 +12,7 @@ mail-parser = "0.10.2"
hyper = "1.6.0"
rkyv = { version = "0.8.10", features = ["little_endian"] }
chrono = { version = "0.4.40", features = ["serde"], optional = true }
compact_str = "0.9.0"
[dev-dependencies]
calcard = { path = "/Users/me/code/calcard", features = ["serde", "rkyv"] }

View File

@@ -4,6 +4,9 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::{CompactString, ToCompactString};
use trc::Value;
pub mod parser;
pub mod requests;
pub mod responses;
@@ -50,7 +53,7 @@ pub struct ResourceState<T: AsRef<str>> {
pub state_token: T,
}
#[derive(Debug, Default, PartialEq, Eq)]
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
pub enum Return {
Minimal,
Representation,
@@ -91,16 +94,87 @@ pub enum Depth {
None,
}
impl From<&RequestHeaders<'_>> for Value {
fn from(headers: &RequestHeaders<'_>) -> Self {
let mut values = Vec::with_capacity(4);
if headers.depth != Depth::None {
values.push(Value::String(CompactString::const_new("Depth")));
values.push(match headers.depth {
Depth::Zero => Value::Int(0),
Depth::One => Value::Int(1),
Depth::Infinity => Value::String(CompactString::const_new("infinity")),
Depth::None => Value::None,
});
}
if headers.timeout != Timeout::None {
values.push(Value::String(CompactString::const_new("Timeout")));
values.push(match headers.timeout {
Timeout::Infinite => Value::String(CompactString::const_new("infinite")),
Timeout::Second(n) => Value::Int(n as i64),
Timeout::None => Value::None,
});
}
for (name, header_value) in [
("Content-Type", headers.content_type),
("Destination", headers.destination),
("Lock-Token", headers.lock_token),
] {
if let Some(value) = header_value {
values.push(CompactString::const_new(name).into());
values.push(value.to_compact_string().into());
}
}
for (name, is_set) in [
("Overwrite", headers.overwrite_fail),
("No-Timezones", headers.no_timezones),
("Depth-No-Root", headers.depth_no_root),
] {
if is_set {
values.push(CompactString::const_new(name).into());
}
}
for if_ in &headers.if_ {
values.push(CompactString::const_new("If").into());
let mut if_values = Vec::with_capacity(if_.list.len() * 2 + 1);
if let Some(resource) = if_.resource {
if_values.push(Value::String(resource.to_compact_string()));
}
for condition in &if_.list {
match condition {
Condition::StateToken { is_not, token } => {
if *is_not {
if_values.push(Value::String(CompactString::const_new("!State-Token")));
} else {
if_values.push(Value::String(CompactString::const_new("State-Token")));
}
if_values.push(Value::String(token.to_compact_string()));
}
Condition::ETag { is_not, tag } => {
if *is_not {
if_values.push(Value::String(CompactString::const_new("!ETag")));
} else {
if_values.push(Value::String(CompactString::const_new("ETag")));
}
if_values.push(Value::String(tag.to_compact_string()));
}
Condition::Exists { is_not } => {
if *is_not {
if_values.push(Value::String(CompactString::const_new("!Exists")));
} else {
if_values.push(Value::String(CompactString::const_new("Exists")));
}
}
}
}
values.push(Value::Array(if_values));
}
Value::Array(values)
}
}
/*
Allow: OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, COPY, MOVE
Allow: MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, REPORT, ACL
DAV: 1, 2, 3, access-control, extended-mkcol
calendar-no-timezone
TODO:
Implemented:

View File

@@ -4,7 +4,10 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use std::{
borrow::Cow,
fmt::{Display, Formatter},
};
use quick_xml::events::BytesStart;
use tokenizer::Tokenizer;
@@ -152,3 +155,18 @@ impl Default for RawElement<'_> {
RawElement(BytesStart::new(""))
}
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Error::Xml(err) => write!(f, "XML error: {}", err),
Error::UnexpectedToken { expected, found } => {
write!(f, "Unexpected token: {found:?}")?;
if let Some(expected) = expected {
write!(f, ", expected: {expected:?}")?;
}
Ok(())
}
}
}
}

View File

@@ -288,6 +288,85 @@ impl MultiStatus {
}
}
impl BaseCondition {
pub fn display_name(&self) -> &'static str {
match self {
BaseCondition::NoConflictingLock(_) => "NoConflictingLock",
BaseCondition::CannotModifyProtectedProperty => "CannotModifyProtectedProperty",
BaseCondition::LockTokenSubmitted(_) => "LockTokenSubmitted",
BaseCondition::LockTokenMatchesRequestUri => "LockTokenMatchesRequestUri",
BaseCondition::NoExternalEntities => "NoExternalEntities",
BaseCondition::PreservedLiveProperties => "PreservedLiveProperties",
BaseCondition::PropFindFiniteDepth => "PropFindFiniteDepth",
BaseCondition::ResourceMustBeNull => "ResourceMustBeNull",
BaseCondition::NeedPrivileges(_) => "NeedPrivileges",
BaseCondition::NoAceConflict => "NoAceConflict",
BaseCondition::NoProtectedAceConflict => "NoProtectedAceConflict",
BaseCondition::NoInheritedAceConflict => "NoInheritedAceConflict",
BaseCondition::LimitedNumberOfAces => "LimitedNumberOfAces",
BaseCondition::DenyBeforeGrant => "DenyBeforeGrant",
BaseCondition::GrantOnly => "GrantOnly",
BaseCondition::NoInvert => "NoInvert",
BaseCondition::NoAbstract => "NoAbstract",
BaseCondition::NotSupportedPrivilege => "NotSupportedPrivilege",
BaseCondition::MissingRequiredPrincipal => "MissingRequiredPrincipal",
BaseCondition::RecognizedPrincipal => "RecognizedPrincipal",
BaseCondition::AllowedPrincipal => "AllowedPrincipal",
BaseCondition::NumberOfMatchesWithinLimit => "NumberOfMatchesWithinLimit",
BaseCondition::QuotaNotExceeded => "QuotaNotExceeded",
BaseCondition::ValidResourceType => "ValidResourceType",
BaseCondition::ValidSyncToken => "ValidSyncToken",
}
}
}
impl CalCondition {
pub fn display_name(&self) -> &'static str {
match self {
CalCondition::CalendarCollectionLocationOk => "CalendarCollectionLocationOk",
CalCondition::ValidCalendarData => "ValidCalendarData",
CalCondition::ValidFilter => "ValidFilter",
CalCondition::ValidCalendarObjectResource => "ValidCalendarObjectResource",
CalCondition::ValidTimezone => "ValidTimezone",
CalCondition::NoUidConflict(_) => "NoUidConflict",
CalCondition::InitializeCalendarCollection => "InitializeCalendarCollection",
CalCondition::SupportedCalendarData => "SupportedCalendarData",
CalCondition::SupportedFilter(_) => "SupportedFilter",
CalCondition::SupportedCollation(_) => "SupportedCollation",
CalCondition::MinDateTime => "MinDateTime",
CalCondition::MaxDateTime => "MaxDateTime",
CalCondition::MaxResourceSize(_) => "MaxResourceSize",
CalCondition::MaxInstances => "MaxInstances",
CalCondition::MaxAttendeesPerInstance => "MaxAttendeesPerInstance",
}
}
}
impl CardCondition {
pub fn display_name(&self) -> &'static str {
match self {
CardCondition::SupportedAddressData => "SupportedAddressData",
CardCondition::SupportedAddressDataConversion => "SupportedAddressDataConversion",
CardCondition::SupportedFilter(_) => "SupportedFilter",
CardCondition::SupportedCollation(_) => "SupportedCollation",
CardCondition::ValidAddressData => "ValidAddressData",
CardCondition::NoUidConflict(_) => "NoUidConflict",
CardCondition::MaxResourceSize(_) => "MaxResourceSize",
CardCondition::AddressBookCollectionLocationOk => "AddressBookCollectionLocationOk",
}
}
}
impl Condition {
pub fn display_name(&self) -> &'static str {
match self {
Condition::Base(base) => base.display_name(),
Condition::Cal(cal) => cal.display_name(),
Condition::Card(card) => card.display_name(),
}
}
}
#[cfg(test)]
mod serde_impl {
use super::Status;