mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-17 04:09:24 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35e78bfb44 | ||
|
|
b6ef2b4c05 | ||
|
|
32bc8c707d | ||
|
|
1757bbee13 | ||
|
|
4dbc316e64 | ||
|
|
4705170dbc | ||
|
|
0e2f08d7f2 | ||
|
|
feb8b3ff09 | ||
|
|
41d5c72e4e | ||
|
|
89adbcf13f |
20
.sqlx/query-3b00b59f047e534a7f7f654984dc880f4aa9281aae5974722d2f22ec6d15cb32.json
generated
Normal file
20
.sqlx/query-3b00b59f047e534a7f7f654984dc880f4aa9281aae5974722d2f22ec6d15cb32.json
generated
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "SELECT principal FROM memberships WHERE member_of = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "principal",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "3b00b59f047e534a7f7f654984dc880f4aa9281aae5974722d2f22ec6d15cb32"
|
||||||
|
}
|
||||||
24
Cargo.lock
generated
24
Cargo.lock
generated
@@ -2703,6 +2703,8 @@ dependencies = [
|
|||||||
"rustical_xml",
|
"rustical_xml",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
|
"strum",
|
||||||
|
"strum_macros",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
@@ -2733,6 +2735,8 @@ dependencies = [
|
|||||||
"rustical_store",
|
"rustical_store",
|
||||||
"rustical_xml",
|
"rustical_xml",
|
||||||
"serde",
|
"serde",
|
||||||
|
"strum",
|
||||||
|
"strum_macros",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
@@ -2758,6 +2762,7 @@ dependencies = [
|
|||||||
"quick-xml",
|
"quick-xml",
|
||||||
"rustical_xml",
|
"rustical_xml",
|
||||||
"serde",
|
"serde",
|
||||||
|
"strum",
|
||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
@@ -3435,6 +3440,25 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.27.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.27.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
|
||||||
|
dependencies = [
|
||||||
|
"heck",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
|
|||||||
@@ -29,4 +29,4 @@ a CalDAV/CardDAV server
|
|||||||
- DAVx5,
|
- DAVx5,
|
||||||
- GNOME Accounts, GNOME Calendar, GNOME Contacts
|
- GNOME Accounts, GNOME Calendar, GNOME Contacts
|
||||||
- Evolution
|
- Evolution
|
||||||
- Apple Calendar (known issue: If a user is member of multiple groups then Apple Calendar just randomly selects a calendar home)
|
- Apple Calendar
|
||||||
|
|||||||
@@ -35,3 +35,5 @@ rustical_ical.workspace = true
|
|||||||
http.workspace = true
|
http.workspace = true
|
||||||
headers.workspace = true
|
headers.workspace = true
|
||||||
tower-http.workspace = true
|
tower-http.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
strum_macros.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
use rustical_ical::CalendarObjectType;
|
use rustical_ical::CalendarObjectType;
|
||||||
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||||
|
use strum_macros::VariantArray;
|
||||||
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, From, Into)]
|
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, From, Into)]
|
||||||
pub struct SupportedCalendarComponent {
|
pub struct SupportedCalendarComponent {
|
||||||
@@ -58,39 +59,12 @@ pub struct SupportedCalendarData {
|
|||||||
calendar_data: CalendarData,
|
calendar_data: CalendarData,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
#[derive(Debug, Clone, XmlSerialize, PartialEq, VariantArray)]
|
||||||
pub enum ReportMethod {
|
pub enum ReportMethod {
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
CalendarQuery,
|
CalendarQuery,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
CalendarMultiget,
|
CalendarMultiget,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
SyncCollection,
|
SyncCollection,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
|
||||||
pub struct ReportWrapper {
|
|
||||||
report: ReportMethod,
|
|
||||||
}
|
|
||||||
|
|
||||||
// RFC 3253 section-3.1.5
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
|
||||||
pub struct SupportedReportSet {
|
|
||||||
#[xml(flatten)]
|
|
||||||
supported_report: Vec<ReportWrapper>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SupportedReportSet {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
supported_report: vec![
|
|
||||||
ReportWrapper {
|
|
||||||
report: ReportMethod::CalendarQuery,
|
|
||||||
},
|
|
||||||
ReportWrapper {
|
|
||||||
report: ReportMethod::CalendarMultiget,
|
|
||||||
},
|
|
||||||
ReportWrapper {
|
|
||||||
report: ReportMethod::SyncCollection,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData, SupportedReportSet};
|
use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData};
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
use crate::calendar::prop::ReportMethod;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
use rustical_dav::extensions::{
|
use rustical_dav::extensions::{
|
||||||
@@ -7,7 +8,7 @@ use rustical_dav::extensions::{
|
|||||||
};
|
};
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
use rustical_dav::privileges::UserPrivilegeSet;
|
||||||
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||||
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
|
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedReportSet};
|
||||||
use rustical_dav_push::DavPushExtension;
|
use rustical_dav_push::DavPushExtension;
|
||||||
use rustical_ical::CalDateTime;
|
use rustical_ical::CalDateTime;
|
||||||
use rustical_store::Calendar;
|
use rustical_store::Calendar;
|
||||||
@@ -40,8 +41,8 @@ pub enum CalendarProp {
|
|||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
MaxResourceSize(i64),
|
MaxResourceSize(i64),
|
||||||
#[xml(skip_deserializing)]
|
#[xml(skip_deserializing)]
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
SupportedReportSet(SupportedReportSet),
|
SupportedReportSet(SupportedReportSet<ReportMethod>),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALENDARSERVER")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALENDARSERVER")]
|
||||||
Source(Option<HrefElement>),
|
Source(Option<HrefElement>),
|
||||||
#[xml(skip_deserializing)]
|
#[xml(skip_deserializing)]
|
||||||
@@ -150,7 +151,7 @@ impl Resource for CalendarResource {
|
|||||||
}
|
}
|
||||||
CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10000000),
|
CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10000000),
|
||||||
CalendarPropName::SupportedReportSet => {
|
CalendarPropName::SupportedReportSet => {
|
||||||
CalendarProp::SupportedReportSet(SupportedReportSet::default())
|
CalendarProp::SupportedReportSet(SupportedReportSet::all())
|
||||||
}
|
}
|
||||||
CalendarPropName::Source => CalendarProp::Source(
|
CalendarPropName::Source => CalendarProp::Source(
|
||||||
self.cal.subscription_url.to_owned().map(HrefElement::from),
|
self.cal.subscription_url.to_owned().map(HrefElement::from),
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourc
|
|||||||
type Principal = User;
|
type Principal = User;
|
||||||
type PrincipalUri = CalDavPrincipalUri;
|
type PrincipalUri = CalDavPrincipalUri;
|
||||||
|
|
||||||
const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
|
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy";
|
||||||
|
|
||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ use crate::Error;
|
|||||||
use rustical_dav::extensions::CommonPropertiesExtension;
|
use rustical_dav::extensions::CommonPropertiesExtension;
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
use rustical_dav::privileges::UserPrivilegeSet;
|
||||||
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||||
use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
|
use rustical_dav::xml::{
|
||||||
|
GroupMemberSet, GroupMembership, Resourcetype, ResourcetypeInner, SupportedReportSet,
|
||||||
|
};
|
||||||
use rustical_store::auth::User;
|
use rustical_store::auth::User;
|
||||||
|
|
||||||
mod service;
|
mod service;
|
||||||
@@ -13,6 +15,7 @@ pub use prop::*;
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct PrincipalResource {
|
pub struct PrincipalResource {
|
||||||
principal: User,
|
principal: User,
|
||||||
|
members: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceName for PrincipalResource {
|
impl ResourceName for PrincipalResource {
|
||||||
@@ -32,6 +35,11 @@ impl Resource for PrincipalResource {
|
|||||||
Resourcetype(&[
|
Resourcetype(&[
|
||||||
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
|
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
|
||||||
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"),
|
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"),
|
||||||
|
// https://github.com/apple/ccs-calendarserver/blob/13c706b985fb728b9aab42dc0fef85aae21921c3/doc/Extensions/caldav-proxy.txt
|
||||||
|
ResourcetypeInner(
|
||||||
|
Some(rustical_dav::namespace::NS_CALENDARSERVER),
|
||||||
|
"calendar-proxy-write",
|
||||||
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,14 +51,6 @@ impl Resource for PrincipalResource {
|
|||||||
) -> Result<Self::Prop, Self::Error> {
|
) -> Result<Self::Prop, Self::Error> {
|
||||||
let principal_url = puri.principal_uri(&self.principal.id);
|
let principal_url = puri.principal_uri(&self.principal.id);
|
||||||
|
|
||||||
let home_set = CalendarHomeSet(
|
|
||||||
self.principal
|
|
||||||
.memberships()
|
|
||||||
.into_iter()
|
|
||||||
.map(|principal| puri.principal_uri(principal).into())
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(match prop {
|
Ok(match prop {
|
||||||
PrincipalPropWrapperName::Principal(prop) => {
|
PrincipalPropWrapperName::Principal(prop) => {
|
||||||
PrincipalPropWrapper::Principal(match prop {
|
PrincipalPropWrapper::Principal(match prop {
|
||||||
@@ -60,10 +60,20 @@ impl Resource for PrincipalResource {
|
|||||||
PrincipalPropName::PrincipalUrl => {
|
PrincipalPropName::PrincipalUrl => {
|
||||||
PrincipalProp::PrincipalUrl(principal_url.into())
|
PrincipalProp::PrincipalUrl(principal_url.into())
|
||||||
}
|
}
|
||||||
PrincipalPropName::CalendarHomeSet => PrincipalProp::CalendarHomeSet(home_set),
|
PrincipalPropName::CalendarHomeSet => {
|
||||||
|
PrincipalProp::CalendarHomeSet(principal_url.into())
|
||||||
|
}
|
||||||
PrincipalPropName::CalendarUserAddressSet => {
|
PrincipalPropName::CalendarUserAddressSet => {
|
||||||
PrincipalProp::CalendarUserAddressSet(principal_url.into())
|
PrincipalProp::CalendarUserAddressSet(principal_url.into())
|
||||||
}
|
}
|
||||||
|
PrincipalPropName::GroupMemberSet => {
|
||||||
|
PrincipalProp::GroupMemberSet(GroupMemberSet(
|
||||||
|
self.members
|
||||||
|
.iter()
|
||||||
|
.map(|principal| puri.principal_uri(principal).into())
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
PrincipalPropName::GroupMembership => {
|
PrincipalPropName::GroupMembership => {
|
||||||
PrincipalProp::GroupMembership(GroupMembership(
|
PrincipalProp::GroupMembership(GroupMembership(
|
||||||
self.principal
|
self.principal
|
||||||
@@ -74,10 +84,11 @@ impl Resource for PrincipalResource {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
PrincipalPropName::AlternateUriSet => PrincipalProp::AlternateUriSet,
|
PrincipalPropName::AlternateUriSet => PrincipalProp::AlternateUriSet,
|
||||||
PrincipalPropName::PrincipalCollectionSet => {
|
// PrincipalPropName::PrincipalCollectionSet => {
|
||||||
PrincipalProp::PrincipalCollectionSet(PrincipalCollectionSet(
|
// PrincipalProp::PrincipalCollectionSet(puri.principal_collection().into())
|
||||||
puri.principal_collection().into(),
|
// }
|
||||||
))
|
PrincipalPropName::SupportedReportSet => {
|
||||||
|
PrincipalProp::SupportedReportSet(SupportedReportSet::all())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use rustical_dav::{extensions::CommonPropertiesProp, xml::HrefElement};
|
use rustical_dav::{
|
||||||
|
extensions::CommonPropertiesProp,
|
||||||
|
xml::{GroupMemberSet, GroupMembership, HrefElement, SupportedReportSet},
|
||||||
|
};
|
||||||
use rustical_store::auth::user::PrincipalType;
|
use rustical_store::auth::user::PrincipalType;
|
||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
|
use strum_macros::VariantArray;
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||||
#[xml(unit_variants_ident = "PrincipalPropName")]
|
#[xml(unit_variants_ident = "PrincipalPropName")]
|
||||||
@@ -16,14 +20,18 @@ pub enum PrincipalProp {
|
|||||||
PrincipalUrl(HrefElement),
|
PrincipalUrl(HrefElement),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
GroupMembership(GroupMembership),
|
GroupMembership(GroupMembership),
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
|
GroupMemberSet(GroupMemberSet),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"alternate-URI-set")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"alternate-URI-set")]
|
||||||
AlternateUriSet,
|
AlternateUriSet,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
// #[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
PrincipalCollectionSet(PrincipalCollectionSet),
|
// PrincipalCollectionSet(HrefElement),
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
|
||||||
|
SupportedReportSet(SupportedReportSet<ReportMethod>),
|
||||||
|
|
||||||
// CalDAV (RFC 4791)
|
// CalDAV (RFC 4791)
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
|
||||||
CalendarHomeSet(CalendarHomeSet),
|
CalendarHomeSet(HrefElement),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||||
@@ -33,11 +41,9 @@ pub enum PrincipalPropWrapper {
|
|||||||
Common(CommonPropertiesProp),
|
Common(CommonPropertiesProp),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
#[derive(XmlSerialize, PartialEq, Clone, VariantArray)]
|
||||||
pub struct CalendarHomeSet(#[xml(ty = "untagged", flatten)] pub(super) Vec<HrefElement>);
|
pub enum ReportMethod {
|
||||||
|
// We don't actually support principal-match
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
pub struct GroupMembership(#[xml(ty = "untagged", flatten)] pub(super) Vec<HrefElement>);
|
PrincipalMatch,
|
||||||
|
}
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
|
||||||
pub struct PrincipalCollectionSet(#[xml(ty = "untagged")] pub(super) HrefElement);
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
|
|||||||
type Principal = User;
|
type Principal = User;
|
||||||
type PrincipalUri = CalDavPrincipalUri;
|
type PrincipalUri = CalDavPrincipalUri;
|
||||||
|
|
||||||
const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
|
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy";
|
||||||
|
|
||||||
async fn get_resource(
|
async fn get_resource(
|
||||||
&self,
|
&self,
|
||||||
@@ -54,7 +54,10 @@ impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Resour
|
|||||||
.get_principal(principal)
|
.get_principal(principal)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(crate::Error::NotFound)?;
|
.ok_or(crate::Error::NotFound)?;
|
||||||
Ok(PrincipalResource { principal: user })
|
Ok(PrincipalResource {
|
||||||
|
members: self.auth_provider.list_members(&user.id).await?,
|
||||||
|
principal: user,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_members(
|
async fn get_members(
|
||||||
|
|||||||
@@ -32,3 +32,5 @@ http.workspace = true
|
|||||||
tower-http.workspace = true
|
tower-http.workspace = true
|
||||||
percent-encoding.workspace = true
|
percent-encoding.workspace = true
|
||||||
ical.workspace = true
|
ical.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
strum_macros.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
use rustical_dav::extensions::{CommonPropertiesProp, SyncTokenExtensionProp};
|
use rustical_dav::{
|
||||||
|
extensions::{CommonPropertiesProp, SyncTokenExtensionProp},
|
||||||
|
xml::SupportedReportSet,
|
||||||
|
};
|
||||||
use rustical_dav_push::DavPushExtensionProp;
|
use rustical_dav_push::DavPushExtensionProp;
|
||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
|
use strum_macros::VariantArray;
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||||
#[xml(unit_variants_ident = "AddressbookPropName")]
|
#[xml(unit_variants_ident = "AddressbookPropName")]
|
||||||
@@ -10,8 +14,8 @@ pub enum AddressbookProp {
|
|||||||
AddressbookDescription(Option<String>),
|
AddressbookDescription(Option<String>),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
||||||
SupportedAddressData(SupportedAddressData),
|
SupportedAddressData(SupportedAddressData),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
|
||||||
SupportedReportSet(SupportedReportSet),
|
SupportedReportSet(SupportedReportSet<ReportMethod>),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
MaxResourceSize(i64),
|
MaxResourceSize(i64),
|
||||||
}
|
}
|
||||||
@@ -56,37 +60,10 @@ impl Default for SupportedAddressData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
#[derive(Debug, Clone, XmlSerialize, PartialEq, VariantArray)]
|
||||||
pub enum ReportMethod {
|
pub enum ReportMethod {
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
AddressbookMultiget,
|
AddressbookMultiget,
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
SyncCollection,
|
SyncCollection,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
|
||||||
pub struct SupportedReportWrapper {
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
|
||||||
report: ReportMethod,
|
|
||||||
}
|
|
||||||
|
|
||||||
// RFC 3253 section-3.1.5
|
|
||||||
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
|
||||||
pub struct SupportedReportSet {
|
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
|
|
||||||
supported_report: &'static [SupportedReportWrapper],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SupportedReportSet {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
supported_report: &[
|
|
||||||
SupportedReportWrapper {
|
|
||||||
report: ReportMethod::AddressbookMultiget,
|
|
||||||
},
|
|
||||||
SupportedReportWrapper {
|
|
||||||
report: ReportMethod::SyncCollection,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::prop::{SupportedAddressData, SupportedReportSet};
|
use super::prop::SupportedAddressData;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
use crate::addressbook::prop::{
|
use crate::addressbook::prop::{
|
||||||
AddressbookProp, AddressbookPropName, AddressbookPropWrapper, AddressbookPropWrapperName,
|
AddressbookProp, AddressbookPropName, AddressbookPropWrapper, AddressbookPropWrapperName,
|
||||||
@@ -7,7 +7,7 @@ use derive_more::derive::{From, Into};
|
|||||||
use rustical_dav::extensions::{CommonPropertiesExtension, SyncTokenExtension};
|
use rustical_dav::extensions::{CommonPropertiesExtension, SyncTokenExtension};
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
use rustical_dav::privileges::UserPrivilegeSet;
|
||||||
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||||
use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
|
use rustical_dav::xml::{Resourcetype, ResourcetypeInner, SupportedReportSet};
|
||||||
use rustical_dav_push::DavPushExtension;
|
use rustical_dav_push::DavPushExtension;
|
||||||
use rustical_store::Addressbook;
|
use rustical_store::Addressbook;
|
||||||
use rustical_store::auth::User;
|
use rustical_store::auth::User;
|
||||||
@@ -60,7 +60,7 @@ impl Resource for AddressbookResource {
|
|||||||
AddressbookProp::MaxResourceSize(10000000)
|
AddressbookProp::MaxResourceSize(10000000)
|
||||||
}
|
}
|
||||||
AddressbookPropName::SupportedReportSet => {
|
AddressbookPropName::SupportedReportSet => {
|
||||||
AddressbookProp::SupportedReportSet(SupportedReportSet::default())
|
AddressbookProp::SupportedReportSet(SupportedReportSet::all())
|
||||||
}
|
}
|
||||||
AddressbookPropName::AddressbookDescription => {
|
AddressbookPropName::AddressbookDescription => {
|
||||||
AddressbookProp::AddressbookDescription(self.0.description.to_owned())
|
AddressbookProp::AddressbookDescription(self.0.description.to_owned())
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ use crate::Error;
|
|||||||
use rustical_dav::extensions::CommonPropertiesExtension;
|
use rustical_dav::extensions::CommonPropertiesExtension;
|
||||||
use rustical_dav::privileges::UserPrivilegeSet;
|
use rustical_dav::privileges::UserPrivilegeSet;
|
||||||
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
|
||||||
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner};
|
use rustical_dav::xml::{
|
||||||
|
GroupMemberSet, GroupMembership, HrefElement, Resourcetype, ResourcetypeInner,
|
||||||
|
};
|
||||||
use rustical_store::auth::User;
|
use rustical_store::auth::User;
|
||||||
|
|
||||||
mod service;
|
mod service;
|
||||||
@@ -13,6 +15,7 @@ pub use prop::*;
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct PrincipalResource {
|
pub struct PrincipalResource {
|
||||||
principal: User,
|
principal: User,
|
||||||
|
members: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceName for PrincipalResource {
|
impl ResourceName for PrincipalResource {
|
||||||
@@ -41,23 +44,14 @@ impl Resource for PrincipalResource {
|
|||||||
user: &User,
|
user: &User,
|
||||||
prop: &PrincipalPropWrapperName,
|
prop: &PrincipalPropWrapperName,
|
||||||
) -> Result<Self::Prop, Self::Error> {
|
) -> Result<Self::Prop, Self::Error> {
|
||||||
let principal_href = HrefElement::new(puri.principal_uri(&user.id));
|
let principal_href = HrefElement::new(puri.principal_uri(&self.principal.id));
|
||||||
|
|
||||||
let home_set = AddressbookHomeSet(
|
|
||||||
self.principal
|
|
||||||
.memberships()
|
|
||||||
.into_iter()
|
|
||||||
.map(|principal| puri.principal_uri(principal))
|
|
||||||
.map(HrefElement::new)
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(match prop {
|
Ok(match prop {
|
||||||
PrincipalPropWrapperName::Principal(prop) => {
|
PrincipalPropWrapperName::Principal(prop) => {
|
||||||
PrincipalPropWrapper::Principal(match prop {
|
PrincipalPropWrapper::Principal(match prop {
|
||||||
PrincipalPropName::PrincipalUrl => PrincipalProp::PrincipalUrl(principal_href),
|
PrincipalPropName::PrincipalUrl => PrincipalProp::PrincipalUrl(principal_href),
|
||||||
PrincipalPropName::AddressbookHomeSet => {
|
PrincipalPropName::AddressbookHomeSet => {
|
||||||
PrincipalProp::AddressbookHomeSet(home_set)
|
PrincipalProp::AddressbookHomeSet(principal_href)
|
||||||
}
|
}
|
||||||
PrincipalPropName::PrincipalAddress => PrincipalProp::PrincipalAddress(None),
|
PrincipalPropName::PrincipalAddress => PrincipalProp::PrincipalAddress(None),
|
||||||
PrincipalPropName::GroupMembership => {
|
PrincipalPropName::GroupMembership => {
|
||||||
@@ -69,11 +63,17 @@ impl Resource for PrincipalResource {
|
|||||||
.collect(),
|
.collect(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
PrincipalPropName::GroupMemberSet => {
|
||||||
|
PrincipalProp::GroupMemberSet(GroupMemberSet(
|
||||||
|
self.members
|
||||||
|
.iter()
|
||||||
|
.map(|principal| puri.principal_uri(principal).into())
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
PrincipalPropName::AlternateUriSet => PrincipalProp::AlternateUriSet,
|
PrincipalPropName::AlternateUriSet => PrincipalProp::AlternateUriSet,
|
||||||
PrincipalPropName::PrincipalCollectionSet => {
|
PrincipalPropName::PrincipalCollectionSet => {
|
||||||
PrincipalProp::PrincipalCollectionSet(PrincipalCollectionSet(
|
PrincipalProp::PrincipalCollectionSet(puri.principal_collection().into())
|
||||||
puri.principal_collection().into(),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
use rustical_dav::{extensions::CommonPropertiesProp, xml::HrefElement};
|
use rustical_dav::{
|
||||||
|
extensions::CommonPropertiesProp,
|
||||||
|
xml::{GroupMemberSet, GroupMembership, HrefElement},
|
||||||
|
};
|
||||||
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
|
||||||
@@ -10,14 +13,16 @@ pub enum PrincipalProp {
|
|||||||
PrincipalUrl(HrefElement),
|
PrincipalUrl(HrefElement),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
GroupMembership(GroupMembership),
|
GroupMembership(GroupMembership),
|
||||||
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
|
GroupMemberSet(GroupMemberSet),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"alternate-URI-set")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"alternate-URI-set")]
|
||||||
AlternateUriSet,
|
AlternateUriSet,
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
|
||||||
PrincipalCollectionSet(PrincipalCollectionSet),
|
PrincipalCollectionSet(HrefElement),
|
||||||
|
|
||||||
// CardDAV (RFC 6352)
|
// CardDAV (RFC 6352)
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
AddressbookHomeSet(AddressbookHomeSet),
|
AddressbookHomeSet(HrefElement),
|
||||||
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
|
||||||
PrincipalAddress(Option<HrefElement>),
|
PrincipalAddress(Option<HrefElement>),
|
||||||
}
|
}
|
||||||
@@ -28,12 +33,3 @@ pub enum PrincipalPropWrapper {
|
|||||||
Principal(PrincipalProp),
|
Principal(PrincipalProp),
|
||||||
Common(CommonPropertiesProp),
|
Common(CommonPropertiesProp),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
|
||||||
pub struct AddressbookHomeSet(#[xml(ty = "untagged", flatten)] pub(super) Vec<HrefElement>);
|
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
|
||||||
pub struct GroupMembership(#[xml(ty = "untagged", flatten)] pub(super) Vec<HrefElement>);
|
|
||||||
|
|
||||||
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
|
||||||
pub struct PrincipalCollectionSet(#[xml(ty = "untagged")] pub(super) HrefElement);
|
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ impl<A: AddressbookStore, AP: AuthenticationProvider, S: SubscriptionStore> Reso
|
|||||||
.get_principal(principal)
|
.get_principal(principal)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(crate::Error::NotFound)?;
|
.ok_or(crate::Error::NotFound)?;
|
||||||
Ok(PrincipalResource { principal: user })
|
Ok(PrincipalResource {
|
||||||
|
members: self.auth_provider.list_members(&user.id).await?,
|
||||||
|
principal: user,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_members(
|
async fn get_members(
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ tracing.workspace = true
|
|||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
http.workspace = true
|
http.workspace = true
|
||||||
headers.workspace = true
|
headers.workspace = true
|
||||||
|
strum.workspace = true
|
||||||
|
|||||||
8
crates/dav/src/xml/group.rs
Normal file
8
crates/dav/src/xml/group.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use crate::xml::HrefElement;
|
||||||
|
use rustical_xml::{XmlDeserialize, XmlSerialize};
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
||||||
|
pub struct GroupMembership(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
||||||
|
|
||||||
|
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)]
|
||||||
|
pub struct GroupMemberSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
|
||||||
@@ -11,3 +11,7 @@ pub use tag_list::TagList;
|
|||||||
mod error;
|
mod error;
|
||||||
pub mod sync_collection;
|
pub mod sync_collection;
|
||||||
pub use error::ErrorElement;
|
pub use error::ErrorElement;
|
||||||
|
mod report_set;
|
||||||
|
pub use report_set::SupportedReportSet;
|
||||||
|
mod group;
|
||||||
|
pub use group::*;
|
||||||
|
|||||||
34
crates/dav/src/xml/report_set.rs
Normal file
34
crates/dav/src/xml/report_set.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use rustical_xml::XmlSerialize;
|
||||||
|
use strum::VariantArray;
|
||||||
|
|
||||||
|
// RFC 3253 section-3.1.5
|
||||||
|
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
||||||
|
pub struct SupportedReportSet<T: XmlSerialize + 'static> {
|
||||||
|
#[xml(flatten)]
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||||
|
supported_report: Vec<ReportWrapper<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: XmlSerialize + Clone + 'static> SupportedReportSet<T> {
|
||||||
|
pub fn new(methods: Vec<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
supported_report: methods
|
||||||
|
.into_iter()
|
||||||
|
.map(|method| ReportWrapper { report: method })
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn all() -> Self
|
||||||
|
where
|
||||||
|
T: VariantArray,
|
||||||
|
{
|
||||||
|
Self::new(T::VARIANTS.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
|
||||||
|
pub struct ReportWrapper<T: XmlSerialize> {
|
||||||
|
#[xml(ns = "crate::namespace::NS_DAV")]
|
||||||
|
report: T,
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ pub trait AuthenticationProvider: Send + Sync + 'static {
|
|||||||
|
|
||||||
async fn add_membership(&self, principal: &str, member_of: &str) -> Result<(), Error>;
|
async fn add_membership(&self, principal: &str, member_of: &str) -> Result<(), Error>;
|
||||||
async fn remove_membership(&self, principal: &str, member_of: &str) -> Result<(), Error>;
|
async fn remove_membership(&self, principal: &str, member_of: &str) -> Result<(), Error>;
|
||||||
|
async fn list_members(&self, principal: &str) -> Result<Vec<String>, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use middleware::AuthenticationMiddleware;
|
pub use middleware::AuthenticationMiddleware;
|
||||||
|
|||||||
@@ -249,4 +249,18 @@ impl AuthenticationProvider for SqlitePrincipalStore {
|
|||||||
.map_err(crate::Error::from)?;
|
.map_err(crate::Error::from)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
async fn list_members(&self, principal: &str) -> Result<Vec<String>, Error> {
|
||||||
|
Ok(sqlx::query!(
|
||||||
|
r#"SELECT principal FROM memberships WHERE member_of = ?"#,
|
||||||
|
principal
|
||||||
|
)
|
||||||
|
.fetch_all(&self.db)
|
||||||
|
.await
|
||||||
|
.map_err(crate::Error::from)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|record| record.principal)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,11 @@ impl Enum {
|
|||||||
let prefix = ns
|
let prefix = ns
|
||||||
.map(|ns| namespaces.get(&ns))
|
.map(|ns| namespaces.get(&ns))
|
||||||
.unwrap_or(None)
|
.unwrap_or(None)
|
||||||
.map(|prefix| [*prefix, b":"].concat());
|
.map(|prefix| if !prefix.is_empty() {
|
||||||
|
[*prefix, b":"].concat()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
});
|
||||||
let has_prefix = prefix.is_some();
|
let has_prefix = prefix.is_some();
|
||||||
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
|
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
|
||||||
let qname = tagname.as_ref().map(|tagname| ::quick_xml::name::QName(tagname));
|
let qname = tagname.as_ref().map(|tagname| ::quick_xml::name::QName(tagname));
|
||||||
|
|||||||
@@ -25,4 +25,4 @@ If you still want to play around with it in its current state, absolutely feel f
|
|||||||
- DAVx5,
|
- DAVx5,
|
||||||
- GNOME Accounts, GNOME Calendar, GNOME Contacts
|
- GNOME Accounts, GNOME Calendar, GNOME Contacts
|
||||||
- Evolution
|
- Evolution
|
||||||
- Apple Calendar (known issue: If a user is member of multiple groups then Apple Calendar just randomly selects a calendar home)
|
- Apple Calendar
|
||||||
|
|||||||
@@ -29,8 +29,7 @@ docker run --rm -it -v YOUR_DATA_DIR:/var/lib/rustical/ ghcr.io/lennart-k/rustic
|
|||||||
This is also the place to set up **groups**.
|
This is also the place to set up **groups**.
|
||||||
Groups and rooms are also just principals and you can specify them as such using the `--principal-type` parameter.
|
Groups and rooms are also just principals and you can specify them as such using the `--principal-type` parameter.
|
||||||
To assign a user to a group you can use the `rustical membership` command. Being a member to a principal means that you can completely act on their behalf and see their collections.
|
To assign a user to a group you can use the `rustical membership` command. Being a member to a principal means that you can completely act on their behalf and see their collections.
|
||||||
|
**Note:** Many clients don't support autodiscovery of principals a user is a member of. In that case you'd have to set up multiple CalDAV profiles in your client with the respective principal URLs.
|
||||||
**Note:** Apple Calendar doesn't play well with the current membership implementation so you might not want to set up groups at the moment.
|
|
||||||
|
|
||||||
## Password vs app tokens
|
## Password vs app tokens
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user