summaryrefslogtreecommitdiff
path: root/crates/jmap/src
diff options
context:
space:
mode:
authormdecimus <mauro@stalw.art>2024-09-10 18:44:44 +0200
committermdecimus <mauro@stalw.art>2024-09-10 18:44:44 +0200
commitfbcf55d8e1891b72499fed7d5ff54964c8a2f256 (patch)
treec6f18add7f0b62ce3bb6848c1b8b9e68c4eb4e5e /crates/jmap/src
parent08a95ae58b451dbc2284ed7ac2d413185bc879fe (diff)
Access token permissions
Diffstat (limited to 'crates/jmap/src')
-rw-r--r--crates/jmap/src/api/autoconfig.rs6
-rw-r--r--crates/jmap/src/api/http.rs27
-rw-r--r--crates/jmap/src/api/management/dkim.rs18
-rw-r--r--crates/jmap/src/api/management/domain.rs19
-rw-r--r--crates/jmap/src/api/management/enterprise/telemetry.rs39
-rw-r--r--crates/jmap/src/api/management/log.rs12
-rw-r--r--crates/jmap/src/api/management/mod.rs79
-rw-r--r--crates/jmap/src/api/management/principal.rs108
-rw-r--r--crates/jmap/src/api/management/queue.rs24
-rw-r--r--crates/jmap/src/api/management/reload.rs33
-rw-r--r--crates/jmap/src/api/management/report.rs12
-rw-r--r--crates/jmap/src/api/management/settings.rs18
-rw-r--r--crates/jmap/src/api/management/sieve.rs6
-rw-r--r--crates/jmap/src/api/management/stores.rs25
-rw-r--r--crates/jmap/src/api/request.rs23
-rw-r--r--crates/jmap/src/api/session.rs4
-rw-r--r--crates/jmap/src/auth/acl.rs23
-rw-r--r--crates/jmap/src/auth/mod.rs203
-rw-r--r--crates/jmap/src/auth/oauth/auth.rs4
-rw-r--r--crates/jmap/src/auth/oauth/token.rs5
-rw-r--r--crates/jmap/src/auth/rate_limit.rs7
-rw-r--r--crates/jmap/src/blob/upload.rs5
-rw-r--r--crates/jmap/src/identity/get.rs15
-rw-r--r--crates/jmap/src/identity/set.rs8
-rw-r--r--crates/jmap/src/lib.rs2
-rw-r--r--crates/jmap/src/mailbox/set.rs5
-rw-r--r--crates/jmap/src/principal/get.rs15
-rw-r--r--crates/jmap/src/principal/query.rs4
-rw-r--r--crates/jmap/src/services/ingest.rs2
-rw-r--r--crates/jmap/src/sieve/ingest.rs12
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) => {