diff options
author | mdecimus <mauro@stalw.art> | 2024-09-10 18:44:44 +0200 |
---|---|---|
committer | mdecimus <mauro@stalw.art> | 2024-09-10 18:44:44 +0200 |
commit | fbcf55d8e1891b72499fed7d5ff54964c8a2f256 (patch) | |
tree | c6f18add7f0b62ce3bb6848c1b8b9e68c4eb4e5e /crates/jmap/src | |
parent | 08a95ae58b451dbc2284ed7ac2d413185bc879fe (diff) |
Access token permissions
Diffstat (limited to 'crates/jmap/src')
30 files changed, 582 insertions, 181 deletions
diff --git a/crates/jmap/src/api/autoconfig.rs b/crates/jmap/src/api/autoconfig.rs index c8c67ef9..11d29547 100644 --- a/crates/jmap/src/api/autoconfig.rs +++ b/crates/jmap/src/api/autoconfig.rs @@ -7,7 +7,7 @@ use std::fmt::Write; use common::manager::webadmin::Resource; -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use quick_xml::events::Event; use quick_xml::Reader; use utils::url_params::UrlParams; @@ -187,14 +187,14 @@ impl JMAP { .await .unwrap_or_default() { - if let Ok(Some(principal)) = self + if let Ok(Some(mut principal)) = self .core .storage .directory .query(QueryBy::Id(id), false) .await { - account_name = principal.name; + account_name = principal.take_str(PrincipalField::Name).unwrap_or_default(); break; } } diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 4968863e..120777db 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -12,6 +12,7 @@ use common::{ manager::webadmin::Resource, Core, }; +use directory::Permission; use http_body_util::{BodyExt, Full}; use hyper::{ body::{self, Bytes}, @@ -29,7 +30,7 @@ use jmap_proto::{ }; use crate::{ - auth::{authenticate::HttpHeaders, oauth::OAuthMetadata}, + auth::{authenticate::HttpHeaders, oauth::OAuthMetadata, AccessToken}, blob::{DownloadResponse, UploadResponse}, services::state, JmapInstance, JMAP, @@ -81,7 +82,7 @@ impl JMAP { let request = fetch_body( &mut req, - if !access_token.is_super_user() { + if !access_token.has_permission(Permission::UnlimitedUploads) { self.core.jmap.upload_max_size } else { 0 @@ -142,7 +143,7 @@ impl JMAP { { return match fetch_body( &mut req, - if !access_token.is_super_user() { + if !access_token.has_permission(Permission::UnlimitedUploads) { self.core.jmap.upload_max_size } else { 0 @@ -302,27 +303,29 @@ impl JMAP { if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) && self.core.is_enterprise_edition() { - if let Some((live_path, token)) = req + if let Some((live_path, grant_type, token)) = req .uri() .path() .strip_prefix("/api/telemetry/") .and_then(|p| { p.strip_prefix("traces/live/") - .map(|t| ("traces", t)) + .map(|t| ("traces", "live_tracing", t)) .or_else(|| { p.strip_prefix("metrics/live/") - .map(|t| ("metrics", t)) + .map(|t| ("metrics", "live_metrics", t)) }) }) { let (account_id, _, _) = - self.validate_access_token("live_telemetry", token).await?; + self.validate_access_token(grant_type, token).await?; return self .handle_telemetry_api_request( &req, vec!["", live_path, "live"], - account_id, + &AccessToken::from_id(account_id) + .with_permission(Permission::MetricsLive) + .with_permission(Permission::TracingLive), ) .await; } @@ -893,7 +896,13 @@ impl ToRequestError for trc::Error { trc::AuthEvent::TooManyAttempts => RequestError::too_many_auth_attempts(), _ => RequestError::unauthorized(), }, - trc::EventType::Security(_) => RequestError::too_many_auth_attempts(), + trc::EventType::Security(cause) => match cause { + trc::SecurityEvent::AuthenticationBan + | trc::SecurityEvent::BruteForceBan + | trc::SecurityEvent::LoiterBan + | trc::SecurityEvent::IpBlocked => RequestError::too_many_auth_attempts(), + trc::SecurityEvent::Unauthorized => RequestError::forbidden(), + }, trc::EventType::Resource(cause) => match cause { trc::ResourceEvent::NotFound => RequestError::not_found(), trc::ResourceEvent::BadParameters => RequestError::blank( diff --git a/crates/jmap/src/api/management/dkim.rs b/crates/jmap/src/api/management/dkim.rs index 00589441..06ddaa59 100644 --- a/crates/jmap/src/api/management/dkim.rs +++ b/crates/jmap/src/api/management/dkim.rs @@ -7,7 +7,7 @@ use std::str::FromStr; use common::config::smtp::auth::simple_pem_parse; -use directory::backend::internal::manage; +use directory::{backend::internal::manage, Permission}; use hyper::Method; use mail_auth::{ common::crypto::{Ed25519Key, RsaKey, Sha256}, @@ -23,6 +23,7 @@ use store::write::now; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -48,10 +49,21 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match *req.method() { - Method::GET => self.handle_get_public_key(path).await, - Method::POST => self.handle_create_signature(body).await, + Method::GET => { + // Validate the access token + access_token.assert_has_permission(Permission::DkimSignatureGet)?; + + self.handle_get_public_key(path).await + } + Method::POST => { + // Validate the access token + access_token.assert_has_permission(Permission::DkimSignatureCreate)?; + + self.handle_create_signature(body).await + } _ => Err(trc::ResourceEvent::NotFound.into_err()), } } diff --git a/crates/jmap/src/api/management/domain.rs b/crates/jmap/src/api/management/domain.rs index e029e4c6..eba1312b 100644 --- a/crates/jmap/src/api/management/domain.rs +++ b/crates/jmap/src/api/management/domain.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::backend::internal::manage::{self, ManageDirectory}; +use directory::{ + backend::internal::manage::{self, ManageDirectory}, + Permission, +}; use hyper::Method; use serde::{Deserialize, Serialize}; @@ -19,6 +22,7 @@ use crate::{ management::dkim::{obtain_dkim_public_key, Algorithm}, HttpRequest, HttpResponse, JsonResponse, }, + auth::AccessToken, JMAP, }; @@ -37,9 +41,13 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match (path.get(1), req.method()) { (None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::DomainList)?; + // List domains let params = UrlParams::new(req.uri().query()); let filter = params.get("filter"); @@ -66,6 +74,9 @@ impl JMAP { .into_http_response()) } (Some(domain), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::DomainGet)?; + // Obtain DNS records let domain = decode_path_element(domain); Ok(JsonResponse::new(json!({ @@ -74,6 +85,9 @@ impl JMAP { .into_http_response()) } (Some(domain), &Method::POST) => { + // Validate the access token + access_token.assert_has_permission(Permission::DomainCreate)?; + // Create domain let domain = decode_path_element(domain); self.core @@ -103,6 +117,9 @@ impl JMAP { .into_http_response()) } (Some(domain), &Method::DELETE) => { + // Validate the access token + access_token.assert_has_permission(Permission::DomainDelete)?; + // Delete domain let domain = decode_path_element(domain); self.core diff --git a/crates/jmap/src/api/management/enterprise/telemetry.rs b/crates/jmap/src/api/management/enterprise/telemetry.rs index 6186509c..80ea5548 100644 --- a/crates/jmap/src/api/management/enterprise/telemetry.rs +++ b/crates/jmap/src/api/management/enterprise/telemetry.rs @@ -17,7 +17,7 @@ use common::telemetry::{ metrics::store::{Metric, MetricsStore}, tracers::store::{TracingQuery, TracingStore}, }; -use directory::backend::internal::manage; +use directory::{backend::internal::manage, Permission}; use http_body_util::{combinators::BoxBody, StreamBody}; use hyper::{ body::{Bytes, Frame}, @@ -38,6 +38,7 @@ use crate::{ http::ToHttpResponse, management::Timestamp, HttpRequest, HttpResponse, HttpResponseBody, JsonResponse, }, + auth::AccessToken, JMAP, }; @@ -46,9 +47,10 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, - account_id: u32, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { let params = UrlParams::new(req.uri().query()); + let account_id = access_token.primary_id(); match ( path.get(1).copied().unwrap_or_default(), @@ -56,6 +58,9 @@ impl JMAP { req.method(), ) { ("traces", None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::TracingList)?; + let page: usize = params.parse("page").unwrap_or(0); let limit: usize = params.parse("limit").unwrap_or(0); let mut tracing_query = Vec::new(); @@ -162,6 +167,9 @@ impl JMAP { } } ("traces", Some("live"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::TracingLive)?; + let mut key_filters = AHashMap::new(); let mut filter = None; @@ -290,6 +298,9 @@ impl JMAP { }) } ("trace", id, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::TracingGet)?; + let store = &self .core .enterprise @@ -327,15 +338,32 @@ impl JMAP { .into_http_response()) } } - ("live", Some("token"), &Method::GET) => { + ("live", Some("tracing-token"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::TracingLive)?; + // Issue a live telemetry token valid for 60 seconds Ok(JsonResponse::new(json!({ - "data": self.issue_custom_token(account_id, "live_telemetry", "web", 60).await?, + "data": self.issue_custom_token(account_id, "live_tracing", "web", 60).await?, + })) + .into_http_response()) + } + ("live", Some("metrics-token"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::MetricsLive)?; + + // Issue a live telemetry token valid for 60 seconds + + Ok(JsonResponse::new(json!({ + "data": self.issue_custom_token(account_id, "live_metrics", "web", 60).await?, })) .into_http_response()) } ("metrics", None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::MetricsList)?; + let before = params .parse::<Timestamp>("before") .map(|t| t.into_inner()) @@ -395,6 +423,9 @@ impl JMAP { .into_http_response()) } ("metrics", Some("live"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::MetricsLive)?; + let interval = Duration::from_secs( params .parse::<u64>("interval") diff --git a/crates/jmap/src/api/management/log.rs b/crates/jmap/src/api/management/log.rs index 1083a009..0bebbf48 100644 --- a/crates/jmap/src/api/management/log.rs +++ b/crates/jmap/src/api/management/log.rs @@ -5,7 +5,7 @@ use std::{ }; use chrono::DateTime; -use directory::backend::internal::manage; +use directory::{backend::internal::manage, Permission}; use rev_lines::RevLines; use serde::Serialize; use serde_json::json; @@ -14,6 +14,7 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -27,7 +28,14 @@ struct LogEntry { } impl JMAP { - pub async fn handle_view_logs(&self, req: &HttpRequest) -> trc::Result<HttpResponse> { + pub async fn handle_view_logs( + &self, + req: &HttpRequest, + access_token: &AccessToken, + ) -> trc::Result<HttpResponse> { + // Validate the access token + access_token.assert_has_permission(Permission::LogsView)?; + let path = self .core .metrics diff --git a/crates/jmap/src/api/management/mod.rs b/crates/jmap/src/api/management/mod.rs index 1715363b..9972a86f 100644 --- a/crates/jmap/src/api/management/mod.rs +++ b/crates/jmap/src/api/management/mod.rs @@ -19,7 +19,7 @@ pub mod stores; use std::{borrow::Cow, str::FromStr, sync::Arc}; -use directory::backend::internal::manage; +use directory::{backend::internal::manage, Permission}; use hyper::Method; use mail_parser::DateTime; use serde::Serialize; @@ -50,31 +50,68 @@ impl JMAP { session: &HttpSessionData, ) -> trc::Result<HttpResponse> { let path = req.uri().path().split('/').skip(2).collect::<Vec<_>>(); - let is_superuser = access_token.is_super_user(); match path.first().copied().unwrap_or_default() { - "queue" if is_superuser => self.handle_manage_queue(req, path).await, - "settings" if is_superuser => self.handle_manage_settings(req, path, body).await, - "reports" if is_superuser => self.handle_manage_reports(req, path).await, - "principal" if is_superuser => self.handle_manage_principal(req, path, body).await, - "domain" if is_superuser => self.handle_manage_domain(req, path).await, - "store" if is_superuser => self.handle_manage_store(req, path, body, session).await, - "reload" if is_superuser => self.handle_manage_reload(req, path).await, - "dkim" if is_superuser => self.handle_manage_dkim(req, path, body).await, - "update" if is_superuser => self.handle_manage_update(req, path).await, - "logs" if is_superuser && req.method() == Method::GET => { - self.handle_view_logs(req).await + "queue" => self.handle_manage_queue(req, path, &access_token).await, + "settings" => { + self.handle_manage_settings(req, path, body, &access_token) + .await } - "sieve" if is_superuser => self.handle_run_sieve(req, path, body).await, - "restart" if is_superuser && req.method() == Method::GET => { + "reports" => self.handle_manage_reports(req, path, &access_token).await, + "principal" => { + self.handle_manage_principal(req, path, body, &access_token) + .await + } + "domain" => self.handle_manage_domain(req, path, &access_token).await, + "store" => { + self.handle_manage_store(req, path, body, session, &access_token) + .await + } + "reload" => self.handle_manage_reload(req, path, &access_token).await, + "dkim" => { + self.handle_manage_dkim(req, path, body, &access_token) + .await + } + "update" => self.handle_manage_update(req, path, &access_token).await, + "logs" if req.method() == Method::GET => { + self.handle_view_logs(req, &access_token).await + } + "sieve" => self.handle_run_sieve(req, path, body, &access_token).await, + "restart" if req.method() == Method::GET => { + // Validate the access token + access_token.assert_has_permission(Permission::Restart)?; + Err(manage::unsupported("Restart is not yet supported")) } - "oauth" => self.handle_oauth_api_request(access_token, body).await, + "oauth" => { + // Validate the access token + access_token.assert_has_permission(Permission::AuthenticateOauth)?; + + self.handle_oauth_api_request(access_token, body).await + } "account" => match (path.get(1).copied().unwrap_or_default(), req.method()) { - ("crypto", &Method::POST) => self.handle_crypto_post(access_token, body).await, - ("crypto", &Method::GET) => self.handle_crypto_get(access_token).await, - ("auth", &Method::GET) => self.handle_account_auth_get(access_token).await, + ("crypto", &Method::POST) => { + // Validate the access token + access_token.assert_has_permission(Permission::ManageEncryption)?; + + self.handle_crypto_post(access_token, body).await + } + ("crypto", &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::ManageEncryption)?; + + self.handle_crypto_get(access_token).await + } + ("auth", &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::ManagePasswords)?; + + self.handle_account_auth_get(access_token).await + } ("auth", &Method::POST) => { + // Validate the access token + access_token.assert_has_permission(Permission::ManagePasswords)?; + self.handle_account_auth_post(req, access_token, body).await } _ => Err(trc::ResourceEvent::NotFound.into_err()), @@ -83,7 +120,7 @@ impl JMAP { // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] - "telemetry" if is_superuser => { + "telemetry" => { // WARNING: TAMPERING WITH THIS FUNCTION IS STRICTLY PROHIBITED // Any attempt to modify, bypass, or disable this license validation mechanism // constitutes a severe violation of the Stalwart Enterprise License Agreement. @@ -94,7 +131,7 @@ impl JMAP { // for copyright infringement, breach of contract, and fraud. if self.core.is_enterprise_edition() { - self.handle_telemetry_api_request(req, path, access_token.primary_id()) + self.handle_telemetry_api_request(req, path, &access_token) .await } else { Err(manage::enterprise()) diff --git a/crates/jmap/src/api/management/principal.rs b/crates/jmap/src/api/management/principal.rs index e88dea9d..e47d56d4 100644 --- a/crates/jmap/src/api/management/principal.rs +++ b/crates/jmap/src/api/management/principal.rs @@ -12,7 +12,7 @@ use directory::{ manage::{self, ManageDirectory}, PrincipalAction, PrincipalField, PrincipalUpdate, PrincipalValue, SpecialSecrets, }, - DirectoryInner, Principal, QueryBy, Type, + DirectoryInner, Permission, Principal, QueryBy, Type, }; use hyper::{header, Method}; @@ -28,7 +28,7 @@ use crate::{ use super::decode_path_element; #[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct PrincipalResponse { +pub struct PrincipalPayload { #[serde(default)] pub id: u32, #[serde(rename = "type")] @@ -68,8 +68,6 @@ pub enum AccountAuthRequest { pub struct AccountAuthResponse { #[serde(rename = "otpEnabled")] pub otp_auth: bool, - #[serde(rename = "isAdministrator")] - pub is_admin: bool, #[serde(rename = "appPasswords")] pub app_passwords: Vec<String>, } @@ -80,43 +78,47 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match (path.get(1), req.method()) { (None, &Method::POST) => { + // Validate the access token + access_token.assert_has_permission(Permission::PrincipalCreate)?; + // Make sure the current directory supports updates self.assert_supported_directory()?; // Create principal - let principal = serde_json::from_slice::<PrincipalResponse>( - body.as_deref().unwrap_or_default(), - ) - .map_err(|err| { - trc::EventType::Resource(trc::ResourceEvent::BadParameters).from_json_error(err) - })?; + let principal = + serde_json::from_slice::<PrincipalPayload>(body.as_deref().unwrap_or_default()) + .map_err(|err| { + trc::EventType::Resource(trc::ResourceEvent::BadParameters) + .from_json_error(err) + })?; + + let principal = Principal::new(principal.id, principal.typ) + .with_field(PrincipalField::Name, principal.name) + .with_field(PrincipalField::Secrets, principal.secrets) + .with_field(PrincipalField::Quota, principal.quota) + .with_field(PrincipalField::Emails, principal.emails) + .with_field(PrincipalField::MemberOf, principal.member_of) + .with_field(PrincipalField::Members, principal.members) + .with_opt_field(PrincipalField::Description, principal.description); Ok(JsonResponse::new(json!({ "data": self .core .storage .data - .create_account( - Principal { - id: principal.id, - typ: principal.typ, - quota: principal.quota, - name: principal.name, - secrets: principal.secrets, - emails: principal.emails, - member_of: principal.member_of, - description: principal.description, - }, - principal.members, - ) + .create_account(principal) .await?, })) .into_http_response()) } (None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::PrincipalList)?; + // List principal ids let params = UrlParams::new(req.uri().query()); let filter = params.get("filter"); @@ -144,6 +146,20 @@ impl JMAP { .into_http_response()) } (Some(name), method) => { + // Validate the access token + match *method { + Method::GET => { + access_token.assert_has_permission(Permission::PrincipalGet)?; + } + Method::DELETE => { + access_token.assert_has_permission(Permission::PrincipalDelete)?; + } + Method::PATCH => { + access_token.assert_has_permission(Permission::PrincipalUpdate)?; + } + _ => {} + } + // Fetch, update or delete principal let name = decode_path_element(name); let account_id = self @@ -166,19 +182,23 @@ impl JMAP { let principal = self.core.storage.data.map_group_ids(principal).await?; // Obtain quota usage - let mut principal = PrincipalResponse::from(principal); + let mut principal = PrincipalPayload::from(principal); principal.used_quota = self.get_used_quota(account_id).await? as u64; // Obtain member names for member_id in self.core.storage.data.get_members(account_id).await? { - if let Some(member_principal) = self + if let Some(mut member_principal) = self .core .storage .data .query(QueryBy::Id(member_id), false) .await? { - principal.members.push(member_principal.name); + principal.members.push( + member_principal + .take_str(PrincipalField::Name) + .unwrap_or_default(), + ); } } @@ -257,7 +277,6 @@ impl JMAP { ) -> trc::Result<HttpResponse> { let mut response = AccountAuthResponse { otp_auth: false, - is_admin: access_token.is_super_user(), app_passwords: Vec::new(), }; @@ -270,7 +289,7 @@ impl JMAP { .await? .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; - for secret in principal.secrets { + for secret in principal.iter_str(PrincipalField::Secrets) { if secret.is_otp_auth() { response.otp_auth = true; } else if let Some((app_name, _)) = @@ -327,7 +346,7 @@ impl JMAP { } // Handle Fallback admin password changes - if access_token.is_super_user() && access_token.primary_id() == u32::MAX { + if access_token.primary_id() == u32::MAX { match requests.into_iter().next().unwrap() { AccountAuthRequest::SetPassword { password } => { self.core @@ -420,24 +439,31 @@ impl JMAP { Err(manage::unsupported(format!( concat!( "{} directory cannot be managed. ", - "Only internal directories support inserts and update operations." + "Only internal directories support inserts ", + "and update operations." ), class ))) } } -impl From<Principal<String>> for PrincipalResponse { - fn from(principal: Principal<String>) -> Self { - PrincipalResponse { - id: principal.id, - typ: principal.typ, - quota: principal.quota, - name: principal.name, - emails: principal.emails, - member_of: principal.member_of, - description: principal.description, - secrets: principal.secrets, +impl From<Principal> for PrincipalPayload { + fn from(mut principal: Principal) -> Self { + PrincipalPayload { + id: principal.id(), + typ: principal.typ(), + quota: principal.quota(), + name: principal.take_str(PrincipalField::Name).unwrap_or_default(), + emails: principal + .take_str_array(PrincipalField::Emails) + .unwrap_or_default(), + member_of: principal + .take_str_array(PrincipalField::MemberOf) + .unwrap_or_default(), + description: principal.take_str(PrincipalField::Description), + secrets: principal + .take_str_array(PrincipalField::Secrets) + .unwrap_or_default(), used_quota: 0, members: Vec::new(), } diff --git a/crates/jmap/src/api/management/queue.rs b/crates/jmap/src/api/management/queue.rs index 2723827e..46fa0616 100644 --- a/crates/jmap/src/api/management/queue.rs +++ b/crates/jmap/src/api/management/queue.rs @@ -5,6 +5,7 @@ */ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use directory::Permission; use hyper::Method; use mail_auth::{ dmarc::URI, @@ -23,6 +24,7 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -105,6 +107,7 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { let params = UrlParams::new(req.uri().query()); @@ -114,6 +117,9 @@ impl JMAP { req.method(), ) { ("messages", None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::MessageQueueList)?; + let text = params.get("text"); let from = params.get("from"); let to = params.get("to"); @@ -217,6 +223,9 @@ impl JMAP { .into_http_response()) } ("messages", Some(queue_id), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::MessageQueueGet)?; + if let Some(message) = self .smtp .read_message(queue_id.parse().unwrap_or_default()) @@ -231,6 +240,9 @@ impl JMAP { } } ("messages", Some(queue_id), &Method::PATCH) => { + // Validate the access token + access_token.assert_has_permission(Permission::MessageQueueUpdate)?; + let time = params .parse::<FutureTimestamp>("at") .map(|t| t.into_inner()) @@ -278,6 +290,9 @@ impl JMAP { } } ("messages", Some(queue_id), &Method::DELETE) => { + // Validate the access token + access_token.assert_has_permission(Permission::MessageQueueDelete)?; + if let Some(mut message) = self .smtp .read_message(queue_id.parse().unwrap_or_default()) @@ -358,6 +373,9 @@ impl JMAP { } } ("reports", None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::OutgoingReportList)?; + let domain = params.get("domain").map(|d| d.to_lowercase()); let type_ = params.get("type").and_then(|t| match t { "dmarc" => 0u8.into(), @@ -436,6 +454,9 @@ impl JMAP { .into_http_response()) } ("reports", Some(report_id), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::OutgoingReportGet)?; + let mut result = None; if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) { match report_id { @@ -473,6 +494,9 @@ impl JMAP { } } ("reports", Some(report_id), &Method::DELETE) => { + // Validate the access token + access_token.assert_has_permission(Permission::OutgoingReportDelete)?; + if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) { match report_id { QueueClass::DmarcReportHeader(event) => { diff --git a/crates/jmap/src/api/management/reload.rs b/crates/jmap/src/api/management/reload.rs index c8653fb1..c8410fc2 100644 --- a/crates/jmap/src/api/management/reload.rs +++ b/crates/jmap/src/api/management/reload.rs @@ -4,12 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use directory::Permission; use hyper::Method; use serde_json::json; use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, services::housekeeper::Event, JMAP, }; @@ -19,7 +21,11 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsReload)?; + match (path.get(1).copied(), req.method()) { (Some("lookup"), &Method::GET) => { let result = self.core.reload_lookups().await?; @@ -92,18 +98,27 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match (path.get(1).copied(), req.method()) { - (Some("spam-filter"), &Method::GET) => Ok(JsonResponse::new(json!({ - "data": self - .core - .storage - .config - .update_config_resource("spam-filter") - .await?, - })) - .into_http_response()), + (Some("spam-filter"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::UpdateSpamFilter)?; + + Ok(JsonResponse::new(json!({ + "data": self + .core + .storage + .config + .update_config_resource("spam-filter") + .await?, + })) + .into_http_response()) + } (Some("webadmin"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::UpdateWebadmin)?; + self.inner.webadmin.update_and_unpack(&self.core).await?; Ok(JsonResponse::new(json!({ diff --git a/crates/jmap/src/api/management/report.rs b/crates/jmap/src/api/management/report.rs index d8bd0318..a53db1f9 100644 --- a/crates/jmap/src/api/management/report.rs +++ b/crates/jmap/src/api/management/report.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use directory::Permission; use hyper::Method; use mail_auth::report::{ tlsrpt::{FailureDetails, Policy, TlsReport}, @@ -19,6 +20,7 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -35,6 +37,7 @@ impl JMAP { &self, req: &HttpRequest, path: Vec<&str>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match ( path.get(1).copied().unwrap_or_default(), @@ -42,6 +45,9 @@ impl JMAP { req.method(), ) { (class @ ("dmarc" | "tls" | "arf"), None, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::IncomingReportList)?; + let params = UrlParams::new(req.uri().query()); let filter = params.get("text"); let page: usize = params.parse::<usize>("page").unwrap_or_default(); @@ -154,6 +160,9 @@ impl JMAP { .into_http_response()) } (class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::IncomingReportGet)?; + if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) { match &report_id { ReportClass::Tls { .. } => match self @@ -207,6 +216,9 @@ impl JMAP { } } (class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::DELETE) => { + // Validate the access token + access_token.assert_has_permission(Permission::IncomingReportDelete)?; + if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) { let mut batch = BatchBuilder::new(); batch.clear(ValueClass::Report(report_id)); diff --git a/crates/jmap/src/api/management/settings.rs b/crates/jmap/src/api/management/settings.rs index db0b6047..1d46799a 100644 --- a/crates/jmap/src/api/management/settings.rs +++ b/crates/jmap/src/api/management/settings.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use directory::Permission; use hyper::Method; use serde_json::json; use store::ahash::AHashMap; @@ -11,6 +12,7 @@ use utils::{config::ConfigKey, map::vec_map::VecMap, url_params::UrlParams}; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -38,9 +40,13 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match (path.get(1).copied(), req.method()) { (Some("group"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsList)?; + // List settings let params = UrlParams::new(req.uri().query()); let prefix = params @@ -168,6 +174,9 @@ impl JMAP { } } (Some("list"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsList)?; + // List settings let params = UrlParams::new(req.uri().query()); let prefix = params @@ -200,6 +209,9 @@ impl JMAP { .into_http_response()) } (Some("keys"), &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsList)?; + // Obtain keys let params = UrlParams::new(req.uri().query()); let keys = params @@ -232,6 +244,9 @@ impl JMAP { .into_http_response()) } (Some(prefix), &Method::DELETE) if !prefix.is_empty() => { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsDelete)?; + let prefix = decode_path_element(prefix); self.core.storage.config.clear(prefix.as_ref()).await?; @@ -242,6 +257,9 @@ impl JMAP { .into_http_response()) } (None, &Method::POST) => { + // Validate the access token + access_token.assert_has_permission(Permission::SettingsUpdate)?; + let changes = serde_json::from_slice::<Vec<UpdateSettings>>( body.as_deref().unwrap_or_default(), ) diff --git a/crates/jmap/src/api/management/sieve.rs b/crates/jmap/src/api/management/sieve.rs index 1305eafa..8e56ed11 100644 --- a/crates/jmap/src/api/management/sieve.rs +++ b/crates/jmap/src/api/management/sieve.rs @@ -7,6 +7,7 @@ use std::time::SystemTime; use common::{scripts::ScriptModification, IntoString}; +use directory::Permission; use hyper::Method; use serde_json::json; use sieve::{runtime::Variable, Envelope}; @@ -15,6 +16,7 @@ use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, + auth::AccessToken, JMAP, }; @@ -41,7 +43,11 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { + // Validate the access token + access_token.assert_has_permission(Permission::SieveRun)?; + let (script, script_id) = match ( path.get(1).and_then(|name| { self.core diff --git a/crates/jmap/src/api/management/stores.rs b/crates/jmap/src/api/management/stores.rs index c8066441..038f7286 100644 --- a/crates/jmap/src/api/management/stores.rs +++ b/crates/jmap/src/api/management/stores.rs @@ -6,7 +6,10 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use common::manager::webadmin::Resource; -use directory::backend::internal::manage::{self, ManageDirectory}; +use directory::{ + backend::internal::manage::{self, ManageDirectory}, + Permission, +}; use hyper::Method; use serde_json::json; use utils::url_params::UrlParams; @@ -16,6 +19,7 @@ use crate::{ http::{HttpSessionData, ToHttpResponse}, HttpRequest, HttpResponse, JsonResponse, }, + auth::AccessToken, services::housekeeper::{Event, PurgeType}, JMAP, }; @@ -29,6 +33,7 @@ impl JMAP { path: Vec<&str>, body: Option<Vec<u8>>, session: &HttpSessionData, + access_token: &AccessToken, ) -> trc::Result<HttpResponse> { match ( path.get(1).copied(), @@ -37,6 +42,9 @@ impl JMAP { req.method(), ) { (Some("blobs"), Some(blob_hash), _, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::BlobFetch)?; + let blob_hash = URL_SAFE_NO_PAD .decode(decode_path_element(blob_hash).as_bytes()) .map_err(|err| { @@ -69,6 +77,9 @@ impl JMAP { .into_http_response()) } (Some("purge"), Some("blob"), _, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::PurgeBlobStore)?; + self.housekeeper_request(Event::Purge(PurgeType::Blobs { store: self.core.storage.data.clone(), blob_store: self.core.storage.blob.clone(), @@ -76,6 +87,9 @@ impl JMAP { .await } (Some("purge"), Some("data"), id, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::PurgeDataStore)?; + let store = if let Some(id) = id { if let Some(store) = self.core.storage.stores.get(id) { store.clone() @@ -90,6 +104,9 @@ impl JMAP { .await } (Some("purge"), Some("lookup"), id, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::PurgeLookupStore)?; + let store = if let Some(id) = id { if let Some(store) = self.core.storage.lookups.get(id) { store.clone() @@ -104,6 +121,9 @@ impl JMAP { .await } (Some("purge"), Some("account"), id, &Method::GET) => { + // Validate the access token + access_token.assert_has_permission(Permission::PurgeAccount)?; + let account_id = if let Some(id) = id { self.core .storage @@ -133,6 +153,9 @@ impl JMAP { // violators to the fullest extent of the law, including but not limited to claims // for copyright infringement, breach of contract, and fraud. + // Validate the access token + access_token.assert_has_permission(Permission::Undelete)?; + if self.core.is_enterprise_edition() { self.handle_undelete_api_request(req, path, body, session) .await diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index f41e0b76..59f2d81a 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -135,6 +135,11 @@ impl JMAP { session: &HttpSessionData, ) -> trc::Result<ResponseMethod> { let op_start = Instant::now(); + + // Check permissions + access_token.assert_has_jmap_permission(&method)?; + + // Handle method let response = match method { RequestMethod::Get(mut req) => match req.take_arguments() { get::RequestArguments::Email(arguments) => { @@ -177,15 +182,7 @@ impl JMAP { self.vacation_response_get(req).await?.into() } - get::RequestArguments::Principal => { - if self.core.jmap.principal_allow_lookups || access_token.is_super_user() { - self.principal_get(req).await?.into() - } else { - return Err(trc::JmapEvent::Forbidden - .into_err() - .details("Principal lookups are disabled".to_string())); - } - } + get::RequestArguments::Principal => self.principal_get(req).await?.into(), get::RequestArguments::Quota => { access_token.assert_is_member(req.account_id)?; @@ -225,13 +222,7 @@ impl JMAP { self.sieve_script_query(req).await?.into() } query::RequestArguments::Principal => { - if self.core.jmap.principal_allow_lookups || access_token.is_super_user() { - self.principal_query(req, session).await?.into() - } else { - return Err(trc::JmapEvent::Forbidden - .into_err() - .details("Principal lookups are disabled".to_string())); - } + self.principal_query(req, session).await?.into() } query::RequestArguments::Quota => { access_token.assert_is_member(req.account_id)?; diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index 15c342d2..3a426e9b 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -6,7 +6,7 @@ use std::sync::Arc; -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ request::capability::{Capability, Session}, types::{acl::Acl, collection::Collection, id::Id}, @@ -52,7 +52,7 @@ impl JMAP { .query(QueryBy::Id(*id), false) .await .caused_by(trc::location!())? - .map(|p| p.name) + .and_then(|mut p| p.take_str(PrincipalField::Name)) .unwrap_or_else(|| Id::from(*id).to_string()), is_personal, is_readonly, diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index 490f675d..d9ec2d2b 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ error::set::SetError, object::Object, @@ -67,17 +67,10 @@ impl JMAP { } if !collections.is_empty() { - if let Some((_, sharing)) = access_token + access_token .access_to - .iter_mut() - .find(|(account_id, _)| *account_id == acl_item.to_account_id) - { - sharing.union(&collections); - } else { - access_token - .access_to - .push((acl_item.to_account_id, collections)); - } + .get_mut_or_insert_with(acl_item.to_account_id, Bitmap::new) + .union(&collections); } } } @@ -322,7 +315,7 @@ impl JMAP { { let mut acl_obj = Object::with_capacity(value.len() / 2); for item in value { - if let Some(principal) = self + if let Some(mut principal) = self .core .storage .directory @@ -331,7 +324,7 @@ impl JMAP { .unwrap_or_default() { acl_obj.append( - Property::_T(principal.name), + Property::_T(principal.take_str(PrincipalField::Name).unwrap_or_default()), item.grants .map(|acl_item| Value::Text(acl_item.to_string())) .collect::<Vec<_>>(), @@ -402,7 +395,7 @@ impl JMAP { { Ok(Some(principal)) => { acls.push(AclGrant { - account_id: principal.id, + account_id: principal.id(), grants: Bitmap::from(*grants), }); } @@ -443,7 +436,7 @@ impl JMAP { { Ok(Some(principal)) => Ok(( AclGrant { - account_id: principal.id, + account_id: principal.id(), grants: Bitmap::from(*grants), }, acl_patch.get(2).map(|v| v.as_bool().unwrap_or(false)), diff --git a/crates/jmap/src/auth/mod.rs b/crates/jmap/src/auth/mod.rs index 6830f82a..3e5c9f66 100644 --- a/crates/jmap/src/auth/mod.rs +++ b/crates/jmap/src/auth/mod.rs @@ -14,10 +14,14 @@ use aes_gcm_siv::{ AeadInPlace, Aes256GcmSiv, KeyInit, Nonce, }; -use directory::{Principal, Type}; -use jmap_proto::types::{collection::Collection, id::Id}; +use directory::{backend::internal::PrincipalField, Permission, Principal, PERMISSION_BITMAP_SIZE}; +use jmap_proto::{ + request::RequestMethod, + types::{collection::Collection, id::Id}, +}; use store::blake3; -use utils::map::bitmap::Bitmap; +use trc::ipc::bitset::Bitset; +use utils::map::{bitmap::Bitmap, vec_map::VecMap}; pub mod acl; pub mod authenticate; @@ -28,30 +32,45 @@ pub mod rate_limit; pub struct AccessToken { pub primary_id: u32, pub member_of: Vec<u32>, - pub access_to: Vec<(u32, Bitmap<Collection>)>, + pub access_to: VecMap<u32, Bitmap<Collection>>, pub name: String, pub description: Option<String>, pub quota: u64, - pub is_superuser: bool, + pub permissions: Bitset<PERMISSION_BITMAP_SIZE>, } impl AccessToken { - pub fn new(principal: Principal<u32>) -> Self { + pub fn new(mut principal: Principal) -> Self { Self { - primary_id: principal.id, - member_of: principal.member_of, - access_to: Vec::new(), - name: principal.name, - description: principal.description, - quota: principal.quota, - is_superuser: principal.typ == Type::Superuser, + primary_id: principal.id(), + member_of: principal + .iter_int(PrincipalField::MemberOf) + .map(|v| v as u32) + .collect(), + access_to: VecMap::new(), + name: principal.take_str(PrincipalField::Name).unwrap_or_default(), + description: principal.take_str(PrincipalField::Description), + quota: principal.quota(), + permissions: Default::default(), } } - pub fn with_access_to(self, access_to: Vec<(u32, Bitmap<Collection>)>) -> Self { + pub fn from_id(primary_id: u32) -> Self { + Self { + primary_id, + ..Default::default() + } + } + + pub fn with_access_to(self, access_to: VecMap<u32, Bitmap<Collection>>) -> Self { Self { access_to, ..self } } + pub fn with_permission(mut self, permission: Permission) -> Self { + self.permissions.set(permission.id()); + self + } + pub fn state(&self) -> u32 { // Hash state let mut s = DefaultHasher::new(); @@ -71,15 +90,44 @@ impl AccessToken { } pub fn is_member(&self, account_id: u32) -> bool { - self.primary_id == account_id || self.member_of.contains(&account_id) || self.is_superuser + self.primary_id == account_id + || self.member_of.contains(&account_id) + || self.has_permission(Permission::Impersonate) } pub fn is_primary_id(&self, account_id: u32) -> bool { self.primary_id == account_id } - pub fn is_super_user(&self) -> bool { - self.is_superuser + #[inline(always)] + pub fn has_permission(&self, permission: Permission) -> bool { + self.permissions.get(permission.id()) + } + + pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> { + if self.has_permission(permission) { + Ok(()) + } else { + Err(trc::SecurityEvent::Unauthorized + .into_err() + .details(permission.name())) + } + } + + pub fn permissions(&self) -> Vec<Permission> { + let mut permissions = Vec::new(); + for (block_num, bytes) in self.permissions.inner().iter().enumerate() { + let mut bytes = *bytes; + + while bytes != 0 { + let item = std::mem::size_of::<usize>() - 1 - bytes.leading_zeros() as usize; + bytes ^= 1 << item; + permissions.push( + Permission::from_id((block_num * std::mem::size_of::<usize>()) + item).unwrap(), + ); + } + } + permissions } pub fn is_shared(&self, account_id: u32) -> bool { @@ -131,6 +179,127 @@ impl AccessToken { .details(format!("You are not an owner of account {}", account_id))) } } + + pub fn assert_has_jmap_permission(&self, request: &RequestMethod) -> trc::Result<()> { + let permission = match request { + RequestMethod::Get(m) => match &m.arguments { + jmap_proto::method::get::RequestArguments::Email(_) => Permission::JmapEmailGet, + jmap_proto::method::get::RequestArguments::Mailbox => Permission::JmapMailboxGet, + jmap_proto::method::get::RequestArguments::Thread => Permission::JmapThreadGet, + jmap_proto::method::get::RequestArguments::Identity => Permission::JmapIdentityGet, + jmap_proto::method::get::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionGet + } + jmap_proto::method::get::RequestArguments::PushSubscription => { + Permission::JmapPushSubscriptionGet + } + jmap_proto::method::get::RequestArguments::SieveScript => { + Permission::JmapSieveScriptGet + } + jmap_proto::method::get::RequestArguments::VacationResponse => { + Permission::JmapVacationResponseGet + } + jmap_proto::method::get::RequestArguments::Principal => { + Permission::JmapPrincipalGet + } + jmap_proto::method::get::RequestArguments::Quota => Permission::JmapQuotaGet, + jmap_proto::method::get::RequestArguments::Blob(_) => Permission::JmapBlobGet, + }, + RequestMethod::Set(m) => match &m.arguments { + jmap_proto::method::set::RequestArguments::Email => Permission::JmapEmailSet, + jmap_proto::method::set::RequestArguments::Mailbox(_) => Permission::JmapMailboxSet, + jmap_proto::method::set::RequestArguments::Identity => Permission::JmapIdentitySet, + jmap_proto::method::set::RequestArguments::EmailSubmission(_) => { + Permission::JmapEmailSubmissionSet + } + jmap_proto::method::set::RequestArguments::PushSubscription => { + Permission::JmapPushSubscriptionSet + } + jmap_proto::method::set::RequestArguments::SieveScript(_) => { + Permission::JmapSieveScriptSet + } + jmap_proto::method::set::RequestArguments::VacationResponse => { + Permission::JmapVacationResponseSet + } + }, + RequestMethod::Changes(m) => match m.arguments { + jmap_proto::method::changes::RequestArguments::Email => { + Permission::JmapEmailChanges + } + jmap_proto::method::changes::RequestArguments::Mailbox => { + Permission::JmapMailboxChanges + } + jmap_proto::method::changes::RequestArguments::Thread => { + Permission::JmapThreadChanges + } + jmap_proto::method::changes::RequestArguments::Identity => { + Permission::JmapIdentityChanges + } + jmap_proto::method::changes::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionChanges + } + jmap_proto::method::changes::RequestArguments::Quota => { + Permission::JmapQuotaChanges + } + }, + RequestMethod::Copy(m) => match m.arguments { + jmap_proto::method::copy::RequestArguments::Email => Permission::JmapEmailCopy, + }, + RequestMethod::CopyBlob(_) => Permission::JmapBlobCopy, + RequestMethod::ImportEmail(_) => Permission::JmapEmailImport, + RequestMethod::ParseEmail(_) => Permission::JmapEmailParse, + RequestMethod::QueryChanges(m) => match m.arguments { + jmap_proto::method::query::RequestArguments::Email(_) => { + Permission::JmapEmailQueryChanges + } + jmap_proto::method::query::RequestArguments::Mailbox(_) => { + Permission::JmapMailboxQueryChanges + } + jmap_proto::method::query::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionQueryChanges + } + jmap_proto::method::query::RequestArguments::SieveScript => { + Permission::JmapSieveScriptQueryChanges + } + jmap_proto::method::query::RequestArguments::Principal => { + Permission::JmapPrincipalQueryChanges + } + jmap_proto::method::query::RequestArguments::Quota => { + Permission::JmapQuotaQueryChanges + } + }, + RequestMethod::Query(m) => match m.arguments { + jmap_proto::method::query::RequestArguments::Email(_) => Permission::JmapEmailQuery, + jmap_proto::method::query::RequestArguments::Mailbox(_) => { + Permission::JmapMailboxQuery + } + jmap_proto::method::query::RequestArguments::EmailSubmission => { + Permission::JmapEmailSubmissionQuery + } + jmap_proto::method::query::RequestArguments::SieveScript => { + Permission::JmapSieveScriptQuery + } + jmap_proto::method::query::RequestArguments::Principal => { + Permission::JmapPrincipalQuery + } + jmap_proto::method::query::RequestArguments::Quota => Permission::JmapQuotaQuery, + }, + RequestMethod::SearchSnippet(_) => Permission::JmapSearchSnippet, + RequestMethod::ValidateScript(_) => Permission::JmapSieveScriptValidate, + RequestMethod::LookupBlob(_) => Permission::JmapBlobLookup, + RequestMethod::UploadBlob(_) => Permission::JmapBlobUpload, + RequestMethod::Echo(_) => Permission::JmapEcho, + 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")) + } + } } pub struct SymmetricEncrypt { diff --git a/crates/jmap/src/auth/oauth/auth.rs b/crates/jmap/src/auth/oauth/auth.rs index 38cf87e6..b25c13cf 100644 --- a/crates/jmap/src/auth/oauth/auth.rs +++ b/crates/jmap/src/auth/oauth/auth.rs @@ -91,8 +91,8 @@ impl JMAP { json!({ "data": { "code": client_code, - "is_admin": access_token.is_super_user(), - "is_enterprise": is_enterprise, + "permissions": access_token.permissions(), + "isEnterprise": is_enterprise, }, }) } diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs index 1817399c..590a9c4d 100644 --- a/crates/jmap/src/auth/oauth/token.rs +++ b/crates/jmap/src/auth/oauth/token.rs @@ -6,7 +6,7 @@ use std::time::SystemTime; -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use hyper::StatusCode; use mail_builder::encoders::base64::base64_encode; use mail_parser::decoders::base64::base64_decode; @@ -187,7 +187,8 @@ impl JMAP { .await .map_err(|_| "Temporary lookup error")? .ok_or("Account no longer exists")? - .secrets + .take_str_array(PrincipalField::Secrets) + .unwrap_or_default() .into_iter() .next() .ok_or("Failed to obtain password hash") diff --git a/crates/jmap/src/auth/rate_limit.rs b/crates/jmap/src/auth/rate_limit.rs index b5ac3491..97548d20 100644 --- a/crates/jmap/src/auth/rate_limit.rs +++ b/crates/jmap/src/auth/rate_limit.rs @@ -7,6 +7,7 @@ use std::{net::IpAddr, sync::Arc}; use common::listener::limiter::{ConcurrencyLimiter, InFlight}; +use directory::Permission; use trc::AddContext; use crate::JMAP; @@ -61,12 +62,12 @@ impl JMAP { if is_rate_allowed { if let Some(in_flight_request) = limiter.concurrent_requests.is_allowed() { Ok(in_flight_request) - } else if access_token.is_super_user() { + } else if access_token.has_permission(Permission::UnlimitedRequests) { Ok(InFlight::default()) } else { Err(trc::LimitEvent::ConcurrentRequest.into_err()) } - } else if access_token.is_super_user() { + } else if access_token.has_permission(Permission::UnlimitedRequests) { Ok(InFlight::default()) } else { Err(trc::LimitEvent::TooManyRequests.into_err()) @@ -97,7 +98,7 @@ impl JMAP { .is_allowed() { Ok(in_flight_request) - } else if access_token.is_super_user() { + } else if access_token.has_permission(Permission::UnlimitedRequests) { Ok(InFlight::default()) } else { Err(trc::LimitEvent::ConcurrentUpload.into_err()) diff --git a/crates/jmap/src/blob/upload.rs b/crates/jmap/src/blob/upload.rs index 7167f948..867b6cb7 100644 --- a/crates/jmap/src/blob/upload.rs +++ b/crates/jmap/src/blob/upload.rs @@ -6,6 +6,7 @@ use std::sync::Arc; +use directory::Permission; use jmap_proto::{ error::set::SetError, method::upload::{ @@ -149,7 +150,7 @@ impl JMAP { && used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size) || (self.core.jmap.upload_tmp_quota_amount > 0 && used.count + 1 > self.core.jmap.upload_tmp_quota_amount)) - && !access_token.is_super_user() + && !access_token.has_permission(Permission::UnlimitedUploads) { response.not_created.append( create_id, @@ -209,7 +210,7 @@ impl JMAP { && used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size) || (self.core.jmap.upload_tmp_quota_amount > 0 && used.count + 1 > self.core.jmap.upload_tmp_quota_amount)) - && !access_token.is_super_user() + && !access_token.has_permission(Permission::UnlimitedUploads) { let err = Err(trc::LimitEvent::BlobQuota .into_err() diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs index 360219f1..f02d8e24 100644 --- a/crates/jmap/src/identity/get.rs +++ b/crates/jmap/src/identity/get.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, @@ -125,7 +125,8 @@ impl JMAP { .await .caused_by(trc::location!())? .unwrap_or_default(); - if principal.emails.is_empty() { + let num_emails = principal.field_len(PrincipalField::Emails); + if num_emails == 0 { return Ok(identity_ids); } @@ -136,14 +137,14 @@ impl JMAP { // Create identities let name = principal - .description - .unwrap_or(principal.name) + .description() + .unwrap_or(principal.name()) .trim() .to_string(); - let has_many = principal.emails.len() > 1; - for (idx, email) in principal.emails.into_iter().enumerate() { + let has_many = num_emails > 1; + for (idx, email) in principal.iter_str(PrincipalField::Emails).enumerate() { let document_id = idx as u32; - let email = sanitize_email(&email).unwrap_or_default(); + let email = sanitize_email(email).unwrap_or_default(); if email.is_empty() { continue; } diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index e840f90f..3ab94e51 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ error::set::SetError, method::set::{RequestArguments, SetRequest, SetResponse}, @@ -61,11 +61,9 @@ impl JMAP { .storage .directory .query(QueryBy::Id(account_id), false) - .await + .await? .unwrap_or_default() - .unwrap_or_default() - .emails - .contains(email) + .has_str_value(PrincipalField::Emails, email) { response.not_created.append( id, diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index c99ff030..5eb6beb6 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -329,7 +329,7 @@ impl JMAP { .query(QueryBy::Id(account_id), false) .await .add_context(|err| err.caused_by(trc::location!()).account_id(account_id))? - .map(|p| p.quota as i64) + .map(|p| p.quota() as i64) .unwrap_or_default() }) } diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index 3a765296..eea8fb13 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -5,6 +5,7 @@ */ use common::config::jmap::settings::SpecialUse; +use directory::Permission; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::set::{SetRequest, SetResponse}, @@ -295,7 +296,9 @@ impl JMAP { ) -> trc::Result<Result<bool, SetError>> { // Internal folders cannot be deleted #[cfg(feature = "test_mode")] - if [INBOX_ID, TRASH_ID].contains(&document_id) && !access_token.is_super_user() { + if [INBOX_ID, TRASH_ID].contains(&document_id) + && !access_token.has_permission(Permission::DeleteSystemFolders) + { return Ok(Err(SetError::forbidden().with_description( "You are not allowed to delete Inbox, Junk or Trash folders.", ))); diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index b2ab9104..b59abd77 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, @@ -67,16 +67,15 @@ impl JMAP { for property in &properties { let value = match property { Property::Id => Value::Id(id), - Property::Type => Value::Text(principal.typ.to_jmap().to_string()), - Property::Name => Value::Text(principal.name.clone()), + Property::Type => Value::Text(principal.typ().to_jmap().to_string()), + Property::Name => Value::Text(principal.name().to_string()), Property::Description => principal - .description - .clone() - .map(Value::Text) + .description() + .map(|v| Value::Text(v.to_string())) .unwrap_or(Value::Null), Property::Email => principal - .emails - .first() + .iter_str(PrincipalField::Emails) + .next() .map(|email| Value::Text(email.clone())) .unwrap_or(Value::Null), _ => Value::Null, diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index ed53c4aa..0ed616ea 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -37,9 +37,9 @@ impl JMAP { .query(QueryBy::Name(name.as_str()), false) .await? { - if is_set || result_set.results.contains(principal.id) { + if is_set || result_set.results.contains(principal.id()) { result_set.results = - RoaringBitmap::from_sorted_iter([principal.id]).unwrap(); + RoaringBitmap::from_sorted_iter([principal.id()]).unwrap(); } else { result_set.results = RoaringBitmap::new(); } diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index ca2548b9..af20adb5 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -104,7 +104,7 @@ impl JMAP { .query(QueryBy::Id(*uid), false) .await { - Ok(Some(p)) => p.quota as i64, + Ok(Some(p)) => p.quota() as i64, Ok(None) => 0, Err(err) => { trc::error!(err diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs index 08ccb3a5..a107bf2f 100644 --- a/crates/jmap/src/sieve/ingest.rs +++ b/crates/jmap/src/sieve/ingest.rs @@ -7,7 +7,7 @@ use std::borrow::Cow; use common::listener::stream::NullIo; -use directory::QueryBy; +use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property}; use mail_parser::MessageParser; use sieve::{Envelope, Event, Input, Mailbox, Recipient}; @@ -72,9 +72,15 @@ impl JMAP { .query(QueryBy::Id(account_id), false) .await { - Ok(Some(p)) => { + Ok(Some(mut p)) => { instance.set_user_full_name(p.description().unwrap_or_else(|| p.name())); - (p.quota as i64, p.emails.into_iter().next()) + ( + p.quota() as i64, + p.take_str_array(PrincipalField::Emails) + .unwrap_or_default() + .into_iter() + .next(), + ) } Ok(None) => (0, None), Err(err) => { |