Coffey Labs is now Coffey Labs LLC, an Arizona limited liability company. Copyright lines and SPDX-FileCopyrightText headers naming Coffey Labs or John Coffey now name Coffey Labs LLC. Upstream copyright notices are unchanged.
129 lines
4.4 KiB
Rust
129 lines
4.4 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2026 Coffey Labs LLC
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
//! inbuxa: a locked account's grants, whole (audit-hold-lock spec, AL-7,
|
|
//! AL-10): its mailboxes here, and its calendars, address books and files
|
|
//! through `groupware::inbuxa_lock`.
|
|
//!
|
|
//! A delegate's access is real ACL grants on the locked account's
|
|
//! containers, the sharing IMAP, DAV and JMAP already honor, so a delegate
|
|
//! sees the account as a shared one everywhere. The lock notes what each
|
|
//! delegate had on a container before, so ending a delegation or the lock
|
|
//! puts it back. Idempotent: run again, it grants on containers made since
|
|
//! and changes nothing else.
|
|
|
|
use crate::{cache::MessageCacheFetch, mailbox::Mailbox};
|
|
use common::{Server, storage::index::ObjectIndexBuilder};
|
|
use groupware::inbuxa_lock::{apply_dav_grants, invalidate, same_replaced};
|
|
use inbuxa_features::lock::{self, Lock, Replaced};
|
|
use store::{
|
|
ValueKey,
|
|
write::{AlignedBytes, Archive, BatchBuilder, now},
|
|
};
|
|
use trc::AddContext;
|
|
use types::{collection::Collection, special_use::SpecialUse};
|
|
|
|
/// Grants a lock's delegates their rights on every container of the locked
|
|
/// account, and takes away those of delegations that ended. Returns what the
|
|
/// lock now has to remember.
|
|
pub async fn apply_grants(
|
|
server: &Server,
|
|
account_id: u32,
|
|
old: Option<&Lock>,
|
|
new: Option<&Lock>,
|
|
) -> trc::Result<Vec<Replaced>> {
|
|
let now = now();
|
|
let mut replaced = Vec::new();
|
|
let mut batch = BatchBuilder::new();
|
|
|
|
let cache = server
|
|
.get_cached_messages(account_id)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
for mailbox in cache.mailboxes.items.iter() {
|
|
// Mail in Trash and Junk is destroyed in time: an organizing
|
|
// delegate may look, not move mail in
|
|
let is_trash = matches!(mailbox.role, SpecialUse::Trash | SpecialUse::Junk);
|
|
let current = mailbox.acls.to_vec();
|
|
let Some(acls) = lock::merge_grants(
|
|
¤t,
|
|
Collection::Mailbox,
|
|
mailbox.document_id,
|
|
is_trash,
|
|
old,
|
|
new,
|
|
now,
|
|
&mut replaced,
|
|
) else {
|
|
continue;
|
|
};
|
|
let Some(archive) = server
|
|
.store()
|
|
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
|
account_id,
|
|
Collection::Mailbox,
|
|
mailbox.document_id,
|
|
))
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
else {
|
|
continue;
|
|
};
|
|
let current = archive
|
|
.into_deserialized::<Mailbox>()
|
|
.caused_by(trc::location!())?;
|
|
let mut changed = current.inner.clone();
|
|
changed.acls = acls;
|
|
batch
|
|
.with_account_id(account_id)
|
|
.with_collection(Collection::Mailbox)
|
|
.with_document(mailbox.document_id)
|
|
.custom(
|
|
ObjectIndexBuilder::new()
|
|
.with_changes(changed)
|
|
.with_current(current),
|
|
)
|
|
.caused_by(trc::location!())?;
|
|
}
|
|
|
|
apply_dav_grants(server, account_id, old, new, now, &mut replaced, &mut batch).await?;
|
|
|
|
if !batch.is_empty() {
|
|
server
|
|
.commit_batch(batch)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
}
|
|
Ok(replaced)
|
|
}
|
|
|
|
/// Re-applies the lock on `account_id`, if any, so containers made since get
|
|
/// its grants: after a delegate creates something there, and daily.
|
|
pub async fn reconcile(server: &Server, account_id: u32) -> trc::Result<()> {
|
|
let data = server.store();
|
|
let Some(current) = lock::get(data, account_id).await? else {
|
|
return Ok(());
|
|
};
|
|
let replaced = apply_grants(server, account_id, Some(¤t), Some(¤t)).await?;
|
|
if !same_replaced(&replaced, ¤t.replaced) {
|
|
let updated = Lock {
|
|
replaced,
|
|
..current.clone()
|
|
};
|
|
lock::set(data, &updated, Some(¤t)).await?;
|
|
}
|
|
invalidate(server, account_id, Some(¤t), Some(¤t)).await
|
|
}
|
|
|
|
/// Re-applies every lock: the daily sweep, for containers made by the server
|
|
/// itself (a Sieve `fileinto :create`) rather than by a delegate.
|
|
pub async fn reconcile_all(server: &Server) -> trc::Result<()> {
|
|
for current in lock::all(server.store()).await? {
|
|
reconcile(server, current.account_id).await?;
|
|
}
|
|
Ok(())
|
|
}
|