/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * 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 Session { pub async fn handle_get_acl(&mut self, request: Request) -> 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::() .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) -> 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::() .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::>(), 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) -> 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::() .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::>(); // 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) -> 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 { match &self.state { State::Authenticated { data } | State::Selected { data, .. } => data .access_token .enforce_permission(permission) .map(|_| true), State::NotAuthenticated { .. } => Ok(false), } } } impl SessionData { async fn get_acl_mailbox( &self, arguments: &Arguments, validate: bool, ) -> trc::Result<(MailboxId, Archive, AccessToken)> { if let Some(mailbox) = self.get_mailbox_by_name(&arguments.mailbox_name) { if let Some(values) = self .server .store() .get_value::>(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::() .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.")) } } }