Files
Stalwart/crates/imap/src/op/acl.rs
2026-04-16 21:24:18 +02:00

500 lines
18 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
core::{MailboxId, Session, SessionData, State},
op::ImapContext,
spawn_op,
};
use common::{
auth::AccessToken, ipc::CacheInvalidation, network::SessionStream, sharing::EffectiveAcl,
storage::index::ObjectIndexBuilder,
};
use compact_str::ToCompactString;
use imap_proto::{
Command, ResponseCode, StatusResponse,
protocol::acl::{
Arguments, GetAclResponse, ListRightsResponse, ModRightsOp, MyRightsResponse, Rights,
},
receiver::Request,
};
use registry::schema::enums::Permission;
use std::time::Instant;
use store::{
ValueKey,
write::{AlignedBytes, Archive, BatchBuilder},
};
use trc::AddContext;
use types::{
acl::{Acl, AclGrant},
collection::Collection,
};
use utils::map::bitmap::Bitmap;
impl<T: SessionStream> Session<T> {
pub async fn handle_get_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapAclGet)?;
let op_start = Instant::now();
let arguments = request.parse_acl(self.is_utf8)?;
let is_utf8 = self.version.is_rev2() || self.is_utf8;
let data = self.state.session_data();
spawn_op!(data, {
let (mailbox_id, mailbox_, _) = data
.get_acl_mailbox(&arguments, true)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
let mut permissions = Vec::new();
let mailbox = mailbox_
.to_unarchived::<email::mailbox::Mailbox>()
.imap_ctx(&arguments.tag, trc::location!())?;
// Add the current user if they are the owner or a group member
if data.access_token.is_member(mailbox_id.account_id) {
let account_name = data
.server
.account(mailbox_id.account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?
.name()
.to_string();
permissions.push((
account_name,
vec![
Rights::Read,
Rights::Lookup,
Rights::Insert,
Rights::DeleteMessages,
Rights::Expunge,
Rights::Seen,
Rights::Write,
Rights::CreateMailbox,
Rights::DeleteMailbox,
Rights::Post,
Rights::Administer,
],
));
}
for item in mailbox.inner.acls.iter() {
if item.account_id == mailbox_id.account_id {
// Skip the current user, as they are already added above
continue;
}
let mut rights = Vec::new();
for acl in Bitmap::from(&item.grants) {
match acl {
Acl::Read => {
rights.push(Rights::Lookup);
}
Acl::Modify => {
rights.push(Rights::CreateMailbox);
}
Acl::Delete => {
rights.push(Rights::DeleteMailbox);
}
Acl::ReadItems => {
rights.push(Rights::Read);
}
Acl::AddItems => {
rights.push(Rights::Insert);
}
Acl::ModifyItems => {
rights.push(Rights::Write);
rights.push(Rights::Seen);
}
Acl::RemoveItems => {
rights.push(Rights::DeleteMessages);
rights.push(Rights::Expunge);
}
Acl::CreateChild => {
rights.push(Rights::CreateMailbox);
}
Acl::Share => {
rights.push(Rights::Administer);
}
Acl::Submit => {
rights.push(Rights::Post);
}
_ => (),
}
}
let account_name = data
.server
.account(item.account_id.into())
.await
.imap_ctx(&arguments.tag, trc::location!())?
.name()
.to_string();
permissions.push((account_name, rights));
}
trc::event!(
Imap(trc::ImapEvent::GetAcl),
SpanId = data.session_id,
MailboxName = arguments.mailbox_name.clone(),
AccountId = mailbox_id.account_id,
MailboxId = mailbox_id.mailbox_id,
Total = permissions.len(),
Elapsed = op_start.elapsed()
);
data.write_bytes(
StatusResponse::completed(Command::GetAcl)
.with_tag(arguments.tag)
.serialize(
GetAclResponse {
mailbox_name: arguments.mailbox_name.to_string(),
permissions,
}
.into_bytes(is_utf8),
),
)
.await
})
}
pub async fn handle_my_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapMyRights)?;
let op_start = Instant::now();
let arguments = request.parse_acl(self.is_utf8)?;
let data = self.state.session_data();
let is_utf8 = self.version.is_rev2() || self.is_utf8;
spawn_op!(data, {
let (mailbox_id, mailbox_, access_token) = data
.get_acl_mailbox(&arguments, false)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
let mailbox = mailbox_
.to_unarchived::<email::mailbox::Mailbox>()
.imap_ctx(&arguments.tag, trc::location!())?;
let rights = if access_token.is_shared(mailbox_id.account_id) {
let acl = mailbox.inner.acls.effective_acl(&access_token);
let mut rights = Vec::with_capacity(5);
if acl.contains(Acl::ReadItems) {
rights.push(Rights::Read);
rights.push(Rights::Lookup);
}
if acl.contains(Acl::AddItems) {
rights.push(Rights::Insert);
}
if acl.contains(Acl::RemoveItems) {
rights.push(Rights::DeleteMessages);
rights.push(Rights::Expunge);
}
if acl.contains(Acl::ModifyItems) {
rights.push(Rights::Seen);
rights.push(Rights::Write);
}
if acl.contains(Acl::CreateChild) {
rights.push(Rights::CreateMailbox);
}
if acl.contains(Acl::Delete) {
rights.push(Rights::DeleteMailbox);
}
if acl.contains(Acl::Submit) {
rights.push(Rights::Post);
}
rights
} else {
vec![
Rights::Read,
Rights::Lookup,
Rights::Insert,
Rights::DeleteMessages,
Rights::Expunge,
Rights::Seen,
Rights::Write,
Rights::CreateMailbox,
Rights::DeleteMailbox,
Rights::Post,
Rights::Administer,
]
};
trc::event!(
Imap(trc::ImapEvent::MyRights),
SpanId = data.session_id,
MailboxName = arguments.mailbox_name.clone(),
AccountId = mailbox_id.account_id,
MailboxId = mailbox_id.mailbox_id,
Details = rights
.iter()
.map(|r| trc::Value::String(r.to_compact_string()))
.collect::<Vec<_>>(),
Elapsed = op_start.elapsed()
);
data.write_bytes(
StatusResponse::completed(Command::MyRights)
.with_tag(arguments.tag)
.serialize(
MyRightsResponse {
mailbox_name: arguments.mailbox_name.to_string(),
rights,
}
.into_bytes(is_utf8),
),
)
.await
})
}
pub async fn handle_set_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapAclSet)?;
let op_start = Instant::now();
let command = request.command;
let arguments = request.parse_acl(self.is_utf8)?;
let data = self.state.session_data();
spawn_op!(data, {
// Validate mailbox
let (mailbox_id, current_mailbox, _) = data
.get_acl_mailbox(&arguments, true)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
let current_mailbox = current_mailbox
.into_deserialized::<email::mailbox::Mailbox>()
.imap_ctx(&arguments.tag, trc::location!())?;
// Obtain principal id
let acl_account_id = data
.server
.account_id_from_email(arguments.identifier.as_ref().unwrap(), false)
.await
.imap_ctx(&arguments.tag, trc::location!())?
.ok_or_else(|| {
trc::ImapEvent::Error
.into_err()
.details("Account does not exist")
.id(arguments.tag.to_string())
.caused_by(trc::location!())
})?;
// Prepare changes
let mut mailbox = current_mailbox.inner.clone();
let (op, rights) = arguments
.mod_rights
.map(|mr| {
(
mr.op,
Bitmap::from_iter(mr.rights.into_iter().map(Acl::from)),
)
})
.unwrap_or_else(|| (ModRightsOp::Replace, Bitmap::new()));
if let Some(item) = mailbox
.acls
.iter_mut()
.find(|item| item.account_id == acl_account_id)
{
match op {
ModRightsOp::Replace => {
if !rights.is_empty() {
item.grants = rights;
} else {
mailbox
.acls
.retain(|item| item.account_id != acl_account_id);
}
}
ModRightsOp::Add => {
item.grants.union(&rights);
}
ModRightsOp::Remove => {
for right in rights {
item.grants.remove(right);
}
if item.grants.is_empty() {
mailbox
.acls
.retain(|item| item.account_id != acl_account_id);
}
}
}
} else if !rights.is_empty() {
match op {
ModRightsOp::Add | ModRightsOp::Replace => {
mailbox.acls.push(AclGrant {
account_id: acl_account_id,
grants: rights,
});
}
ModRightsOp::Remove => (),
}
}
if mailbox.acls.len() > data.server.core.groupware.max_shares_per_item {
return Err(trc::ImapEvent::Error
.into_err()
.details("Maximum shares per item exceeded")
.caused_by(trc::location!()));
}
let grants = mailbox
.acls
.iter()
.map(|r| trc::Value::from(r.account_id))
.collect::<Vec<_>>();
// Write changes
let mut batch = BatchBuilder::new();
batch
.with_account_id(mailbox_id.account_id)
.with_collection(Collection::Mailbox)
.with_document(mailbox_id.mailbox_id)
.custom(
ObjectIndexBuilder::new()
.with_changes(mailbox)
.with_current(current_mailbox),
)
.imap_ctx(&arguments.tag, trc::location!())?;
if !batch.is_empty() {
data.server
.commit_batch(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
}
// Invalidate ACLs
data.server
.invalidate_caches(CacheInvalidation::AccessToken(acl_account_id).into())
.await
.imap_ctx(&arguments.tag, trc::location!())?;
trc::event!(
Imap(trc::ImapEvent::SetAcl),
SpanId = data.session_id,
MailboxName = arguments.mailbox_name.clone(),
AccountId = mailbox_id.account_id,
MailboxId = mailbox_id.mailbox_id,
Details = grants,
Elapsed = op_start.elapsed()
);
data.write_bytes(
StatusResponse::completed(command)
.with_tag(arguments.tag)
.into_bytes(),
)
.await
})
}
pub async fn handle_list_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapListRights)?;
let op_start = Instant::now();
let arguments = request.parse_acl(self.is_utf8)?;
trc::event!(
Imap(trc::ImapEvent::ListRights),
SpanId = self.session_id,
MailboxName = arguments.mailbox_name.clone(),
Elapsed = op_start.elapsed()
);
self.write_bytes(
StatusResponse::completed(Command::ListRights)
.with_tag(arguments.tag)
.serialize(
ListRightsResponse {
mailbox_name: arguments.mailbox_name,
identifier: arguments.identifier.unwrap(),
permissions: vec![
vec![Rights::Read],
vec![Rights::Lookup],
vec![Rights::Write, Rights::Seen],
vec![Rights::Insert],
vec![Rights::Expunge, Rights::DeleteMessages],
vec![Rights::CreateMailbox],
vec![Rights::DeleteMailbox],
vec![Rights::Post],
vec![Rights::Administer],
],
}
.into_bytes(self.version.is_rev2() || self.is_utf8),
),
)
.await
}
pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<bool> {
match &self.state {
State::Authenticated { data } | State::Selected { data, .. } => data
.access_token
.enforce_permission(permission)
.map(|_| true),
State::NotAuthenticated { .. } => Ok(false),
}
}
}
impl<T: SessionStream> SessionData<T> {
async fn get_acl_mailbox(
&self,
arguments: &Arguments,
validate: bool,
) -> trc::Result<(MailboxId, Archive<AlignedBytes>, AccessToken)> {
if let Some(mailbox) = self.get_mailbox_by_name(&arguments.mailbox_name) {
if let Some(values) = self
.server
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
mailbox.account_id,
Collection::Mailbox,
mailbox.mailbox_id,
))
.await
.caused_by(trc::location!())?
{
let access_token = self
.refresh_access_token()
.await
.caused_by(trc::location!())?;
if !validate
|| access_token.is_member(mailbox.account_id)
|| values
.unarchive::<email::mailbox::Mailbox>()
.caused_by(trc::location!())?
.acls
.effective_acl(&access_token)
.contains(Acl::Share)
{
Ok((mailbox, values, access_token))
} else {
Err(trc::ImapEvent::Error
.into_err()
.details("You do not have enough permissions to perform this operation.")
.code(ResponseCode::NoPerm))
}
} else {
Err(trc::ImapEvent::Error
.caused_by(trc::location!())
.details("Mailbox does not exist."))
}
} else {
Err(trc::ImapEvent::Error
.into_err()
.details("Mailbox does not exist."))
}
}
}