A locked account can't sign in (it fails as a wrong password does), its sessions end on every node, refresh tokens stop working, and its Sieve scripts forward and reply to nothing. Mail keeps arriving. Delegates get real ACL grants on the account's mailboxes, calendars, address books and files at read, organize or full, with the rights they replaced restored on unlock. Folders made later are granted after the create and in a daily sweep. Organize delegates can't destroy; send-as needs organize or full. The JMAP session marks delegated accounts in urn:inbuxa:jmap. New inbuxa:AccountLock object with get/set, permissions 665-668, and a Compliance > Locked Accounts entry in the schema. Lock, unlock and delegate changes need a reason and are audited; delegate access and writes are audited too (audit-hold-lock spec AL-1 to AL-12).
129 lines
4.4 KiB
Rust
129 lines
4.4 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
|
*
|
|
* 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(())
|
|
}
|