Deliverability spec (inbuxa-drafts specs/deliverability.md), the server side. Every node that sends mail checks itself once a day, at its own minute in the first hour (UTC), and when an administrator asks: - its outgoing addresses (the connection strategy's, or what its EHLO name resolves to), their reverse DNS and whether it resolves back, and nine blocklists, read by each list's own codes so a refused query is never taken for a listing (DL-1 to DL-6); - for every domain: SPF for each address, each DKIM key (by signing a message that's never sent and verifying it as a receiver would), DMARC, the MTA-STS policy against the MX, TLS reporting, and the domain blocklists (DL-7 to DL-12); - whether it holds a certificate for its EHLO and MX names (DL-13). It keeps one report per node, facts only; the console grades them. - inbuxa:DeliverabilityReport: /get, and a create that asks every node to check now, broadcast as DeliverabilityCheck (DL-15). A tenant administrator gets their own domains only (DL-20). - inbuxa:DeliverabilitySettings: which built-in lists are left out, and the lists themselves (DL-6). - sysDeliverabilityGet, sysDeliverabilityUpdate, sysDeliverabilityCheck; a tenant ceiling always turns the last two off.
677 lines
33 KiB
Rust
677 lines
33 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 common::auth::AccessToken;
|
|
use jmap_proto::{
|
|
method::set::SetRequest,
|
|
object::JmapObject,
|
|
request::{
|
|
CopyRequestMethod, GetRequestMethod, ParseRequestMethod, QueryChangesRequestMethod,
|
|
QueryRequestMethod, RequestMethod, SetRequestMethod, method::MethodObject,
|
|
reference::MaybeResultReference,
|
|
},
|
|
};
|
|
use registry::schema::enums::Permission;
|
|
use types::{collection::Collection, id::Id};
|
|
|
|
pub trait JmapAuthorization {
|
|
fn assert_is_member(&self, account_id: Id) -> trc::Result<&Self>;
|
|
/// inbuxa: AL-8: the account's own, or a delegate allowed to send as it.
|
|
fn assert_can_send(&self, account_id: Id) -> trc::Result<&Self>;
|
|
fn assert_has_jmap_permission(
|
|
&self,
|
|
request: &RequestMethod,
|
|
object: MethodObject,
|
|
) -> trc::Result<()>;
|
|
fn assert_has_access(&self, to_account_id: Id, to_collection: Collection)
|
|
-> trc::Result<&Self>;
|
|
}
|
|
|
|
impl JmapAuthorization for AccessToken {
|
|
fn assert_can_send(&self, account_id: Id) -> trc::Result<&Self> {
|
|
if self
|
|
.delegation(account_id.document_id())
|
|
.is_some_and(|delegation| delegation.send_as)
|
|
{
|
|
Ok(self)
|
|
} else {
|
|
self.assert_is_member(account_id)
|
|
}
|
|
}
|
|
|
|
fn assert_is_member(&self, account_id: Id) -> trc::Result<&Self> {
|
|
if self.is_member(account_id.document_id()) {
|
|
Ok(self)
|
|
} else {
|
|
Err(trc::JmapEvent::Forbidden
|
|
.into_err()
|
|
.details(format!("You are not an owner of account {}", account_id)))
|
|
}
|
|
}
|
|
|
|
fn assert_has_access(
|
|
&self,
|
|
to_account_id: Id,
|
|
to_collection: Collection,
|
|
) -> trc::Result<&Self> {
|
|
if self.has_access(to_account_id.document_id(), to_collection) {
|
|
Ok(self)
|
|
} else {
|
|
Err(trc::JmapEvent::Forbidden.into_err().details(format!(
|
|
"You do not have access to account {}",
|
|
to_account_id
|
|
)))
|
|
}
|
|
}
|
|
|
|
fn assert_has_jmap_permission(
|
|
&self,
|
|
request: &RequestMethod,
|
|
object: MethodObject,
|
|
) -> trc::Result<()> {
|
|
let permission = match request {
|
|
RequestMethod::Get(m) => match &m {
|
|
GetRequestMethod::Email(_) => Permission::JmapEmailGet,
|
|
GetRequestMethod::Mailbox(_) => Permission::JmapMailboxGet,
|
|
GetRequestMethod::Thread(_) => Permission::JmapThreadGet,
|
|
GetRequestMethod::Identity(_) => Permission::JmapIdentityGet,
|
|
GetRequestMethod::EmailSubmission(_) => Permission::JmapEmailSubmissionGet,
|
|
GetRequestMethod::PushSubscription(_) => Permission::JmapPushSubscriptionGet,
|
|
GetRequestMethod::Sieve(_) => Permission::JmapSieveScriptGet,
|
|
GetRequestMethod::VacationResponse(_) => Permission::JmapVacationResponseGet,
|
|
// inbuxa: Fastmail's MaskedEmail (ME-18)
|
|
GetRequestMethod::MaskedEmail(_) => Permission::SysMaskedEmailGet,
|
|
// inbuxa: deleted accounts (UD-17)
|
|
GetRequestMethod::DeletedAccount(_) => Permission::SysAccountGet,
|
|
// inbuxa: AI call limits, with the classifier's permissions
|
|
GetRequestMethod::AiLimits(_) => Permission::SysSpamLlmGet,
|
|
// inbuxa: log file retention, with the tracers' permissions
|
|
GetRequestMethod::LogSettings(_) => Permission::SysTracerGet,
|
|
GetRequestMethod::DlpSettings(_) => Permission::SysDlpPolicyGet,
|
|
// inbuxa: personal-data catalog, the inventory and its history
|
|
GetRequestMethod::DataInventory(_) | GetRequestMethod::InventorySnapshot(_) => {
|
|
Permission::SysComplianceGet
|
|
}
|
|
// inbuxa: the audit log (AU-9)
|
|
GetRequestMethod::AuditEvent(_) | GetRequestMethod::AuditSettings(_) => {
|
|
Permission::SysAuditGet
|
|
}
|
|
// inbuxa: account lock (AL-12)
|
|
GetRequestMethod::AccountLock(_) => Permission::SysAccountLockGet,
|
|
GetRequestMethod::LegalHold(_) => Permission::SysLegalHoldGet,
|
|
// inbuxa: DLP and mail flow rules share an object; either
|
|
// permission reaches it, and the handler shows each kind
|
|
// only to those who may see it
|
|
// inbuxa: mail held for review (§2.8)
|
|
GetRequestMethod::HeldMessage(_) => Permission::SysDlpReviewGet,
|
|
GetRequestMethod::MailRule(_) => {
|
|
if self.has_permission(Permission::SysMailRuleGet) {
|
|
Permission::SysMailRuleGet
|
|
} else {
|
|
Permission::SysDlpPolicyGet
|
|
}
|
|
}
|
|
// inbuxa: journaling (JR-18)
|
|
GetRequestMethod::Journal(_) => Permission::SysJournalGet,
|
|
GetRequestMethod::JournalEntry(_) => Permission::SysJournalSearch,
|
|
GetRequestMethod::HoldExport(_) => Permission::SysLegalHoldExport,
|
|
// inbuxa: accepted security items are read by whoever may
|
|
// see the server's security settings
|
|
GetRequestMethod::SecurityAcceptance(_) => Permission::SysSecurityGet,
|
|
// inbuxa: deliverability spec; the lists are named on the
|
|
// page that shows the findings, so they read the same way
|
|
GetRequestMethod::DeliverabilityReport(_)
|
|
| GetRequestMethod::DeliverabilitySettings(_) => Permission::SysDeliverabilityGet,
|
|
// inbuxa: legacy protocols off. It takes listeners away and
|
|
// puts them back, so it takes the listener's permissions
|
|
GetRequestMethod::ProtocolPolicy(_) => Permission::SysNetworkListenerGet,
|
|
// inbuxa: legacy protocols off, per tenant. It governs
|
|
// sign-in on the tenant's domains, so it takes the domain's
|
|
// permissions, which a tenant administrator already holds.
|
|
GetRequestMethod::TenantProtocolPolicy(_) => Permission::SysDomainGet,
|
|
// inbuxa: MA-C, who may share mail: a tenant administrator
|
|
// manages their tenant's, so the domain's permissions; the
|
|
// server's own also needs sysSharingUpdate (see the method)
|
|
GetRequestMethod::SharingPolicy(_) => Permission::SysDomainGet,
|
|
GetRequestMethod::Principal(_) => Permission::JmapPrincipalGet,
|
|
GetRequestMethod::Quota(_) => Permission::JmapQuotaGet,
|
|
GetRequestMethod::Blob(_) => Permission::JmapBlobGet,
|
|
GetRequestMethod::AddressBook(_) => Permission::JmapAddressBookGet,
|
|
GetRequestMethod::ContactCard(_) => Permission::JmapContactCardGet,
|
|
GetRequestMethod::FileNode(_) => Permission::JmapFileNodeGet,
|
|
GetRequestMethod::PrincipalAvailability(_) => {
|
|
Permission::JmapPrincipalGetAvailability
|
|
}
|
|
GetRequestMethod::Calendar(_) => Permission::JmapCalendarGet,
|
|
GetRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventGet,
|
|
GetRequestMethod::CalendarEventNotification(_) => {
|
|
Permission::JmapCalendarEventNotificationGet
|
|
}
|
|
GetRequestMethod::ParticipantIdentity(_) => Permission::JmapParticipantIdentityGet,
|
|
GetRequestMethod::ShareNotification(_) => Permission::JmapShareNotificationGet,
|
|
GetRequestMethod::Registry(_) => {
|
|
let MethodObject::Registry(object_type) = object else {
|
|
unreachable!()
|
|
};
|
|
// inbuxa: MT-2: server-level objects are out of a tenant's reach
|
|
assert_tenant_reach(
|
|
self,
|
|
inbuxa_features::tenancy::reach::can_read(object_type),
|
|
)?;
|
|
object_type.get_permission()
|
|
}
|
|
},
|
|
RequestMethod::Set(m) => {
|
|
return match &m {
|
|
SetRequestMethod::Email(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapEmailCreate,
|
|
Permission::JmapEmailUpdate,
|
|
Permission::JmapEmailDestroy,
|
|
),
|
|
SetRequestMethod::Mailbox(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapMailboxCreate,
|
|
Permission::JmapMailboxUpdate,
|
|
Permission::JmapMailboxDestroy,
|
|
),
|
|
SetRequestMethod::Identity(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapIdentityCreate,
|
|
Permission::JmapIdentityUpdate,
|
|
Permission::JmapIdentityDestroy,
|
|
),
|
|
SetRequestMethod::EmailSubmission(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapEmailSubmissionCreate,
|
|
Permission::JmapEmailSubmissionUpdate,
|
|
Permission::JmapEmailSubmissionDestroy,
|
|
),
|
|
SetRequestMethod::PushSubscription(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapPushSubscriptionCreate,
|
|
Permission::JmapPushSubscriptionUpdate,
|
|
Permission::JmapPushSubscriptionDestroy,
|
|
),
|
|
SetRequestMethod::Sieve(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapSieveScriptCreate,
|
|
Permission::JmapSieveScriptUpdate,
|
|
Permission::JmapSieveScriptDestroy,
|
|
),
|
|
// inbuxa: Fastmail's MaskedEmail (ME-18)
|
|
SetRequestMethod::MaskedEmail(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysMaskedEmailCreate,
|
|
Permission::SysMaskedEmailUpdate,
|
|
Permission::SysMaskedEmailDestroy,
|
|
),
|
|
// inbuxa: deleted accounts; a restore creates the account again (UD-17)
|
|
SetRequestMethod::DeletedAccount(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysAccountCreate,
|
|
Permission::SysAccountCreate,
|
|
Permission::SysAccountDestroy,
|
|
),
|
|
// inbuxa: AI call limits, with the classifier's permissions
|
|
SetRequestMethod::AiLimits(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysSpamLlmUpdate,
|
|
Permission::SysSpamLlmUpdate,
|
|
Permission::SysSpamLlmUpdate,
|
|
),
|
|
// inbuxa: log file retention, with the tracers' permissions
|
|
SetRequestMethod::LogSettings(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysTracerUpdate,
|
|
Permission::SysTracerUpdate,
|
|
Permission::SysTracerUpdate,
|
|
),
|
|
SetRequestMethod::DlpSettings(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysDlpPolicyUpdate,
|
|
Permission::SysDlpPolicyUpdate,
|
|
Permission::SysDlpPolicyUpdate,
|
|
),
|
|
// inbuxa: the audit log (AU-7, AU-9, AU-11)
|
|
SetRequestMethod::AuditSettings(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysAuditSettingsUpdate,
|
|
Permission::SysAuditSettingsUpdate,
|
|
Permission::SysAuditSettingsUpdate,
|
|
),
|
|
SetRequestMethod::AuditExport(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysAuditExport,
|
|
Permission::SysAuditExport,
|
|
Permission::SysAuditExport,
|
|
),
|
|
// inbuxa: account lock (AL-12)
|
|
SetRequestMethod::AccountLock(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysAccountLockCreate,
|
|
Permission::SysAccountLockUpdate,
|
|
Permission::SysAccountLockDestroy,
|
|
),
|
|
// inbuxa: legal hold (LH-13); holds are never destroyed,
|
|
// and the handler refuses a destroy outright
|
|
SetRequestMethod::LegalHold(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysLegalHoldCreate,
|
|
Permission::SysLegalHoldUpdate,
|
|
Permission::SysLegalHoldUpdate,
|
|
),
|
|
// inbuxa: releasing or rejecting held mail (§2.8)
|
|
SetRequestMethod::HeldMessage(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysDlpReviewUpdate,
|
|
Permission::SysDlpReviewUpdate,
|
|
Permission::SysDlpReviewUpdate,
|
|
),
|
|
// inbuxa: DLP and mail flow rules: either change
|
|
// permission gets in; the handler checks each rule's kind
|
|
SetRequestMethod::MailRule(_) => {
|
|
if self.has_permission(Permission::SysMailRuleUpdate)
|
|
|| self.has_permission(Permission::SysDlpPolicyUpdate)
|
|
{
|
|
Ok(())
|
|
} else {
|
|
Err(trc::JmapEvent::Forbidden
|
|
.into_err()
|
|
.details("You are not authorized to change mail rules"))
|
|
}
|
|
}
|
|
// inbuxa: journaling (JR-18)
|
|
SetRequestMethod::Journal(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysJournalUpdate,
|
|
Permission::SysJournalUpdate,
|
|
Permission::SysJournalUpdate,
|
|
),
|
|
SetRequestMethod::JournalExport(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysJournalExport,
|
|
Permission::SysJournalExport,
|
|
Permission::SysJournalExport,
|
|
),
|
|
SetRequestMethod::JournalVerification(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysJournalGet,
|
|
Permission::SysJournalGet,
|
|
Permission::SysJournalGet,
|
|
),
|
|
// inbuxa: accepting a security to-do item, or removing
|
|
// an acceptance; nothing is ever edited
|
|
SetRequestMethod::SecurityAcceptance(s) => {
|
|
if s.update.as_ref().is_some_and(|u| !u.is_empty()) {
|
|
Err(trc::JmapEvent::Forbidden
|
|
.into_err()
|
|
.details("An acceptance is replaced, not edited"))
|
|
} else if self.has_permission(Permission::SysSecurityAccept) {
|
|
Ok(())
|
|
} else {
|
|
Err(trc::JmapEvent::Forbidden
|
|
.into_err()
|
|
.details("You are not authorized to accept security items"))
|
|
}
|
|
}
|
|
// inbuxa: DL-15: a create runs the check; the handler
|
|
// refuses the rest
|
|
SetRequestMethod::DeliverabilityReport(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysDeliverabilityCheck,
|
|
Permission::SysDeliverabilityCheck,
|
|
Permission::SysDeliverabilityCheck,
|
|
),
|
|
// inbuxa: DL-6, which lists are asked
|
|
SetRequestMethod::DeliverabilitySettings(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysDeliverabilityUpdate,
|
|
Permission::SysDeliverabilityUpdate,
|
|
Permission::SysDeliverabilityUpdate,
|
|
),
|
|
// inbuxa: LH-12, exporting held data
|
|
SetRequestMethod::HoldExport(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysLegalHoldExport,
|
|
Permission::SysLegalHoldExport,
|
|
Permission::SysLegalHoldExport,
|
|
),
|
|
SetRequestMethod::AuditVerification(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysAuditGet,
|
|
Permission::SysAuditGet,
|
|
Permission::SysAuditGet,
|
|
),
|
|
// inbuxa: "Explain this" (EX-4)
|
|
SetRequestMethod::Explanation(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysAiExplain,
|
|
Permission::SysAiExplain,
|
|
Permission::SysAiExplain,
|
|
),
|
|
// inbuxa: legacy protocols off, with the listener's
|
|
SetRequestMethod::ProtocolPolicy(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysNetworkListenerUpdate,
|
|
Permission::SysNetworkListenerUpdate,
|
|
Permission::SysNetworkListenerUpdate,
|
|
),
|
|
// inbuxa: legacy protocols off, per tenant, with the domain's
|
|
SetRequestMethod::TenantProtocolPolicy(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysDomainUpdate,
|
|
Permission::SysDomainUpdate,
|
|
Permission::SysDomainUpdate,
|
|
),
|
|
// inbuxa: MA-C, who may share mail, with the domain's
|
|
SetRequestMethod::SharingPolicy(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::SysDomainUpdate,
|
|
Permission::SysDomainUpdate,
|
|
Permission::SysDomainUpdate,
|
|
),
|
|
SetRequestMethod::VacationResponse(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapVacationResponseCreate,
|
|
Permission::JmapVacationResponseUpdate,
|
|
Permission::JmapVacationResponseDestroy,
|
|
),
|
|
SetRequestMethod::AddressBook(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapAddressBookCreate,
|
|
Permission::JmapAddressBookUpdate,
|
|
Permission::JmapAddressBookDestroy,
|
|
),
|
|
SetRequestMethod::ContactCard(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapContactCardCreate,
|
|
Permission::JmapContactCardUpdate,
|
|
Permission::JmapContactCardDestroy,
|
|
),
|
|
SetRequestMethod::FileNode(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapFileNodeCreate,
|
|
Permission::JmapFileNodeUpdate,
|
|
Permission::JmapFileNodeDestroy,
|
|
),
|
|
SetRequestMethod::ShareNotification(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapShareNotificationCreate,
|
|
Permission::JmapShareNotificationUpdate,
|
|
Permission::JmapShareNotificationDestroy,
|
|
),
|
|
SetRequestMethod::Calendar(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapCalendarCreate,
|
|
Permission::JmapCalendarUpdate,
|
|
Permission::JmapCalendarDestroy,
|
|
),
|
|
SetRequestMethod::CalendarEvent(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapCalendarEventCreate,
|
|
Permission::JmapCalendarEventUpdate,
|
|
Permission::JmapCalendarEventDestroy,
|
|
),
|
|
SetRequestMethod::CalendarEventNotification(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapCalendarEventNotificationCreate,
|
|
Permission::JmapCalendarEventNotificationUpdate,
|
|
Permission::JmapCalendarEventNotificationDestroy,
|
|
),
|
|
SetRequestMethod::ParticipantIdentity(s) => validate_set(
|
|
s,
|
|
self,
|
|
Permission::JmapParticipantIdentityCreate,
|
|
Permission::JmapParticipantIdentityUpdate,
|
|
Permission::JmapParticipantIdentityDestroy,
|
|
),
|
|
SetRequestMethod::Registry(s) => {
|
|
let MethodObject::Registry(object_type) = object else {
|
|
unreachable!()
|
|
};
|
|
// inbuxa: MT-2, MT-12: server-level objects are out of a tenant's reach
|
|
assert_tenant_reach(
|
|
self,
|
|
inbuxa_features::tenancy::reach::can_write(object_type),
|
|
)?;
|
|
let set_permissions = object_type.set_permission();
|
|
validate_set(
|
|
s,
|
|
self,
|
|
set_permissions[0],
|
|
set_permissions[1],
|
|
set_permissions[2],
|
|
)
|
|
}
|
|
};
|
|
}
|
|
RequestMethod::Changes(_) => match object {
|
|
MethodObject::Email => Permission::JmapEmailChanges,
|
|
MethodObject::Mailbox => Permission::JmapMailboxChanges,
|
|
MethodObject::Thread => Permission::JmapThreadChanges,
|
|
MethodObject::Identity => Permission::JmapIdentityChanges,
|
|
MethodObject::EmailSubmission => Permission::JmapEmailSubmissionChanges,
|
|
MethodObject::Quota => Permission::JmapQuotaChanges,
|
|
MethodObject::ContactCard => Permission::JmapContactCardChanges,
|
|
MethodObject::FileNode => Permission::JmapFileNodeChanges,
|
|
MethodObject::Calendar => Permission::JmapCalendarChanges,
|
|
MethodObject::CalendarEvent => Permission::JmapCalendarEventChanges,
|
|
MethodObject::CalendarEventNotification => {
|
|
Permission::JmapCalendarEventNotificationChanges
|
|
}
|
|
MethodObject::ParticipantIdentity => Permission::JmapParticipantIdentityChanges,
|
|
MethodObject::ShareNotification => Permission::JmapShareNotificationChanges,
|
|
MethodObject::Principal => Permission::JmapPrincipalChanges,
|
|
MethodObject::AddressBook => Permission::JmapAddressBookChanges,
|
|
MethodObject::Core
|
|
| MethodObject::Blob
|
|
| MethodObject::PushSubscription
|
|
| MethodObject::SearchSnippet
|
|
| MethodObject::VacationResponse
|
|
| MethodObject::SieveScript
|
|
| MethodObject::MaskedEmail
|
|
| MethodObject::DeletedAccount
|
|
| MethodObject::AiLimits
|
|
| MethodObject::LogSettings
|
|
| MethodObject::DlpSettings
|
|
| MethodObject::DataInventory
|
|
| MethodObject::InventorySnapshot
|
|
| MethodObject::Explanation
|
|
| MethodObject::AuditEvent
|
|
| MethodObject::AuditSettings
|
|
| MethodObject::AuditExport
|
|
| MethodObject::AuditVerification
|
|
| MethodObject::AccountLock
|
|
| MethodObject::LegalHold
|
|
| MethodObject::HoldExport
|
|
| MethodObject::MailRule
|
|
| MethodObject::SecurityAcceptance
|
|
| MethodObject::DeliverabilityReport
|
|
| MethodObject::DeliverabilitySettings
|
|
| MethodObject::HeldMessage
|
|
| MethodObject::Journal
|
|
| MethodObject::JournalEntry
|
|
| MethodObject::JournalExport
|
|
| MethodObject::JournalVerification
|
|
| MethodObject::ProtocolPolicy
|
|
| MethodObject::TenantProtocolPolicy
|
|
| MethodObject::SharingPolicy => Permission::JmapEmailChanges,
|
|
// inbuxa: x:MaskedEmail/changes reads what /get reads
|
|
MethodObject::Registry(object_type) => object_type.get_permission(),
|
|
},
|
|
RequestMethod::Copy(m) => match &m {
|
|
CopyRequestMethod::Email(_) => Permission::JmapEmailCopy,
|
|
CopyRequestMethod::Blob(_) => Permission::JmapBlobCopy,
|
|
CopyRequestMethod::ContactCard(_) => Permission::JmapContactCardCopy,
|
|
CopyRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventCopy,
|
|
CopyRequestMethod::FileNode(_) => Permission::JmapFileNodeCopy,
|
|
},
|
|
RequestMethod::ImportEmail(_) => Permission::JmapEmailImport,
|
|
RequestMethod::Parse(m) => match &m {
|
|
ParseRequestMethod::Email(_) => Permission::JmapEmailParse,
|
|
ParseRequestMethod::ContactCard(_) => Permission::JmapContactCardParse,
|
|
ParseRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventParse,
|
|
},
|
|
RequestMethod::QueryChanges(m) => match m {
|
|
QueryChangesRequestMethod::Email(_) => Permission::JmapEmailQueryChanges,
|
|
QueryChangesRequestMethod::Mailbox(_) => Permission::JmapMailboxQueryChanges,
|
|
QueryChangesRequestMethod::EmailSubmission(_) => {
|
|
Permission::JmapEmailSubmissionQueryChanges
|
|
}
|
|
QueryChangesRequestMethod::Principal(_) => Permission::JmapPrincipalQueryChanges,
|
|
QueryChangesRequestMethod::Quota(_) => Permission::JmapQuotaQueryChanges,
|
|
QueryChangesRequestMethod::ContactCard(_) => {
|
|
Permission::JmapContactCardQueryChanges
|
|
}
|
|
QueryChangesRequestMethod::FileNode(_) => Permission::JmapFileNodeQueryChanges,
|
|
QueryChangesRequestMethod::CalendarEvent(_) => {
|
|
Permission::JmapCalendarEventQueryChanges
|
|
}
|
|
QueryChangesRequestMethod::CalendarEventNotification(_) => {
|
|
Permission::JmapCalendarEventNotificationQueryChanges
|
|
}
|
|
QueryChangesRequestMethod::ShareNotification(_) => {
|
|
Permission::JmapShareNotificationQueryChanges
|
|
}
|
|
},
|
|
RequestMethod::Query(m) => match m {
|
|
QueryRequestMethod::Email(_) => Permission::JmapEmailQuery,
|
|
QueryRequestMethod::Mailbox(_) => Permission::JmapMailboxQuery,
|
|
QueryRequestMethod::EmailSubmission(_) => Permission::JmapEmailSubmissionQuery,
|
|
QueryRequestMethod::Sieve(_) => Permission::JmapSieveScriptQuery,
|
|
QueryRequestMethod::Principal(_) => Permission::JmapPrincipalQuery,
|
|
QueryRequestMethod::Quota(_) => Permission::JmapQuotaQuery,
|
|
QueryRequestMethod::AddressBook(_) => Permission::JmapAddressBookGet,
|
|
QueryRequestMethod::ContactCard(_) => Permission::JmapContactCardQuery,
|
|
QueryRequestMethod::FileNode(_) => Permission::JmapFileNodeQuery,
|
|
QueryRequestMethod::Calendar(_) => Permission::JmapCalendarGet,
|
|
QueryRequestMethod::CalendarEvent(_) => Permission::JmapCalendarEventQuery,
|
|
QueryRequestMethod::CalendarEventNotification(_) => {
|
|
Permission::JmapCalendarEventNotificationQuery
|
|
}
|
|
QueryRequestMethod::ShareNotification(_) => Permission::JmapShareNotificationQuery,
|
|
// inbuxa: the audit log (AU-9)
|
|
QueryRequestMethod::AuditEvent(_) => Permission::SysAuditGet,
|
|
// inbuxa: journaling (JR-15)
|
|
QueryRequestMethod::JournalEntry(_) => Permission::SysJournalSearch,
|
|
QueryRequestMethod::Registry(_) => {
|
|
let MethodObject::Registry(object_type) = object else {
|
|
unreachable!()
|
|
};
|
|
// inbuxa: MT-2: server-level objects are out of a tenant's reach
|
|
assert_tenant_reach(
|
|
self,
|
|
inbuxa_features::tenancy::reach::can_read(object_type),
|
|
)?;
|
|
object_type.query_permission()
|
|
}
|
|
},
|
|
RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippetGet,
|
|
RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate,
|
|
RequestMethod::LookupBlob(_) => Permission::JmapBlobLookup,
|
|
RequestMethod::UploadBlob(_) => Permission::JmapBlobUpload,
|
|
RequestMethod::Echo(_) => Permission::JmapCoreEcho,
|
|
RequestMethod::Error(_) => return Ok(()),
|
|
};
|
|
|
|
if self.has_permission(permission) {
|
|
Ok(())
|
|
} else {
|
|
Err(trc::JmapEvent::Forbidden
|
|
.into_err()
|
|
.details("You are not authorized to perform this action"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// inbuxa: MT-2
|
|
fn assert_tenant_reach(access_token: &AccessToken, reachable: bool) -> trc::Result<()> {
|
|
if reachable || access_token.tenant_id().is_none() {
|
|
Ok(())
|
|
} else {
|
|
Err(trc::JmapEvent::Forbidden
|
|
.into_err()
|
|
.details("You are not authorized to perform this action"))
|
|
}
|
|
}
|
|
|
|
fn validate_set<T: JmapObject>(
|
|
set: &SetRequest<'_, T>,
|
|
access_token: &AccessToken,
|
|
create_permission: Permission,
|
|
update_permission: Permission,
|
|
destroy_permission: Permission,
|
|
) -> trc::Result<()> {
|
|
let can_create = access_token.has_permission(create_permission);
|
|
let can_update = access_token.has_permission(update_permission);
|
|
let can_destroy = access_token.has_permission(destroy_permission);
|
|
|
|
if can_create && can_update && can_destroy {
|
|
Ok(())
|
|
} else if !can_create && !can_update && !can_destroy {
|
|
Err(trc::JmapEvent::Forbidden
|
|
.into_err()
|
|
.details("You are not authorized to create, update or destroy objects of this type"))
|
|
} else if !can_create && set.create.as_ref().is_some_and(|objs| !objs.is_empty()) {
|
|
Err(trc::JmapEvent::Forbidden
|
|
.into_err()
|
|
.details("You are not authorized to create objects of this type"))
|
|
} else if !can_update && set.update.as_ref().is_some_and(|objs| !objs.is_empty()) {
|
|
Err(trc::JmapEvent::Forbidden
|
|
.into_err()
|
|
.details("You are not authorized to update objects of this type"))
|
|
} else if !can_destroy
|
|
&& set.destroy.as_ref().is_some_and(|objs| match objs {
|
|
MaybeResultReference::Value(v) => !v.is_empty(),
|
|
MaybeResultReference::Reference(_) => true,
|
|
})
|
|
{
|
|
Err(trc::JmapEvent::Forbidden
|
|
.into_err()
|
|
.details("You are not authorized to destroy objects of this type"))
|
|
} else {
|
|
Ok(())
|
|
}
|
|
}
|