#146 stopped a group's members sharing its mailboxes on. The same shortcut lets them through everywhere else a group owns things: a member counts as the account's owner, so Calendar/set, AddressBook/set and FileNode/set skip the share check, and so does the WebDAV ACL method. Who has what a group owns is decided by who is in the group. For a member through a group only (is_group_member_only): - Calendar/set, AddressBook/set and FileNode/set refuse a shareWith change as forbidden, on create and update; for files at the top of the account too, not only inside a folder; - the DAV ACL method answers 403 on the group's calendars, address books and files; - myRights reports mayShare false (JmapRights::owner_rights), and the DAV current-user-privilege-set leaves out all and write-acl. Reading who something is shared with is unchanged, as in JMAP. Tests: a new jmap::group_share module has a member create with a share, create without one (and check myRights), share afterwards, and an outsider reach each kind; the WebDAV ACL test has a member try the ACL method on the group's folders; the IMAP ACL test now checks #146's SETACL refusal, which had no test of its own. jmap_tests, webdav_tests and imap_tests pass (RocksDB). specs/multi-account.md MA-D0.
321 lines
13 KiB
Rust
321 lines
13 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*
|
|
* Modified by Coffey Labs in 2026 for INBUXA.
|
|
*/
|
|
|
|
use crate::{api::acl::JmapRights, calendar::Availability, changes::state::JmapCacheState};
|
|
use calcard::jscalendar::{JSCalendarAlertAction, JSCalendarRelativeTo, JSCalendarType};
|
|
use common::{Server, auth::AccessToken, sharing::EffectiveAcl};
|
|
use groupware::{
|
|
cache::GroupwareCache,
|
|
calendar::{
|
|
ALERT_EMAIL, ALERT_RELATIVE_TO_END, ArchivedDefaultAlert, CALENDAR_INVISIBLE,
|
|
CALENDAR_SUBSCRIBED, Calendar,
|
|
},
|
|
};
|
|
use jmap_proto::{
|
|
method::get::{GetRequest, GetResponse},
|
|
object::calendar::{self, CalendarProperty, CalendarValue, IncludeInAvailability},
|
|
};
|
|
use jmap_tools::{Key, Map, Value};
|
|
use store::{
|
|
ValueKey,
|
|
roaring::RoaringBitmap,
|
|
write::{AlignedBytes, Archive, ValueClass},
|
|
};
|
|
use trc::AddContext;
|
|
use types::{
|
|
acl::{Acl, AclGrant},
|
|
collection::{Collection, SyncCollection},
|
|
field::PrincipalField,
|
|
};
|
|
|
|
pub trait CalendarGet: Sync + Send {
|
|
fn calendar_get(
|
|
&self,
|
|
request: GetRequest<calendar::Calendar>,
|
|
access_token: &AccessToken,
|
|
) -> impl Future<Output = trc::Result<GetResponse<calendar::Calendar>>> + Send;
|
|
}
|
|
|
|
impl CalendarGet for Server {
|
|
async fn calendar_get(
|
|
&self,
|
|
mut request: GetRequest<calendar::Calendar>,
|
|
access_token: &AccessToken,
|
|
) -> trc::Result<GetResponse<calendar::Calendar>> {
|
|
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
|
let properties = request.unwrap_properties(&[
|
|
CalendarProperty::Id,
|
|
CalendarProperty::Name,
|
|
CalendarProperty::Description,
|
|
CalendarProperty::Color,
|
|
CalendarProperty::SortOrder,
|
|
CalendarProperty::IsSubscribed,
|
|
CalendarProperty::IsVisible,
|
|
CalendarProperty::IsDefault,
|
|
CalendarProperty::IncludeInAvailability,
|
|
CalendarProperty::DefaultAlertsWithTime,
|
|
CalendarProperty::DefaultAlertsWithoutTime,
|
|
CalendarProperty::TimeZone,
|
|
CalendarProperty::ShareWith,
|
|
CalendarProperty::MyRights,
|
|
]);
|
|
let account_id = request.account_id.document_id();
|
|
let personal_id = access_token.personal_id(account_id, Collection::Calendar);
|
|
let cache = self
|
|
.fetch_dav_resources(
|
|
access_token.account_id(),
|
|
account_id,
|
|
SyncCollection::Calendar,
|
|
)
|
|
.await?;
|
|
let is_owner = access_token.is_member(account_id);
|
|
let calendar_ids = if is_owner {
|
|
cache.document_ids(true).collect::<RoaringBitmap>()
|
|
} else {
|
|
cache.shared_containers(access_token, [Acl::Read, Acl::ReadItems], true)
|
|
};
|
|
let default_calendar_id = self
|
|
.store()
|
|
.get_value::<u32>(ValueKey {
|
|
account_id,
|
|
collection: Collection::Principal.into(),
|
|
document_id: 0,
|
|
class: ValueClass::Property(PrincipalField::DefaultCalendarId.into()),
|
|
})
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.or_else(|| cache.document_ids(true).min());
|
|
|
|
let ids = if let Some(ids) = ids {
|
|
ids
|
|
} else {
|
|
calendar_ids
|
|
.iter()
|
|
.take(self.core.jmap.get_max_objects)
|
|
.map(Into::into)
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let mut response = GetResponse {
|
|
account_id: request.account_id.into(),
|
|
state: cache.get_state(true).into(),
|
|
list: Vec::with_capacity(ids.len()),
|
|
not_found: not_found_ids,
|
|
};
|
|
|
|
for id in ids {
|
|
// Obtain the calendar object
|
|
let document_id = id.document_id();
|
|
if !calendar_ids.contains(document_id) {
|
|
response.push_not_found(id);
|
|
continue;
|
|
}
|
|
let _calendar = if let Some(calendar) = self
|
|
.store()
|
|
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
|
account_id,
|
|
Collection::Calendar,
|
|
document_id,
|
|
))
|
|
.await?
|
|
{
|
|
calendar
|
|
} else {
|
|
response.push_not_found(id);
|
|
continue;
|
|
};
|
|
let calendar = _calendar
|
|
.unarchive::<Calendar>()
|
|
.caused_by(trc::location!())?;
|
|
let mut result = Map::with_capacity(properties.len());
|
|
for property in &properties {
|
|
match property {
|
|
CalendarProperty::Id => {
|
|
result.insert_unchecked(CalendarProperty::Id, CalendarValue::Id(id));
|
|
}
|
|
CalendarProperty::Name => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::Name,
|
|
calendar.preferences(personal_id).name.to_string(),
|
|
);
|
|
}
|
|
CalendarProperty::Description => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::Description,
|
|
calendar
|
|
.preferences(personal_id)
|
|
.description
|
|
.as_ref()
|
|
.map(|v| v.to_string()),
|
|
);
|
|
}
|
|
CalendarProperty::SortOrder => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::SortOrder,
|
|
calendar.preferences(personal_id).sort_order.to_native(),
|
|
);
|
|
}
|
|
CalendarProperty::IsDefault => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::IsDefault,
|
|
default_calendar_id == Some(document_id),
|
|
);
|
|
}
|
|
CalendarProperty::IsSubscribed => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::IsSubscribed,
|
|
Value::Bool(
|
|
calendar.preferences(personal_id).flags & CALENDAR_SUBSCRIBED != 0,
|
|
),
|
|
);
|
|
}
|
|
CalendarProperty::Color => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::Color,
|
|
calendar
|
|
.preferences(personal_id)
|
|
.color
|
|
.as_ref()
|
|
.map(|c| c.to_string()),
|
|
);
|
|
}
|
|
CalendarProperty::IsVisible => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::IsVisible,
|
|
Value::Bool(
|
|
calendar.preferences(personal_id).flags & CALENDAR_INVISIBLE == 0,
|
|
),
|
|
);
|
|
}
|
|
CalendarProperty::IncludeInAvailability => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::IncludeInAvailability,
|
|
Value::Element(CalendarValue::IncludeInAvailability(
|
|
IncludeInAvailability::from_flags(
|
|
calendar.preferences(personal_id).flags.to_native(),
|
|
)
|
|
.unwrap_or(if is_owner {
|
|
IncludeInAvailability::All
|
|
} else {
|
|
IncludeInAvailability::None
|
|
}),
|
|
)),
|
|
);
|
|
}
|
|
CalendarProperty::DefaultAlertsWithTime => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::DefaultAlertsWithTime,
|
|
Value::Object(Map::from_iter(
|
|
calendar
|
|
.default_alerts(personal_id, true)
|
|
.map(default_alarm_to_value),
|
|
)),
|
|
);
|
|
}
|
|
CalendarProperty::DefaultAlertsWithoutTime => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::DefaultAlertsWithoutTime,
|
|
Value::Object(Map::from_iter(
|
|
calendar
|
|
.default_alerts(personal_id, false)
|
|
.map(default_alarm_to_value),
|
|
)),
|
|
);
|
|
}
|
|
CalendarProperty::TimeZone => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::TimeZone,
|
|
calendar
|
|
.preferences(personal_id)
|
|
.time_zone
|
|
.tz()
|
|
.map(|tz| Value::Element(CalendarValue::Timezone(tz)))
|
|
.unwrap_or(Value::Null),
|
|
);
|
|
}
|
|
CalendarProperty::ShareWith => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::ShareWith,
|
|
JmapRights::share_with::<calendar::Calendar>(
|
|
account_id,
|
|
access_token,
|
|
&calendar.acls.iter().map(AclGrant::from).collect::<Vec<_>>(),
|
|
),
|
|
);
|
|
}
|
|
CalendarProperty::MyRights => {
|
|
result.insert_unchecked(
|
|
CalendarProperty::MyRights,
|
|
if access_token.is_shared(account_id) {
|
|
JmapRights::rights::<calendar::Calendar>(
|
|
calendar.acls.effective_acl(access_token),
|
|
)
|
|
} else {
|
|
JmapRights::owner_rights::<calendar::Calendar>(access_token, account_id)
|
|
},
|
|
);
|
|
}
|
|
property => {
|
|
result.insert_unchecked(property.clone(), Value::Null);
|
|
}
|
|
}
|
|
}
|
|
response.list.push(result.into());
|
|
}
|
|
|
|
Ok(response)
|
|
}
|
|
}
|
|
|
|
fn default_alarm_to_value(
|
|
alarm: &ArchivedDefaultAlert,
|
|
) -> (
|
|
Key<'static, CalendarProperty>,
|
|
Value<'static, CalendarProperty, CalendarValue>,
|
|
) {
|
|
(
|
|
Key::Owned(alarm.id.to_string()),
|
|
Value::Object(Map::from(vec![
|
|
(
|
|
Key::Property(CalendarProperty::Type),
|
|
Value::Element(CalendarValue::Type(JSCalendarType::Alert)),
|
|
),
|
|
(
|
|
Key::Property(CalendarProperty::Action),
|
|
Value::Element(CalendarValue::Action(if alarm.flags & ALERT_EMAIL != 0 {
|
|
JSCalendarAlertAction::Email
|
|
} else {
|
|
JSCalendarAlertAction::Display
|
|
})),
|
|
),
|
|
(
|
|
Key::Property(CalendarProperty::Trigger),
|
|
Value::Object(Map::from(vec![
|
|
(
|
|
Key::Property(CalendarProperty::Type),
|
|
Value::Element(CalendarValue::Type(JSCalendarType::OffsetTrigger)),
|
|
),
|
|
(
|
|
Key::Property(CalendarProperty::Offset),
|
|
Value::Element(CalendarValue::Duration(alarm.offset.to_native())),
|
|
),
|
|
(
|
|
Key::Property(CalendarProperty::RelativeTo),
|
|
Value::Element(CalendarValue::RelativeTo(
|
|
if alarm.flags & ALERT_RELATIVE_TO_END != 0 {
|
|
JSCalendarRelativeTo::End
|
|
} else {
|
|
JSCalendarRelativeTo::Start
|
|
},
|
|
)),
|
|
),
|
|
])),
|
|
),
|
|
])),
|
|
)
|
|
}
|