Access token permissions

This commit is contained in:
mdecimus 2024-09-10 18:44:44 +02:00
parent 08a95ae58b
commit fbcf55d8e1
128 changed files with 2415 additions and 906 deletions

13
Cargo.lock generated
View file

@ -1667,6 +1667,7 @@ dependencies = [
"parking_lot",
"password-hash",
"pbkdf2",
"proc_macros",
"pwhash",
"regex",
"rustls 0.23.12",
@ -1963,7 +1964,7 @@ version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.77",
]
[[package]]
@ -4505,6 +4506,7 @@ name = "pop3"
version = "0.9.4"
dependencies = [
"common",
"directory",
"imap",
"jmap",
"jmap_proto",
@ -4659,6 +4661,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proc_macros"
version = "0.1.0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "prometheus"
version = "0.13.4"

View file

@ -6,7 +6,7 @@
use std::borrow::Cow;
use prettytable::{Attr, Cell, Row, Table, format};
use prettytable::{format, Attr, Cell, Row, Table};
use reqwest::Method;
use serde_json::Value;

View file

@ -83,8 +83,6 @@ pub struct JmapConfig {
pub encrypt: bool,
pub encrypt_append: bool,
pub principal_allow_lookups: bool,
pub capabilities: BaseCapabilities,
pub session_purge_frequency: SimpleCron,
pub account_purge_frequency: SimpleCron,
@ -371,9 +369,6 @@ impl JmapConfig {
push_max_total: config
.property_or_default("jmap.push.max-total", "100")
.unwrap_or(100),
principal_allow_lookups: config
.property("jmap.principal.allow-lookups")
.unwrap_or(true),
encrypt: config
.property_or_default("storage.encryption.enable", "true")
.unwrap_or(true),

View file

@ -19,7 +19,7 @@ use config::{
storage::Storage,
telemetry::Metrics,
};
use directory::{core::secret::verify_secret_hash, Directory, Principal, QueryBy, Type};
use directory::{core::secret::verify_secret_hash, Directory, Principal, QueryBy};
use expr::if_block::IfBlock;
use listener::{
blocked::{AllowedIps, BlockedIps},
@ -227,7 +227,7 @@ impl Core {
credentials: &Credentials<String>,
remote_ip: IpAddr,
return_member_of: bool,
) -> trc::Result<Principal<u32>> {
) -> trc::Result<Principal> {
// First try to authenticate the user against the default directory
let result = match directory
.query(QueryBy::Credentials(credentials), return_member_of)
@ -237,9 +237,9 @@ impl Core {
trc::event!(
Auth(trc::AuthEvent::Success),
AccountName = credentials.login().to_string(),
AccountId = principal.id,
AccountId = principal.id(),
SpanId = session_id,
Type = principal.typ.as_str(),
Type = principal.typ().as_str(),
);
return Ok(principal);
@ -268,7 +268,6 @@ impl Core {
Auth(trc::AuthEvent::Success),
AccountName = username.clone(),
SpanId = session_id,
Type = Type::Superuser.as_str(),
);
return Ok(Principal::fallback_admin(fallback_pass));
@ -289,8 +288,8 @@ impl Core {
Auth(trc::AuthEvent::Success),
AccountName = username.to_string(),
SpanId = session_id,
AccountId = principal.id,
Type = principal.typ.as_str(),
AccountId = principal.id(),
Type = principal.typ().as_str(),
);
return Ok(principal);

View file

@ -23,7 +23,11 @@ pub(crate) fn sign(
let combined = format!("{}.{}", &protected, &payload);
let signature = key
.sign(&SystemRandom::new(), combined.as_bytes())
.map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).caused_by(trc::location!()).reason(err))?;
.map_err(|err| {
trc::EventType::Acme(trc::AcmeEvent::Error)
.caused_by(trc::location!())
.reason(err)
})?;
let signature = URL_SAFE_NO_PAD.encode(signature.as_ref());
let body = Body {
protected,
@ -31,7 +35,8 @@ pub(crate) fn sign(
signature,
};
serde_json::to_string(&body).map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err))
serde_json::to_string(&body)
.map_err(|err| trc::EventType::Acme(trc::AcmeEvent::Error).from_json_error(err))
}
pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> trc::Result<String> {

View file

@ -6,6 +6,7 @@ resolver = "2"
[dependencies]
utils = { path = "../utils" }
proc_macros = { path = "../utils/proc-macros" }
store = { path = "../store" }
trc = { path = "../trc" }
jmap_proto = { path = "../jmap-proto" }

View file

@ -12,7 +12,7 @@ use crate::{IntoError, Principal, QueryBy};
use super::{ImapDirectory, ImapError};
impl ImapDirectory {
pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal>> {
if let QueryBy::Credentials(credentials) = query {
let mut client = self
.pool

View file

@ -12,7 +12,7 @@ use store::{
use crate::{Principal, QueryBy, Type};
use super::{manage::ManageDirectory, PrincipalIdType};
use super::{manage::ManageDirectory, PrincipalField, PrincipalIdType};
#[allow(async_fn_in_trait)]
pub trait DirectoryStore: Sync + Send {
@ -20,7 +20,7 @@ pub trait DirectoryStore: Sync + Send {
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> trc::Result<Option<Principal<u32>>>;
) -> trc::Result<Option<Principal>>;
async fn email_to_ids(&self, email: &str) -> trc::Result<Vec<u32>>;
async fn is_local_domain(&self, domain: &str) -> trc::Result<bool>;
@ -34,7 +34,7 @@ impl DirectoryStore for Store {
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> trc::Result<Option<Principal<u32>>> {
) -> trc::Result<Option<Principal>> {
let (account_id, secret) = match by {
QueryBy::Name(name) => (self.get_account_id(name).await?, None),
QueryBy::Id(account_id) => (account_id.into(), None),
@ -53,7 +53,7 @@ impl DirectoryStore for Store {
if let Some(account_id) = account_id {
match (
self.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
self.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await?,
@ -61,13 +61,19 @@ impl DirectoryStore for Store {
) {
(Some(mut principal), Some(secret)) if principal.verify_secret(secret).await? => {
if return_member_of {
principal.member_of = self.get_member_of(principal.id).await?;
principal.set(
PrincipalField::MemberOf,
self.get_member_of(principal.id).await?,
);
}
Ok(Some(principal))
}
(Some(mut principal), None) => {
if return_member_of {
principal.member_of = self.get_member_of(principal.id).await?;
principal.set(
PrincipalField::MemberOf,
self.get_member_of(principal.id).await?,
);
}
Ok(Some(principal))
@ -143,11 +149,11 @@ impl DirectoryStore for Store {
let mut results = Vec::new();
for account_id in self.email_to_ids(address).await? {
if let Some(email) = self
.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await?
.and_then(|p| p.emails.into_iter().next())
.and_then(|mut p| p.take_str(PrincipalField::Emails))
{
results.push(email);
}

View file

@ -28,11 +28,7 @@ pub trait ManageDirectory: Sized {
async fn get_account_name(&self, account_id: u32) -> trc::Result<Option<String>>;
async fn get_member_of(&self, account_id: u32) -> trc::Result<Vec<u32>>;
async fn get_members(&self, account_id: u32) -> trc::Result<Vec<u32>>;
async fn create_account(
&self,
principal: Principal<String>,
members: Vec<String>,
) -> trc::Result<u32>;
async fn create_account(&self, principal: Principal) -> trc::Result<u32>;
async fn update_account(
&self,
by: QueryBy<'_>,
@ -44,12 +40,12 @@ pub trait ManageDirectory: Sized {
filter: Option<&str>,
typ: Option<Type>,
) -> trc::Result<Vec<String>>;
async fn map_group_ids(&self, principal: Principal<u32>) -> trc::Result<Principal<String>>;
async fn map_group_ids(&self, principal: Principal) -> trc::Result<Principal>;
async fn map_principal(
&self,
principal: Principal<String>,
principal: Principal,
create_if_missing: bool,
) -> trc::Result<Principal<u32>>;
) -> trc::Result<Principal>;
async fn map_group_names(
&self,
members: Vec<String>,
@ -62,11 +58,11 @@ pub trait ManageDirectory: Sized {
impl ManageDirectory for Store {
async fn get_account_name(&self, account_id: u32) -> trc::Result<Option<String>> {
self.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
self.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await
.map(|v| if let Some(v) = v { Some(v.name) } else { None })
.map(|v| v.and_then(|mut v| v.take_str(PrincipalField::Name)))
.caused_by(trc::location!())
}
@ -108,9 +104,9 @@ impl ManageDirectory for Store {
ValueClass::Directory(DirectoryClass::Principal(MaybeDynamicId::Dynamic(0))),
Principal {
typ: Type::Individual,
name: name.to_string(),
..Default::default()
},
}
.with_field(PrincipalField::Name, name.to_string()),
);
match self
@ -133,39 +129,42 @@ impl ManageDirectory for Store {
}
}
async fn create_account(
&self,
principal: Principal<String>,
members: Vec<String>,
) -> trc::Result<u32> {
async fn create_account(&self, mut principal: Principal) -> trc::Result<u32> {
// Make sure the principal has a name
if principal.name.is_empty() {
let name = principal.name().to_lowercase();
if name.is_empty() {
return Err(err_missing(PrincipalField::Name));
}
// Map group names
let members = self
.map_group_names(
principal
.take(PrincipalField::Members)
.map(|v| v.into_str_array())
.unwrap_or_default(),
false,
)
.await
.caused_by(trc::location!())?;
let mut principal = self
.map_principal(principal, false)
.await
.caused_by(trc::location!())?;
let members = self
.map_group_names(members, false)
.await
.caused_by(trc::location!())?;
// Make sure new name is not taken
principal.name = principal.name.to_lowercase();
if self
.get_account_id(&principal.name)
.get_account_id(&name)
.await
.caused_by(trc::location!())?
.is_some()
{
return Err(err_exists(PrincipalField::Name, principal.name));
return Err(err_exists(PrincipalField::Name, name));
}
principal.set(PrincipalField::Name, name);
// Make sure the e-mail is not taken and validate domain
for email in principal.emails.iter_mut() {
for email in principal.iter_mut_str(PrincipalField::Emails) {
*email = email.to_lowercase();
if self.rcpt(email).await.caused_by(trc::location!())? {
return Err(err_exists(PrincipalField::Emails, email.to_string()));
@ -183,14 +182,14 @@ impl ManageDirectory for Store {
// Write principal
let mut batch = BatchBuilder::new();
let ptype = DynamicPrincipalIdType(principal.typ.into_base_type());
let ptype = DynamicPrincipalIdType(principal.typ);
batch
.with_account_id(u32::MAX)
.with_collection(Collection::Principal)
.create_document()
.assert_value(
ValueClass::Directory(DirectoryClass::NameToId(
principal.name.clone().into_bytes(),
principal.name().to_string().into_bytes(),
)),
(),
)
@ -199,30 +198,40 @@ impl ManageDirectory for Store {
principal.clone(),
)
.set(
ValueClass::Directory(DirectoryClass::NameToId(principal.name.into_bytes())),
ValueClass::Directory(DirectoryClass::NameToId(
principal
.take_str(PrincipalField::Name)
.unwrap()
.into_bytes(),
)),
ptype,
);
// Write email to id mapping
for email in principal.emails {
batch.set(
ValueClass::Directory(DirectoryClass::EmailToId(email.into_bytes())),
ptype,
);
if let Some(emails) = principal
.take(PrincipalField::Emails)
.map(|v| v.into_str_array())
{
for email in emails {
batch.set(
ValueClass::Directory(DirectoryClass::EmailToId(email.into_bytes())),
ptype,
);
}
}
// Write membership
for member_of in principal.member_of {
for member_of in principal.iter_int(PrincipalField::MemberOf) {
batch.set(
ValueClass::Directory(DirectoryClass::MemberOf {
principal_id: MaybeDynamicId::Dynamic(0),
member_of: MaybeDynamicId::Static(member_of),
member_of: MaybeDynamicId::Static(member_of as u32),
}),
vec![],
);
batch.set(
ValueClass::Directory(DirectoryClass::Members {
principal_id: MaybeDynamicId::Static(member_of),
principal_id: MaybeDynamicId::Static(member_of as u32),
has_member: MaybeDynamicId::Dynamic(0),
}),
vec![],
@ -261,8 +270,8 @@ impl ManageDirectory for Store {
QueryBy::Credentials(_) => unreachable!(),
};
let principal = self
.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
let mut principal = self
.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await
@ -288,14 +297,21 @@ impl ManageDirectory for Store {
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.clear(DirectoryClass::NameToId(principal.name.into_bytes()))
.clear(DirectoryClass::NameToId(
principal
.take_str(PrincipalField::Name)
.unwrap_or_default()
.into_bytes(),
))
.clear(DirectoryClass::Principal(MaybeDynamicId::Static(
account_id,
)))
.clear(DirectoryClass::UsedQuota(account_id));
for email in principal.emails {
batch.clear(DirectoryClass::EmailToId(email.into_bytes()));
if let Some(emails) = principal.take_str_array(PrincipalField::Emails) {
for email in emails {
batch.clear(DirectoryClass::EmailToId(email.into_bytes()));
}
}
for member_id in self
@ -352,7 +368,7 @@ impl ManageDirectory for Store {
// Fetch principal
let mut principal = self
.get_value::<HashedValue<Principal<u32>>>(ValueKey::from(ValueClass::Directory(
.get_value::<HashedValue<Principal>>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await
@ -371,8 +387,7 @@ impl ManageDirectory for Store {
// Apply changes
let mut batch = BatchBuilder::new();
let ptype =
PrincipalIdType::new(account_id, principal.inner.typ.into_base_type()).serialize();
let ptype = PrincipalIdType::new(account_id, principal.inner.typ).serialize();
let update_principal = !changes.is_empty()
&& !changes
.iter()
@ -391,7 +406,7 @@ impl ManageDirectory for Store {
(PrincipalAction::Set, PrincipalField::Name, PrincipalValue::String(new_name)) => {
// Make sure new name is not taken
let new_name = new_name.to_lowercase();
if principal.inner.name != new_name {
if principal.inner.name() != new_name {
if self
.get_account_id(&new_name)
.await
@ -402,10 +417,10 @@ impl ManageDirectory for Store {
}
batch.clear(ValueClass::Directory(DirectoryClass::NameToId(
principal.inner.name.as_bytes().to_vec(),
principal.inner.name().as_bytes().to_vec(),
)));
principal.inner.name.clone_from(&new_name);
principal.inner.set(PrincipalField::Name, new_name.clone());
batch.set(
ValueClass::Directory(DirectoryClass::NameToId(new_name.into_bytes())),
@ -413,35 +428,27 @@ impl ManageDirectory for Store {
);
}
}
(PrincipalAction::Set, PrincipalField::Type, PrincipalValue::String(new_type)) => {
if let Some(new_type) = Type::parse(&new_type) {
if matches!(principal.inner.typ, Type::Individual | Type::Superuser)
&& matches!(new_type, Type::Individual | Type::Superuser)
{
principal.inner.typ = new_type;
continue;
}
}
return Err(trc::ManageEvent::NotSupported.caused_by(trc::location!()));
}
(
PrincipalAction::Set,
PrincipalField::Secrets,
PrincipalValue::StringList(secrets),
value @ (PrincipalValue::StringList(_) | PrincipalValue::String(_)),
) => {
principal.inner.secrets = secrets;
principal.inner.set(PrincipalField::Secrets, value);
}
(
PrincipalAction::AddItem,
PrincipalField::Secrets,
PrincipalValue::String(secret),
) => {
if !principal.inner.secrets.contains(&secret) {
if secret.is_otp_auth() && !principal.inner.secrets.is_empty() {
if !principal
.inner
.has_str_value(PrincipalField::Secrets, &secret)
{
if secret.is_otp_auth() {
// Add OTP Auth URLs to the beginning of the list
principal.inner.secrets.insert(0, secret);
principal.inner.prepend_str(PrincipalField::Secrets, secret);
} else {
principal.inner.secrets.push(secret);
principal.inner.append_str(PrincipalField::Secrets, secret);
}
}
}
@ -451,14 +458,17 @@ impl ManageDirectory for Store {
PrincipalValue::String(secret),
) => {
if secret.is_app_password() || secret.is_otp_auth() {
principal.inner.retain_str(PrincipalField::Secrets, |v| {
*v != secret && !v.starts_with(&secret)
});
} else if !secret.is_empty() {
principal
.inner
.secrets
.retain(|v| *v != secret && !v.starts_with(&secret));
} else if !secret.is_empty() {
principal.inner.secrets.retain(|v| *v != secret);
.retain_str(PrincipalField::Secrets, |v| *v != secret);
} else {
principal.inner.secrets.retain(|v| !v.is_password());
principal
.inner
.retain_str(PrincipalField::Secrets, |v| !v.is_password());
}
}
(
@ -467,13 +477,15 @@ impl ManageDirectory for Store {
PrincipalValue::String(description),
) => {
if !description.is_empty() {
principal.inner.description = Some(description);
principal
.inner
.set(PrincipalField::Description, description);
} else {
principal.inner.description = None;
principal.inner.remove(PrincipalField::Description);
}
}
(PrincipalAction::Set, PrincipalField::Quota, PrincipalValue::Integer(quota)) => {
principal.inner.quota = quota;
principal.inner.set(PrincipalField::Quota, quota);
}
// Emails
@ -488,7 +500,7 @@ impl ManageDirectory for Store {
.map(|v| v.to_lowercase())
.collect::<Vec<_>>();
for email in &emails {
if !principal.inner.emails.contains(email) {
if !principal.inner.has_str_value(PrincipalField::Emails, email) {
if self.rcpt(email).await.caused_by(trc::location!())? {
return Err(err_exists(PrincipalField::Emails, email.to_string()));
}
@ -510,7 +522,7 @@ impl ManageDirectory for Store {
}
}
for email in &principal.inner.emails {
for email in principal.inner.iter_str(PrincipalField::Emails) {
if !emails.contains(email) {
batch.clear(ValueClass::Directory(DirectoryClass::EmailToId(
email.as_bytes().to_vec(),
@ -518,7 +530,7 @@ impl ManageDirectory for Store {
}
}
principal.inner.emails = emails;
principal.inner.set(PrincipalField::Emails, emails);
}
(
PrincipalAction::AddItem,
@ -526,7 +538,10 @@ impl ManageDirectory for Store {
PrincipalValue::String(email),
) => {
let email = email.to_lowercase();
if !principal.inner.emails.contains(&email) {
if !principal
.inner
.has_str_value(PrincipalField::Emails, &email)
{
if self.rcpt(&email).await.caused_by(trc::location!())? {
return Err(err_exists(PrincipalField::Emails, email));
}
@ -545,7 +560,7 @@ impl ManageDirectory for Store {
)),
ptype.clone(),
);
principal.inner.emails.push(email);
principal.inner.append_str(PrincipalField::Emails, email);
}
}
(
@ -554,11 +569,16 @@ impl ManageDirectory for Store {
PrincipalValue::String(email),
) => {
let email = email.to_lowercase();
if let Some(pos) = principal.inner.emails.iter().position(|v| *v == email) {
if principal
.inner
.has_str_value(PrincipalField::Emails, &email)
{
principal
.inner
.retain_str(PrincipalField::Emails, |v| *v != email);
batch.clear(ValueClass::Directory(DirectoryClass::EmailToId(
email.as_bytes().to_vec(),
email.into_bytes(),
)));
principal.inner.emails.remove(pos);
}
}
@ -806,49 +826,37 @@ impl ManageDirectory for Store {
self.write(batch.build()).await.map(|_| ())
}
async fn map_group_ids(&self, principal: Principal<u32>) -> trc::Result<Principal<String>> {
let mut mapped = Principal {
id: principal.id,
typ: principal.typ,
quota: principal.quota,
name: principal.name,
secrets: principal.secrets,
emails: principal.emails,
member_of: Vec::with_capacity(principal.member_of.len()),
description: principal.description,
};
for account_id in principal.member_of {
if let Some(name) = self
.get_account_name(account_id)
.await
.caused_by(trc::location!())?
{
mapped.member_of.push(name);
async fn map_group_ids(&self, mut principal: Principal) -> trc::Result<Principal> {
if let Some(member_of) = principal.take_int_array(PrincipalField::MemberOf) {
for account_id in member_of {
if let Some(name) = self
.get_account_name(account_id as u32)
.await
.caused_by(trc::location!())?
{
principal.append_str(PrincipalField::MemberOf, name);
}
}
}
Ok(mapped)
Ok(principal)
}
async fn map_principal(
&self,
principal: Principal<String>,
mut principal: Principal,
create_if_missing: bool,
) -> trc::Result<Principal<u32>> {
Ok(Principal {
id: principal.id,
typ: principal.typ,
quota: principal.quota,
name: principal.name,
secrets: principal.secrets,
emails: principal.emails,
member_of: self
.map_group_names(principal.member_of, create_if_missing)
.await
.caused_by(trc::location!())?,
description: principal.description,
})
) -> trc::Result<Principal> {
if let Some(member_of) = principal.take_str_array(PrincipalField::MemberOf) {
principal.set(
PrincipalField::MemberOf,
self.map_group_names(member_of, create_if_missing)
.await
.caused_by(trc::location!())?,
);
}
Ok(principal)
}
async fn map_group_names(
@ -914,21 +922,20 @@ impl ManageDirectory for Store {
for (account_id, account_name) in results {
let principal = self
.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
DirectoryClass::Principal(account_id),
)))
.await
.caused_by(trc::location!())?
.ok_or_else(|| not_found(account_id.to_string()))?;
if filters.iter().all(|f| {
principal.name.to_lowercase().contains(f)
principal.name().to_lowercase().contains(f)
|| principal
.description
.description()
.as_ref()
.map_or(false, |d| d.to_lowercase().contains(f))
|| principal
.emails
.iter()
.iter_str(PrincipalField::Emails)
.any(|email| email.to_lowercase().contains(f))
}) {
filtered.push(account_name);
@ -1010,7 +1017,7 @@ impl ManageDirectory for Store {
}
}
impl SerializeWithId for Principal<u32> {
impl SerializeWithId for Principal {
fn serialize_with_id(&self, ids: &AssignedIds) -> trc::Result<Vec<u8>> {
let mut principal = self.clone();
principal.id = ids.last_document_id().caused_by(trc::location!())?;
@ -1018,8 +1025,8 @@ impl SerializeWithId for Principal<u32> {
}
}
impl From<Principal<u32>> for MaybeDynamicValue {
fn from(principal: Principal<u32>) -> Self {
impl From<Principal> for MaybeDynamicValue {
fn from(principal: Principal) -> Self {
MaybeDynamicValue::Dynamic(Box::new(principal))
}
}
@ -1040,21 +1047,6 @@ impl From<DynamicPrincipalIdType> for MaybeDynamicValue {
}
}
impl From<Principal<String>> for Principal<u32> {
fn from(principal: Principal<String>) -> Self {
Principal {
id: principal.id,
typ: principal.typ,
quota: principal.quota,
name: principal.name,
secrets: principal.secrets,
emails: principal.emails,
member_of: Vec::with_capacity(0),
description: principal.description,
}
}
}
pub fn err_missing(field: impl Into<trc::Value>) -> trc::Error {
trc::ManageEvent::MissingParameter.ctx(trc::Key::Key, field)
}

View file

@ -9,45 +9,70 @@ pub mod manage;
use std::{fmt::Display, slice::Iter, str::FromStr};
use ahash::AHashMap;
use store::{write::key::KeySerializer, Deserialize, Serialize, U32_LEN};
use utils::codec::leb128::Leb128Iterator;
use crate::{Principal, Type};
const INT_MARKER: u8 = 1 << 7;
pub(super) struct PrincipalIdType {
pub account_id: u32,
pub typ: Type,
}
impl Serialize for Principal<u32> {
impl Serialize for Principal {
fn serialize(self) -> Vec<u8> {
(&self).serialize()
}
}
impl Serialize for &Principal<u32> {
impl Serialize for &Principal {
fn serialize(self) -> Vec<u8> {
let mut serializer = KeySerializer::new(
U32_LEN * 3
U32_LEN * 2
+ 2
+ self.name.len()
+ self.emails.iter().map(|s| s.len()).sum::<usize>()
+ self.secrets.iter().map(|s| s.len()).sum::<usize>()
+ self.description.as_ref().map(|s| s.len()).unwrap_or(0),
+ self
.fields
.values()
.map(|v| v.serialized_size() + 1)
.sum::<usize>(),
)
.write(1u8)
.write(2u8)
.write_leb128(self.id)
.write(self.typ as u8)
.write_leb128(self.quota)
.write_leb128(self.name.len())
.write(self.name.as_bytes())
.write_leb128(self.description.as_ref().map_or(0, |s| s.len()))
.write(self.description.as_deref().unwrap_or_default().as_bytes());
.write_leb128(self.fields.len());
for list in [&self.secrets, &self.emails] {
serializer = serializer.write_leb128(list.len());
for value in list {
serializer = serializer.write_leb128(value.len()).write(value.as_bytes());
for (k, v) in &self.fields {
let id = k.id();
match v {
PrincipalValue::String(v) => {
serializer = serializer
.write(id)
.write_leb128(1usize)
.write_leb128(v.len())
.write(v.as_bytes());
}
PrincipalValue::StringList(l) => {
serializer = serializer.write(id).write_leb128(l.len());
for v in l {
serializer = serializer.write_leb128(v.len()).write(v.as_bytes());
}
}
PrincipalValue::Integer(v) => {
serializer = serializer
.write(id | INT_MARKER)
.write_leb128(1usize)
.write_leb128(*v);
}
PrincipalValue::IntegerList(l) => {
serializer = serializer.write(id | INT_MARKER).write_leb128(l.len());
for v in l {
serializer = serializer.write_leb128(*v);
}
}
}
}
@ -55,7 +80,7 @@ impl Serialize for &Principal<u32> {
}
}
impl Deserialize for Principal<u32> {
impl Deserialize for Principal {
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
deserialize(bytes).ok_or_else(|| {
trc::StoreEvent::DataCorruption
@ -98,32 +123,89 @@ impl PrincipalIdType {
}
}
fn deserialize(bytes: &[u8]) -> Option<Principal<u32>> {
fn deserialize(bytes: &[u8]) -> Option<Principal> {
let mut bytes = bytes.iter();
if bytes.next()? != &1 {
return None;
}
Principal {
id: bytes.next_leb128()?,
typ: Type::from_u8(*bytes.next()?),
quota: bytes.next_leb128()?,
name: deserialize_string(&mut bytes)?,
description: deserialize_string(&mut bytes).map(|v| {
if !v.is_empty() {
Some(v)
} else {
None
let version = *bytes.next()?;
let id = bytes.next_leb128()?;
let type_id = *bytes.next()?;
let typ = Type::from_u8(type_id);
match version {
1 => {
// Version 1 (legacy)
let mut principal = Principal {
id,
typ,
..Default::default()
};
principal.set(PrincipalField::Quota, bytes.next_leb128::<u64>()?);
principal.set(PrincipalField::Name, deserialize_string(&mut bytes)?);
if let Some(description) = deserialize_string(&mut bytes).filter(|s| !s.is_empty()) {
principal.set(PrincipalField::Description, description);
}
})?,
secrets: deserialize_string_list(&mut bytes)?,
emails: deserialize_string_list(&mut bytes)?,
member_of: Vec::new(),
for key in [PrincipalField::Secrets, PrincipalField::Emails] {
for _ in 0..bytes.next_leb128::<usize>()? {
principal.append_str(key, deserialize_string(&mut bytes)?);
}
}
if type_id != 4 {
principal
} else {
principal.into_superuser()
}
.into()
}
2 => {
// Version 2
let num_fields = bytes.next_leb128::<usize>()?;
let mut principal = Principal {
id,
typ,
fields: AHashMap::with_capacity(num_fields),
};
for _ in 0..num_fields {
let id = *bytes.next()?;
let num_values = bytes.next_leb128::<usize>()?;
if (id & INT_MARKER) == 0 {
let field = PrincipalField::from_id(id)?;
if num_values == 1 {
principal.set(field, deserialize_string(&mut bytes)?);
} else {
let mut values = Vec::with_capacity(num_values);
for _ in 0..num_values {
values.push(deserialize_string(&mut bytes)?);
}
principal.set(field, values);
}
} else {
let field = PrincipalField::from_id(id & !INT_MARKER)?;
if num_values == 1 {
principal.set(field, bytes.next_leb128::<u64>()?);
} else {
let mut values = Vec::with_capacity(num_values);
for _ in 0..num_values {
values.push(bytes.next_leb128::<u64>()?);
}
principal.set(field, values);
}
}
}
principal.into()
}
_ => None,
}
.into()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[derive(
Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
pub enum PrincipalField {
#[serde(rename = "name")]
Name,
@ -166,6 +248,7 @@ pub enum PrincipalValue {
String(String),
StringList(Vec<String>),
Integer(u64),
IntegerList(Vec<u64>),
}
impl PrincipalUpdate {
@ -201,6 +284,33 @@ impl Display for PrincipalField {
}
impl PrincipalField {
pub fn id(&self) -> u8 {
match self {
PrincipalField::Name => 0,
PrincipalField::Type => 1,
PrincipalField::Quota => 2,
PrincipalField::Description => 3,
PrincipalField::Secrets => 4,
PrincipalField::Emails => 5,
PrincipalField::MemberOf => 6,
PrincipalField::Members => 7,
}
}
pub fn from_id(id: u8) -> Option<Self> {
match id {
0 => Some(PrincipalField::Name),
1 => Some(PrincipalField::Type),
2 => Some(PrincipalField::Quota),
3 => Some(PrincipalField::Description),
4 => Some(PrincipalField::Secrets),
5 => Some(PrincipalField::Emails),
6 => Some(PrincipalField::MemberOf),
7 => Some(PrincipalField::Members),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
PrincipalField::Name => "name",
@ -224,24 +334,16 @@ fn deserialize_string(bytes: &mut Iter<'_, u8>) -> Option<String> {
String::from_utf8(string).ok()
}
fn deserialize_string_list(bytes: &mut Iter<'_, u8>) -> Option<Vec<String>> {
let len = bytes.next_leb128()?;
let mut list = Vec::with_capacity(len);
for _ in 0..len {
list.push(deserialize_string(bytes)?);
}
Some(list)
}
impl Type {
pub fn parse(value: &str) -> Option<Self> {
match value {
"individual" => Some(Type::Individual),
"superuser" => Some(Type::Superuser),
"group" => Some(Type::Group),
"resource" => Some(Type::Resource),
"location" => Some(Type::Location),
"list" => Some(Type::List),
"tenant" => Some(Type::Tenant),
"superuser" => Some(Type::Individual), // legacy
_ => None,
}
}
@ -252,18 +354,12 @@ impl Type {
1 => Type::Group,
2 => Type::Resource,
3 => Type::Location,
4 => Type::Superuser,
4 => Type::Individual, // legacy
5 => Type::List,
7 => Type::Tenant,
_ => Type::Other,
}
}
pub fn into_base_type(self) -> Self {
match self {
Type::Superuser => Type::Individual,
any => any,
}
}
}
impl FromStr for Type {
@ -275,7 +371,6 @@ impl FromStr for Type {
}
pub trait SpecialSecrets {
fn is_disabled(&self) -> bool;
fn is_otp_auth(&self) -> bool;
fn is_app_password(&self) -> bool;
fn is_password(&self) -> bool;
@ -285,10 +380,6 @@ impl<T> SpecialSecrets for T
where
T: AsRef<str>,
{
fn is_disabled(&self) -> bool {
self.as_ref() == "$disabled$"
}
fn is_otp_auth(&self) -> bool {
self.as_ref().starts_with("otpauth://")
}
@ -298,6 +389,6 @@ where
}
fn is_password(&self) -> bool {
!self.is_disabled() && !self.is_otp_auth() && !self.is_app_password()
!self.is_otp_auth() && !self.is_app_password()
}
}

View file

@ -7,7 +7,10 @@
use ldap3::{Ldap, LdapConnAsync, Scope, SearchEntry};
use mail_send::Credentials;
use crate::{backend::internal::manage::ManageDirectory, IntoError, Principal, QueryBy, Type};
use crate::{
backend::internal::{manage::ManageDirectory, PrincipalField},
IntoError, Principal, QueryBy, Type,
};
use super::{LdapDirectory, LdapMappings};
@ -16,7 +19,7 @@ impl LdapDirectory {
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> trc::Result<Option<Principal<u32>>> {
) -> trc::Result<Option<Principal>> {
let mut conn = self.pool.get().await.map_err(|err| err.into_error())?;
let mut account_id = None;
let account_name;
@ -125,11 +128,11 @@ impl LdapDirectory {
.get_or_create_account_id(&account_name)
.await?;
}
principal.name = account_name;
principal.append_str(PrincipalField::Name, account_name);
// Obtain groups
if return_member_of && !principal.member_of.is_empty() {
for member_of in principal.member_of.iter_mut() {
if return_member_of && principal.has_field(PrincipalField::MemberOf) {
for member_of in principal.iter_mut_str(PrincipalField::MemberOf) {
if member_of.contains('=') {
let (rs, _res) = conn
.search(
@ -163,8 +166,8 @@ impl LdapDirectory {
.await
.map(Some)
} else {
principal.member_of.clear();
Ok(Some(principal.into()))
principal.remove(PrincipalField::MemberOf);
Ok(Some(principal))
}
}
@ -370,7 +373,7 @@ impl LdapDirectory {
&self,
conn: &mut Ldap,
filter: &str,
) -> trc::Result<Option<Principal<String>>> {
) -> trc::Result<Option<Principal>> {
conn.search(
&self.mappings.base_dn,
Scope::Subtree,
@ -400,39 +403,47 @@ impl LdapDirectory {
}
impl LdapMappings {
fn entry_to_principal(&self, entry: SearchEntry) -> Principal<String> {
fn entry_to_principal(&self, entry: SearchEntry) -> Principal {
let mut principal = Principal::default();
for (attr, value) in entry.attrs {
if self.attr_name.contains(&attr) {
principal.name = value.into_iter().next().unwrap_or_default();
principal.set(
PrincipalField::Name,
value.into_iter().next().unwrap_or_default(),
);
} else if self.attr_secret.contains(&attr) {
principal.secrets.extend(value);
for item in value {
principal.append_str(PrincipalField::Secrets, item);
}
} else if self.attr_email_address.contains(&attr) {
for value in value {
if principal.emails.is_empty() {
principal.emails.push(value);
} else {
principal.emails.insert(0, value);
}
for item in value {
principal.prepend_str(PrincipalField::Emails, item);
}
} else if self.attr_email_alias.contains(&attr) {
principal.emails.extend(value);
for item in value {
principal.append_str(PrincipalField::Emails, item);
}
} else if let Some(idx) = self.attr_description.iter().position(|a| a == &attr) {
if principal.description.is_none() || idx == 0 {
principal.description = value.into_iter().next();
if !principal.has_field(PrincipalField::Description) || idx == 0 {
principal.set(
PrincipalField::Description,
value.into_iter().next().unwrap_or_default(),
);
}
} else if self.attr_groups.contains(&attr) {
principal.member_of.extend(value);
for item in value {
principal.append_str(PrincipalField::MemberOf, item);
}
} else if self.attr_quota.contains(&attr) {
if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse() {
principal.quota = quota;
if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse::<u64>() {
principal.set(PrincipalField::Quota, quota);
}
} else if self.attr_type.contains(&attr) {
for value in value {
match value.to_ascii_lowercase().as_str() {
"admin" | "administrator" | "root" | "superuser" => {
principal.typ = Type::Superuser
principal = principal.into_superuser();
}
"posixaccount" | "individual" | "person" | "inetorgperson" => {
principal.typ = Type::Individual

View file

@ -7,7 +7,10 @@
use store::Store;
use utils::config::{utils::AsKey, Config};
use crate::{backend::internal::manage::ManageDirectory, Principal, Type};
use crate::{
backend::internal::{manage::ManageDirectory, PrincipalField},
Principal, Type,
};
use super::{EmailType, MemoryDirectory};
@ -34,12 +37,13 @@ impl MemoryDirectory {
let name = config
.value_require((prefix.as_str(), "principals", lookup_id, "name"))?
.to_string();
let typ = match config.value((prefix.as_str(), "principals", lookup_id, "class")) {
Some("individual") => Type::Individual,
Some("admin") => Type::Superuser,
Some("group") => Type::Group,
_ => Type::Individual,
};
let (typ, is_superuser) =
match config.value((prefix.as_str(), "principals", lookup_id, "class")) {
Some("individual") => (Type::Individual, false),
Some("admin") => (Type::Individual, true),
Some("group") => (Type::Group, false),
_ => (Type::Individual, false),
};
// Obtain id
let id = directory
@ -57,14 +61,30 @@ impl MemoryDirectory {
})
.ok()?;
// Create principal
let mut principal = if is_superuser {
Principal {
id,
typ,
..Default::default()
}
.into_superuser()
} else {
Principal {
id,
typ,
..Default::default()
}
};
// Obtain group ids
let mut member_of = Vec::new();
for group in config
.values((prefix.as_str(), "principals", lookup_id, "member-of"))
.map(|(_, s)| s.to_string())
.collect::<Vec<_>>()
{
member_of.push(
principal.append_int(
PrincipalField::MemberOf,
directory
.data_store
.get_or_create_account_id(&group)
@ -83,7 +103,6 @@ impl MemoryDirectory {
}
// Parse email addresses
let mut emails = Vec::new();
for (pos, (_, email)) in config
.values((prefix.as_str(), "principals", lookup_id, "email"))
.enumerate()
@ -102,7 +121,7 @@ impl MemoryDirectory {
directory.domains.insert(domain.to_lowercase());
}
emails.push(email.to_lowercase());
principal.append_str(PrincipalField::Emails, email.to_lowercase());
}
// Parse mailing lists
@ -119,23 +138,20 @@ impl MemoryDirectory {
}
}
directory.principals.push(Principal {
name: name.clone(),
secrets: config
.values((prefix.as_str(), "principals", lookup_id, "secret"))
.map(|(_, v)| v.to_string())
.collect(),
typ,
description: config
.value((prefix.as_str(), "principals", lookup_id, "description"))
.map(|v| v.to_string()),
quota: config
.property((prefix.as_str(), "principals", lookup_id, "quota"))
.unwrap_or(0),
member_of,
id,
emails,
});
principal.set(PrincipalField::Name, name.clone());
for (_, secret) in config.values((prefix.as_str(), "principals", lookup_id, "secret")) {
principal.append_str(PrincipalField::Secrets, secret.to_string());
}
if let Some(description) =
config.value((prefix.as_str(), "principals", lookup_id, "description"))
{
principal.set(PrincipalField::Description, description.to_string());
}
if let Some(quota) =
config.property::<u64>((prefix.as_str(), "principals", lookup_id, "quota"))
{
principal.set(PrincipalField::Quota, quota);
}
}
Some(directory)

View file

@ -6,16 +6,16 @@
use mail_send::Credentials;
use crate::{Principal, QueryBy};
use crate::{backend::internal::PrincipalField, Principal, QueryBy};
use super::{EmailType, MemoryDirectory};
impl MemoryDirectory {
pub async fn query(&self, by: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
pub async fn query(&self, by: QueryBy<'_>) -> trc::Result<Option<Principal>> {
match by {
QueryBy::Name(name) => {
for principal in &self.principals {
if principal.name == name {
if principal.name() == name {
return Ok(Some(principal.clone()));
}
}
@ -35,7 +35,7 @@ impl MemoryDirectory {
};
for principal in &self.principals {
if &principal.name == username {
if principal.name() == username {
return if principal.verify_secret(secret).await? {
Ok(Some(principal.clone()))
} else {
@ -87,8 +87,10 @@ impl MemoryDirectory {
if let EmailType::List(uid) = item {
for principal in &self.principals {
if principal.id == *uid {
if let Some(addr) = principal.emails.first() {
result.push(addr.clone())
if let Some(addr) =
principal.iter_str(PrincipalField::Emails).next()
{
result.push(addr.to_string())
}
break;
}

View file

@ -14,7 +14,7 @@ pub mod lookup;
#[derive(Debug)]
pub struct MemoryDirectory {
principals: Vec<Principal<u32>>,
principals: Vec<Principal>,
emails_to_ids: AHashMap<String, Vec<EmailType>>,
pub(crate) data_store: Store,
domains: AHashSet<String>,

View file

@ -12,7 +12,7 @@ use crate::{IntoError, Principal, QueryBy};
use super::{SmtpClient, SmtpDirectory};
impl SmtpDirectory {
pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal<u32>>> {
pub async fn query(&self, query: QueryBy<'_>) -> trc::Result<Option<Principal>> {
if let QueryBy::Credentials(credentials) = query {
self.pool
.get()
@ -93,7 +93,7 @@ impl SmtpClient {
async fn authenticate(
&mut self,
credentials: &Credentials<String>,
) -> trc::Result<Option<Principal<u32>>> {
) -> trc::Result<Option<Principal>> {
match self
.client
.authenticate(credentials, &self.capabilities)

View file

@ -8,7 +8,10 @@ use mail_send::Credentials;
use store::{NamedRows, Rows, Value};
use trc::AddContext;
use crate::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type};
use crate::{
backend::internal::{manage::ManageDirectory, PrincipalField, PrincipalValue},
Principal, QueryBy, Type,
};
use super::{SqlDirectory, SqlMappings};
@ -17,7 +20,7 @@ impl SqlDirectory {
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> trc::Result<Option<Principal<u32>>> {
) -> trc::Result<Option<Principal>> {
let mut account_id = None;
let account_name;
let mut secret = None;
@ -99,22 +102,20 @@ impl SqlDirectory {
.await
.caused_by(trc::location!())?;
}
principal.name = account_name;
principal.set(PrincipalField::Name, account_name);
// Obtain members
if return_member_of && !self.mappings.query_members.is_empty() {
for row in self
.store
.query::<Rows>(
&self.mappings.query_members,
vec![principal.name.clone().into()],
)
.query::<Rows>(&self.mappings.query_members, vec![principal.name().into()])
.await
.caused_by(trc::location!())?
.rows
{
if let Some(Value::Text(account_id)) = row.values.first() {
principal.member_of.push(
principal.append_int(
PrincipalField::MemberOf,
self.data_store
.get_or_create_account_id(account_id)
.await
@ -126,15 +127,16 @@ impl SqlDirectory {
// Obtain emails
if !self.mappings.query_emails.is_empty() {
principal.emails = self
.store
.query::<Rows>(
&self.mappings.query_emails,
vec![principal.name.clone().into()],
)
.await
.caused_by(trc::location!())?
.into();
principal.set(
PrincipalField::Emails,
PrincipalValue::StringList(
self.store
.query::<Rows>(&self.mappings.query_emails, vec![principal.name().into()])
.await
.caused_by(trc::location!())?
.into(),
),
);
}
Ok(Some(principal))
@ -204,7 +206,7 @@ impl SqlDirectory {
}
impl SqlMappings {
pub fn row_to_principal(&self, rows: NamedRows) -> trc::Result<Principal<u32>> {
pub fn row_to_principal(&self, rows: NamedRows) -> trc::Result<Principal> {
let mut principal = Principal::default();
if let Some(row) = rows.rows.into_iter().next() {
@ -215,22 +217,25 @@ impl SqlMappings {
.any(|c| name.eq_ignore_ascii_case(c))
{
if let Value::Text(secret) = value {
principal.secrets.push(secret.into_owned());
principal.append_str(PrincipalField::Secrets, secret.into_owned());
}
} else if name.eq_ignore_ascii_case(&self.column_type) {
match value.to_str().as_ref() {
"individual" | "person" | "user" => principal.typ = Type::Individual,
"group" => principal.typ = Type::Group,
"admin" | "superuser" | "administrator" => principal.typ = Type::Superuser,
"admin" | "superuser" | "administrator" => {
principal.typ = Type::Individual;
principal = principal.into_superuser();
}
_ => (),
}
} else if name.eq_ignore_ascii_case(&self.column_description) {
if let Value::Text(text) = value {
principal.description = text.into_owned().into();
principal.set(PrincipalField::Description, text.into_owned());
}
} else if name.eq_ignore_ascii_case(&self.column_quota) {
if let Value::Integer(quota) = value {
principal.quota = quota as u64;
principal.set(PrincipalField::Quota, quota as u64);
}
}
}

View file

@ -15,7 +15,7 @@ impl Directory {
&self,
by: QueryBy<'_>,
return_member_of: bool,
) -> trc::Result<Option<Principal<u32>>> {
) -> trc::Result<Option<Principal>> {
match &self.store {
DirectoryInner::Internal(store) => store.query(by, return_member_of).await,
DirectoryInner::Ldap(store) => store.query(by, return_member_of).await,

View file

@ -7,4 +7,5 @@
pub mod cache;
pub mod config;
pub mod dispatch;
pub mod principal;
pub mod secret;

View file

@ -0,0 +1,490 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::collections::hash_map::Entry;
use store::U64_LEN;
use crate::{
backend::internal::{PrincipalField, PrincipalValue},
Principal, Type,
};
impl Principal {
pub fn new(id: u32, typ: Type) -> Self {
Self {
id,
typ,
..Default::default()
}
}
pub fn id(&self) -> u32 {
self.id
}
pub fn typ(&self) -> Type {
self.typ
}
pub fn name(&self) -> &str {
self.get_str(PrincipalField::Name).unwrap_or_default()
}
pub fn has_name(&self) -> bool {
self.fields.contains_key(&PrincipalField::Name)
}
pub fn quota(&self) -> u64 {
self.get_int(PrincipalField::Quota).unwrap_or_default()
}
pub fn description(&self) -> Option<&str> {
self.get_str(PrincipalField::Description)
}
pub fn get_str(&self, key: PrincipalField) -> Option<&str> {
self.fields.get(&key).and_then(|v| v.as_str())
}
pub fn get_int(&self, key: PrincipalField) -> Option<u64> {
self.fields.get(&key).and_then(|v| v.as_int())
}
pub fn take(&mut self, key: PrincipalField) -> Option<PrincipalValue> {
self.fields.remove(&key)
}
pub fn take_str(&mut self, key: PrincipalField) -> Option<String> {
self.take(key).and_then(|v| match v {
PrincipalValue::String(s) => Some(s),
PrincipalValue::StringList(l) => l.into_iter().next(),
PrincipalValue::Integer(i) => Some(i.to_string()),
PrincipalValue::IntegerList(l) => l.into_iter().next().map(|i| i.to_string()),
})
}
pub fn take_str_array(&mut self, key: PrincipalField) -> Option<Vec<String>> {
self.take(key).map(|v| v.into_str_array())
}
pub fn take_int_array(&mut self, key: PrincipalField) -> Option<Vec<u64>> {
self.take(key).map(|v| v.into_int_array())
}
pub fn iter_str(
&self,
key: PrincipalField,
) -> Box<dyn Iterator<Item = &String> + Sync + Send + '_> {
self.fields
.get(&key)
.map(|v| v.iter_str())
.unwrap_or_else(|| Box::new(std::iter::empty()))
}
pub fn iter_mut_str(
&mut self,
key: PrincipalField,
) -> Box<dyn Iterator<Item = &mut String> + Sync + Send + '_> {
self.fields
.get_mut(&key)
.map(|v| v.iter_mut_str())
.unwrap_or_else(|| Box::new(std::iter::empty()))
}
pub fn iter_int(
&self,
key: PrincipalField,
) -> Box<dyn Iterator<Item = u64> + Sync + Send + '_> {
self.fields
.get(&key)
.map(|v| v.iter_int())
.unwrap_or_else(|| Box::new(std::iter::empty()))
}
pub fn iter_mut_int(
&mut self,
key: PrincipalField,
) -> Box<dyn Iterator<Item = &mut u64> + Sync + Send + '_> {
self.fields
.get_mut(&key)
.map(|v| v.iter_mut_int())
.unwrap_or_else(|| Box::new(std::iter::empty()))
}
pub fn append_int(&mut self, key: PrincipalField, value: impl Into<u64>) -> &mut Self {
let value = value.into();
match self.fields.entry(key) {
Entry::Occupied(v) => {
let v = v.into_mut();
match v {
PrincipalValue::IntegerList(v) => {
v.push(value);
}
PrincipalValue::Integer(i) => {
*v = PrincipalValue::IntegerList(vec![*i, value]);
}
PrincipalValue::String(s) => {
*v =
PrincipalValue::IntegerList(vec![s.parse().unwrap_or_default(), value]);
}
PrincipalValue::StringList(l) => {
*v = PrincipalValue::IntegerList(
l.iter()
.map(|s| s.parse().unwrap_or_default())
.chain(std::iter::once(value))
.collect(),
);
}
}
}
Entry::Vacant(v) => {
v.insert(PrincipalValue::IntegerList(vec![value]));
}
}
self
}
pub fn append_str(&mut self, key: PrincipalField, value: impl Into<String>) -> &mut Self {
let value = value.into();
match self.fields.entry(key) {
Entry::Occupied(v) => {
let v = v.into_mut();
match v {
PrincipalValue::StringList(v) => {
v.push(value);
}
PrincipalValue::String(s) => {
*v = PrincipalValue::StringList(vec![std::mem::take(s), value]);
}
PrincipalValue::Integer(i) => {
*v = PrincipalValue::StringList(vec![i.to_string(), value]);
}
PrincipalValue::IntegerList(l) => {
*v = PrincipalValue::StringList(
l.iter()
.map(|i| i.to_string())
.chain(std::iter::once(value))
.collect(),
);
}
}
}
Entry::Vacant(v) => {
v.insert(PrincipalValue::StringList(vec![value]));
}
}
self
}
pub fn prepend_str(&mut self, key: PrincipalField, value: impl Into<String>) -> &mut Self {
let value = value.into();
match self.fields.entry(key) {
Entry::Occupied(v) => {
let v = v.into_mut();
match v {
PrincipalValue::StringList(v) => {
v.insert(0, value);
}
PrincipalValue::String(s) => {
*v = PrincipalValue::StringList(vec![value, std::mem::take(s)]);
}
PrincipalValue::Integer(i) => {
*v = PrincipalValue::StringList(vec![value, i.to_string()]);
}
PrincipalValue::IntegerList(l) => {
*v = PrincipalValue::StringList(
std::iter::once(value)
.chain(l.iter().map(|i| i.to_string()))
.collect(),
);
}
}
}
Entry::Vacant(v) => {
v.insert(PrincipalValue::StringList(vec![value]));
}
}
self
}
pub fn set(&mut self, key: PrincipalField, value: impl Into<PrincipalValue>) -> &mut Self {
self.fields.insert(key, value.into());
self
}
pub fn with_field(mut self, key: PrincipalField, value: impl Into<PrincipalValue>) -> Self {
self.set(key, value);
self
}
pub fn with_opt_field(
mut self,
key: PrincipalField,
value: Option<impl Into<PrincipalValue>>,
) -> Self {
if let Some(value) = value {
self.set(key, value);
}
self
}
pub fn has_field(&self, key: PrincipalField) -> bool {
self.fields.contains_key(&key)
}
pub fn has_str_value(&self, key: PrincipalField, value: &str) -> bool {
self.fields.get(&key).map_or(false, |v| match v {
PrincipalValue::String(v) => v == value,
PrincipalValue::StringList(l) => l.iter().any(|v| v == value),
PrincipalValue::Integer(_) | PrincipalValue::IntegerList(_) => false,
})
}
pub fn has_int_value(&self, key: PrincipalField, value: u64) -> bool {
self.fields.get(&key).map_or(false, |v| match v {
PrincipalValue::Integer(v) => *v == value,
PrincipalValue::IntegerList(l) => l.iter().any(|v| *v == value),
PrincipalValue::String(_) | PrincipalValue::StringList(_) => false,
})
}
pub fn field_len(&self, key: PrincipalField) -> usize {
self.fields.get(&key).map_or(0, |v| match v {
PrincipalValue::String(_) => 1,
PrincipalValue::StringList(l) => l.len(),
PrincipalValue::Integer(_) => 1,
PrincipalValue::IntegerList(l) => l.len(),
})
}
pub fn remove(&mut self, key: PrincipalField) -> Option<PrincipalValue> {
self.fields.remove(&key)
}
pub fn retain_str<F>(&mut self, key: PrincipalField, mut f: F)
where
F: FnMut(&String) -> bool,
{
if let Some(value) = self.fields.get_mut(&key) {
match value {
PrincipalValue::String(s) => {
if !f(s) {
self.fields.remove(&key);
}
}
PrincipalValue::StringList(l) => {
l.retain(f);
if l.is_empty() {
self.fields.remove(&key);
}
}
_ => {}
}
}
}
pub fn retain_int<F>(&mut self, key: PrincipalField, mut f: F)
where
F: FnMut(&u64) -> bool,
{
if let Some(value) = self.fields.get_mut(&key) {
match value {
PrincipalValue::Integer(i) => {
if !f(i) {
self.fields.remove(&key);
}
}
PrincipalValue::IntegerList(l) => {
l.retain(f);
if l.is_empty() {
self.fields.remove(&key);
}
}
_ => {}
}
}
}
pub fn fallback_admin(fallback_pass: impl Into<String>) -> Self {
Principal {
id: u32::MAX,
typ: Type::Individual,
..Default::default()
}
.with_field(PrincipalField::Name, "Fallback Administrator")
.with_field(
PrincipalField::Secrets,
PrincipalValue::String(fallback_pass.into()),
)
.into_superuser()
}
pub fn into_superuser(mut self) -> Self {
let todo = "add role";
self
}
}
impl PrincipalValue {
pub fn as_str(&self) -> Option<&str> {
match self {
PrincipalValue::String(v) => Some(v.as_str()),
PrincipalValue::StringList(v) => v.first().map(|s| s.as_str()),
_ => None,
}
}
pub fn as_int(&self) -> Option<u64> {
match self {
PrincipalValue::Integer(v) => Some(*v),
PrincipalValue::IntegerList(v) => v.first().copied(),
_ => None,
}
}
pub fn iter_str(&self) -> Box<dyn Iterator<Item = &String> + Sync + Send + '_> {
match self {
PrincipalValue::String(v) => Box::new(std::iter::once(v)),
PrincipalValue::StringList(v) => Box::new(v.iter()),
_ => Box::new(std::iter::empty()),
}
}
pub fn iter_mut_str(&mut self) -> Box<dyn Iterator<Item = &mut String> + Sync + Send + '_> {
match self {
PrincipalValue::String(v) => Box::new(std::iter::once(v)),
PrincipalValue::StringList(v) => Box::new(v.iter_mut()),
_ => Box::new(std::iter::empty()),
}
}
pub fn iter_int(&self) -> Box<dyn Iterator<Item = u64> + Sync + Send + '_> {
match self {
PrincipalValue::Integer(v) => Box::new(std::iter::once(*v)),
PrincipalValue::IntegerList(v) => Box::new(v.iter().copied()),
_ => Box::new(std::iter::empty()),
}
}
pub fn iter_mut_int(&mut self) -> Box<dyn Iterator<Item = &mut u64> + Sync + Send + '_> {
match self {
PrincipalValue::Integer(v) => Box::new(std::iter::once(v)),
PrincipalValue::IntegerList(v) => Box::new(v.iter_mut()),
_ => Box::new(std::iter::empty()),
}
}
pub fn into_array(self) -> Self {
match self {
PrincipalValue::String(v) => PrincipalValue::StringList(vec![v]),
PrincipalValue::Integer(v) => PrincipalValue::IntegerList(vec![v]),
v => v,
}
}
pub fn into_str_array(self) -> Vec<String> {
match self {
PrincipalValue::StringList(v) => v,
PrincipalValue::String(v) => vec![v],
PrincipalValue::Integer(v) => vec![v.to_string()],
PrincipalValue::IntegerList(v) => v.into_iter().map(|v| v.to_string()).collect(),
}
}
pub fn into_int_array(self) -> Vec<u64> {
match self {
PrincipalValue::IntegerList(v) => v,
PrincipalValue::Integer(v) => vec![v],
PrincipalValue::String(v) => vec![v.parse().unwrap_or_default()],
PrincipalValue::StringList(v) => v
.into_iter()
.map(|v| v.parse().unwrap_or_default())
.collect(),
}
}
pub fn serialized_size(&self) -> usize {
match self {
PrincipalValue::String(s) => s.len() + 2,
PrincipalValue::StringList(s) => s.iter().map(|s| s.len() + 2).sum(),
PrincipalValue::Integer(_) => U64_LEN,
PrincipalValue::IntegerList(l) => l.len() * U64_LEN,
}
}
}
impl From<u64> for PrincipalValue {
fn from(v: u64) -> Self {
Self::Integer(v)
}
}
impl From<String> for PrincipalValue {
fn from(v: String) -> Self {
Self::String(v)
}
}
impl From<&str> for PrincipalValue {
fn from(v: &str) -> Self {
Self::String(v.to_string())
}
}
impl From<Vec<String>> for PrincipalValue {
fn from(v: Vec<String>) -> Self {
Self::StringList(v)
}
}
impl From<Vec<u64>> for PrincipalValue {
fn from(v: Vec<u64>) -> Self {
Self::IntegerList(v)
}
}
impl From<u32> for PrincipalValue {
fn from(v: u32) -> Self {
Self::Integer(v as u64)
}
}
impl From<Vec<u32>> for PrincipalValue {
fn from(v: Vec<u32>) -> Self {
Self::IntegerList(v.into_iter().map(|v| v as u64).collect())
}
}
impl Type {
pub fn to_jmap(&self) -> &'static str {
match self {
Self::Individual => "individual",
Self::Group => "group",
Self::Resource => "resource",
Self::Location => "location",
Self::Other => "other",
Self::List => "list",
Self::Tenant => "tenant",
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Individual => "Individual",
Self::Group => "Group",
Self::Resource => "Resource",
Self::Location => "Location",
Self::Tenant => "Tenant",
Self::List => "List",
Self::Other => "Other",
}
}
}

View file

@ -18,10 +18,11 @@ use sha2::Sha512;
use tokio::sync::oneshot;
use totp_rs::TOTP;
use crate::backend::internal::PrincipalField;
use crate::backend::internal::SpecialSecrets;
use crate::Principal;
impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
impl Principal {
pub async fn verify_secret(&self, mut code: &str) -> trc::Result<bool> {
let mut totp_token = None;
let mut is_totp_token_missing = false;
@ -30,12 +31,10 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
let mut is_authenticated = false;
let mut is_app_authenticated = false;
for secret in &self.secrets {
if secret.is_disabled() {
// Account is disabled, no need to check further
let todo = "validate authenticate permission";
return Ok(false);
} else if secret.is_otp_auth() {
for secret in self.iter_str(PrincipalField::Secrets) {
if secret.is_otp_auth() {
if !is_totp_verified && !is_totp_token_missing {
is_totp_required = true;
@ -99,7 +98,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
} else {
if is_totp_verified {
// TOTP URL appeared after password hash in secrets list
for secret in &self.secrets {
for secret in self.iter_str(PrincipalField::Secrets) {
if secret.is_password() && verify_secret_hash(secret, code).await? {
return Ok(true);
}

View file

@ -10,6 +10,7 @@ use std::{fmt::Debug, sync::Arc};
use ahash::AHashMap;
use backend::{
imap::{ImapDirectory, ImapError},
internal::{PrincipalField, PrincipalValue},
ldap::LdapDirectory,
memory::MemoryDirectory,
smtp::SmtpDirectory,
@ -18,6 +19,7 @@ use backend::{
use deadpool::managed::PoolError;
use ldap3::LdapError;
use mail_send::Credentials;
use proc_macros::EnumMethods;
use store::Store;
pub mod backend;
@ -28,24 +30,12 @@ pub struct Directory {
pub cache: Option<CachedDirectory>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Principal<T> {
#[serde(default, skip)]
pub id: u32,
#[serde(rename = "type")]
pub typ: Type,
#[serde(default)]
pub quota: u64,
pub name: String,
#[serde(default)]
pub secrets: Vec<String>,
#[serde(default)]
pub emails: Vec<String>,
#[serde(default)]
#[serde(rename = "memberOf")]
pub member_of: Vec<T>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Principal {
pub(crate) id: u32,
pub(crate) typ: Type,
pub(crate) fields: AHashMap<PrincipalField, PrincipalValue>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@ -59,14 +49,178 @@ pub enum Type {
Resource = 2,
#[serde(rename = "location")]
Location = 3,
#[serde(rename = "superuser")]
Superuser = 4,
#[serde(rename = "list")]
List = 5,
#[serde(rename = "other")]
Other = 6,
#[serde(rename = "tenant")]
Tenant = 7,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, EnumMethods,
)]
#[serde(rename_all = "camelCase")]
pub enum Permission {
// Admin
Impersonate,
UnlimitedRequests,
UnlimitedUploads,
DeleteSystemFolders,
MessageQueueList,
MessageQueueGet,
MessageQueueUpdate,
MessageQueueDelete,
OutgoingReportList,
OutgoingReportGet,
OutgoingReportDelete,
IncomingReportList,
IncomingReportGet,
IncomingReportDelete,
SettingsList,
SettingsUpdate,
SettingsDelete,
SettingsReload,
PrincipalList,
PrincipalGet,
PrincipalUpdate,
PrincipalDelete,
PrincipalCreate,
DomainList,
DomainGet,
DomainCreate,
DomainUpdate,
DomainDelete,
BlobFetch,
PurgeBlobStore,
PurgeDataStore,
PurgeLookupStore,
PurgeAccount,
Undelete,
DkimSignatureCreate,
DkimSignatureGet,
UpdateSpamFilter,
UpdateWebadmin,
LogsView,
SieveRun,
Restart,
TracingList,
TracingGet,
TracingLive,
MetricsList,
MetricsLive,
// Generic
Authenticate,
AuthenticateOauth,
// Account Management
ManageEncryption,
ManagePasswords,
// JMAP
JmapEmailGet,
JmapMailboxGet,
JmapThreadGet,
JmapIdentityGet,
JmapEmailSubmissionGet,
JmapPushSubscriptionGet,
JmapSieveScriptGet,
JmapVacationResponseGet,
JmapPrincipalGet,
JmapQuotaGet,
JmapBlobGet,
JmapEmailSet,
JmapMailboxSet,
JmapIdentitySet,
JmapEmailSubmissionSet,
JmapPushSubscriptionSet,
JmapSieveScriptSet,
JmapVacationResponseSet,
JmapEmailChanges,
JmapMailboxChanges,
JmapThreadChanges,
JmapIdentityChanges,
JmapEmailSubmissionChanges,
JmapQuotaChanges,
JmapEmailCopy,
JmapBlobCopy,
JmapEmailImport,
JmapEmailParse,
JmapEmailQueryChanges,
JmapMailboxQueryChanges,
JmapEmailSubmissionQueryChanges,
JmapSieveScriptQueryChanges,
JmapPrincipalQueryChanges,
JmapQuotaQueryChanges,
JmapEmailQuery,
JmapMailboxQuery,
JmapEmailSubmissionQuery,
JmapSieveScriptQuery,
JmapPrincipalQuery,
JmapQuotaQuery,
JmapSearchSnippet,
JmapSieveScriptValidate,
JmapBlobLookup,
JmapBlobUpload,
JmapEcho,
// IMAP
ImapAuthenticate,
ImapAclGet,
ImapAclSet,
ImapMyRights,
ImapListRights,
ImapAppend,
ImapCapability,
ImapId,
ImapCopy,
ImapMove,
ImapCreate,
ImapDelete,
ImapEnable,
ImapExpunge,
ImapFetch,
ImapIdle,
ImapList,
ImapLsub,
ImapNamespace,
ImapRename,
ImapSearch,
ImapSort,
ImapSelect,
ImapExamine,
ImapStatus,
ImapStore,
ImapSubscribe,
ImapThread,
// SMTP
SmtpAuthenticate,
// POP3
Pop3Authenticate,
Pop3List,
Pop3Uidl,
Pop3Stat,
Pop3Retr,
Pop3Dele,
// ManageSieve
SieveAuthenticate,
SieveListScripts,
SieveSetActive,
SieveGetScript,
SievePutScript,
SieveDeleteScript,
SieveRenameScript,
SieveCheckScript,
SieveHaveSpace,
}
pub const PERMISSION_BITMAP_SIZE: usize =
(Permission::COUNT + std::mem::size_of::<usize>() - 1) / std::mem::size_of::<usize>();
pub enum DirectoryInner {
Internal(Store),
Ldap(LdapDirectory),
@ -82,20 +236,6 @@ pub enum QueryBy<'x> {
Credentials(&'x Credentials<String>),
}
impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
pub fn name(&self) -> &str {
&self.name
}
pub fn has_name(&self) -> bool {
!self.name.is_empty()
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}
impl Default for Directory {
fn default() -> Self {
Self {
@ -111,57 +251,11 @@ impl Debug for Directory {
}
}
impl Type {
pub fn to_jmap(&self) -> &'static str {
match self {
Self::Individual | Self::Superuser => "individual",
Self::Group => "group",
Self::Resource => "resource",
Self::Location => "location",
Self::Other => "other",
Self::List => "list",
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Individual => "Individual",
Self::Group => "Group",
Self::Resource => "Resource",
Self::Location => "Location",
Self::Superuser => "Superuser",
Self::List => "List",
Self::Other => "Other",
}
}
}
#[derive(Default, Clone, Debug)]
pub struct Directories {
pub directories: AHashMap<String, Arc<Directory>>,
}
impl Principal<u32> {
pub fn fallback_admin(fallback_pass: impl Into<String>) -> Self {
Principal {
id: u32::MAX,
typ: Type::Superuser,
quota: 0,
name: "Fallback Administrator".to_string(),
secrets: vec![fallback_pass.into()],
..Default::default()
}
}
}
impl<T: Ord> Principal<T> {
pub fn into_sorted(mut self) -> Self {
self.member_of.sort_unstable();
self.emails.sort_unstable();
self
}
}
trait IntoError {
fn into_error(self) -> trc::Error;
}

View file

@ -501,9 +501,12 @@ impl SerializeResponse for trc::Error {
Some(ResponseCode::NonExistent.as_str())
}
trc::EventType::Store(_) => Some(ResponseCode::ContactAdmin.as_str()),
trc::EventType::Limit(trc::LimitEvent::Quota) => Some(ResponseCode::OverQuota.as_str()),
trc::EventType::Limit(trc::LimitEvent::Quota) => {
Some(ResponseCode::OverQuota.as_str())
}
trc::EventType::Limit(_) => Some(ResponseCode::Limit.as_str()),
trc::EventType::Auth(_) => Some(ResponseCode::AuthenticationFailed.as_str()),
trc::EventType::Security(_) => Some(ResponseCode::AuthorizationFailed.as_str()),
_ => None,
})
{

View file

@ -8,7 +8,7 @@ use common::{
config::jmap::settings::SpecialUse,
listener::{limiter::InFlight, SessionStream},
};
use directory::QueryBy;
use directory::{backend::internal::PrincipalField, QueryBy};
use imap_proto::protocol::list::Attribute;
use jmap::{
auth::{acl::EffectiveAcl, AccessToken},
@ -28,7 +28,7 @@ use super::{Account, AccountId, Mailbox, MailboxId, MailboxSync, Session, Sessio
impl<T: SessionStream> SessionData<T> {
pub async fn new(
session: &Session<T>,
access_token: &AccessToken,
access_token: Arc<AccessToken>,
in_flight: Option<InFlight>,
) -> trc::Result<Self> {
let mut session = SessionData {
@ -39,12 +39,14 @@ impl<T: SessionStream> SessionData<T> {
session_id: session.session_id,
mailboxes: Mutex::new(vec![]),
state: access_token.state().into(),
access_token,
in_flight,
};
let access_token = session.access_token.clone();
// Fetch mailboxes for the main account
let mut mailboxes = vec![session
.fetch_account_mailboxes(session.account_id, None, access_token)
.fetch_account_mailboxes(session.account_id, None, &access_token)
.await
.caused_by(trc::location!())?];
@ -65,11 +67,11 @@ impl<T: SessionStream> SessionData<T> {
.query(QueryBy::Id(account_id), false)
.await
.unwrap_or_default()
.map(|p| p.name)
.and_then(|mut p| p.take_str(PrincipalField::Name))
.unwrap_or_else(|| Id::from(account_id).to_string())
)
.into(),
access_token,
&access_token,
)
.await
.caused_by(trc::location!())?,
@ -389,8 +391,8 @@ impl<T: SessionStream> SessionData<T> {
.directory
.query(QueryBy::Id(account_id), false)
.await
.unwrap_or_default()
.map(|p| p.name)
.caused_by(trc::location!())?
.and_then(|mut p| p.take_str(PrincipalField::Name))
.unwrap_or_else(|| Id::from(account_id).to_string())
);
added_accounts.push(
@ -495,7 +497,7 @@ impl<T: SessionStream> SessionData<T> {
.query(QueryBy::Id(account_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(account_id).to_string())
)
.into()

View file

@ -82,6 +82,7 @@ pub struct Session<T: SessionStream> {
pub struct SessionData<T: SessionStream> {
pub account_id: u32,
pub access_token: Arc<AccessToken>,
pub jmap: JMAP,
pub imap: Arc<Inner>,
pub session_id: u64,
@ -239,6 +240,7 @@ impl<T: SessionStream> SessionData<T> {
stream_tx: new_stream,
state: self.state,
in_flight: self.in_flight,
access_token: self.access_token,
}
}
}

View file

@ -7,7 +7,7 @@
use std::{sync::Arc, time::Instant};
use common::listener::SessionStream;
use directory::QueryBy;
use directory::{backend::internal::PrincipalField, Permission, QueryBy};
use imap_proto::{
protocol::acl::{
Arguments, GetAclResponse, ListRightsResponse, ModRightsOp, MyRightsResponse, Rights,
@ -36,13 +36,16 @@ use trc::AddContext;
use utils::map::bitmap::Bitmap;
use crate::{
core::{MailboxId, Session, SessionData},
core::{MailboxId, Session, SessionData, State},
op::ImapContext,
spawn_op,
};
impl<T: SessionStream> Session<T> {
pub async fn handle_get_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapAuthenticate)?;
let op_start = Instant::now();
let arguments = request.parse_acl(self.version)?;
let is_rev2 = self.version.is_rev2();
@ -69,7 +72,7 @@ impl<T: SessionStream> Session<T> {
.query(QueryBy::Id(item.account_id), false)
.await
.imap_ctx(&arguments.tag, trc::location!())?
.map(|p| p.name)
.and_then(|mut p| p.take_str(PrincipalField::Name))
{
let mut rights = Vec::new();
@ -142,6 +145,9 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_my_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapMyRights)?;
let op_start = Instant::now();
let arguments = request.parse_acl(self.version)?;
let data = self.state.session_data();
@ -224,6 +230,9 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_set_acl(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapAclSet)?;
let op_start = Instant::now();
let command = request.command;
let arguments = request.parse_acl(self.version)?;
@ -252,7 +261,7 @@ impl<T: SessionStream> Session<T> {
.id(arguments.tag.to_string())
.caused_by(trc::location!())
})?
.id;
.id();
// Prepare changes
let mut changes = Object::with_capacity(1);
@ -381,6 +390,9 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_list_rights(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapListRights)?;
let op_start = Instant::now();
let arguments = request.parse_acl(self.version)?;
@ -415,6 +427,15 @@ impl<T: SessionStream> Session<T> {
)
.await
}
pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> {
match &self.state {
State::Authenticated { data } | State::Selected { data, .. } => {
data.access_token.assert_has_permission(permission)
}
State::NotAuthenticated { .. } => Ok(()),
}
}
}
impl<T: SessionStream> SessionData<T> {

View file

@ -6,6 +6,7 @@
use std::{sync::Arc, time::Instant};
use directory::Permission;
use imap_proto::{
protocol::{append::Arguments, select::HighestModSeq},
receiver::Request,
@ -25,6 +26,9 @@ use super::{ImapContext, ToModSeq};
impl<T: SessionStream> Session<T> {
pub async fn handle_append(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapAppend)?;
let op_start = Instant::now();
let arguments = request.parse_append(self.version)?;
let (data, selected_mailbox) = self.state.session_mailbox_state();

View file

@ -5,6 +5,7 @@
*/
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
protocol::{authenticate::Mechanism, capability::Capability},
receiver::{self, Request},
@ -121,6 +122,9 @@ impl<T: SessionStream> Session<T> {
}
};
// Validate access
access_token.assert_has_permission(Permission::ImapAuthenticate)?;
// Cache access token
let access_token = Arc::new(access_token);
self.jmap.cache_access_token(access_token.clone());
@ -128,7 +132,7 @@ impl<T: SessionStream> Session<T> {
// Create session
self.state = State::Authenticated {
data: Arc::new(
SessionData::new(self, &access_token, in_flight)
SessionData::new(self, access_token, in_flight)
.await
.map_err(|err| err.id(tag.clone()))?,
),

View file

@ -8,6 +8,7 @@ use std::time::Instant;
use crate::core::Session;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
protocol::{
capability::{Capability, Response},
@ -19,6 +20,9 @@ use imap_proto::{
impl<T: SessionStream> Session<T> {
pub async fn handle_capability(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapCapability)?;
let op_start = Instant::now();
trc::event!(
Imap(trc::ImapEvent::Capabilities),
@ -45,6 +49,9 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_id(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapId)?;
let op_start = Instant::now();
trc::event!(
Imap(trc::ImapEvent::Id),

View file

@ -6,6 +6,7 @@
use std::{sync::Arc, time::Instant};
use directory::Permission;
use imap_proto::{
protocol::copy_move::Arguments, receiver::Request, Command, ResponseCode, ResponseType,
StatusResponse,
@ -38,6 +39,13 @@ impl<T: SessionStream> Session<T> {
is_move: bool,
is_uid: bool,
) -> trc::Result<()> {
// Validate access
self.assert_has_permission(if is_move {
Permission::ImapMove
} else {
Permission::ImapCopy
})?;
let op_start = Instant::now();
let arguments = request.parse_copy_move(self.version)?;
let (data, src_mailbox) = self.state.mailbox_state();

View file

@ -12,6 +12,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
protocol::{create::Arguments, list::Attribute},
receiver::Request,
@ -30,6 +31,9 @@ use trc::AddContext;
impl<T: SessionStream> Session<T> {
pub async fn handle_create(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapCreate)?;
let data = self.state.session_data();
let version = self.version;

View file

@ -11,6 +11,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
protocol::delete::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
};
@ -21,6 +22,9 @@ use super::ImapContext;
impl<T: SessionStream> Session<T> {
pub async fn handle_delete(&mut self, requests: Vec<Request<Command>>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapDelete)?;
let data = self.state.session_data();
let version = self.version;

View file

@ -8,6 +8,7 @@ use std::time::Instant;
use crate::core::Session;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
protocol::{capability::Capability, enable, ImapResponse, ProtocolVersion},
receiver::Request,
@ -16,6 +17,9 @@ use imap_proto::{
impl<T: SessionStream> Session<T> {
pub async fn handle_enable(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapEnable)?;
let op_start = Instant::now();
let arguments = request.parse_enable()?;

View file

@ -7,6 +7,7 @@
use std::{sync::Arc, time::Instant};
use ahash::AHashMap;
use directory::Permission;
use imap_proto::{
parser::parse_sequence_set,
receiver::{Request, Token},
@ -34,6 +35,9 @@ impl<T: SessionStream> Session<T> {
request: Request<Command>,
is_uid: bool,
) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapExpunge)?;
let op_start = Instant::now();
let (data, mailbox) = self.state.select_data();

View file

@ -12,6 +12,7 @@ use crate::{
};
use ahash::AHashMap;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
parser::PushUnique,
protocol::{
@ -44,6 +45,9 @@ impl<T: SessionStream> Session<T> {
request: Request<Command>,
is_uid: bool,
) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapFetch)?;
let op_start = Instant::now();
let arguments = request.parse_fetch()?;

View file

@ -7,6 +7,7 @@
use std::{sync::Arc, time::Instant};
use ahash::AHashSet;
use directory::Permission;
use imap_proto::{
protocol::{
fetch,
@ -32,6 +33,9 @@ use crate::{
impl<T: SessionStream> Session<T> {
pub async fn handle_idle(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapIdle)?;
let op_start = Instant::now();
let (data, mailbox, types) = match &self.state {
State::Authenticated { data, .. } => {

View file

@ -11,6 +11,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
protocol::{
list::{
@ -30,8 +31,14 @@ impl<T: SessionStream> Session<T> {
let command = request.command;
let is_lsub = command == Command::Lsub;
let arguments = if !is_lsub {
// Validate access
self.assert_has_permission(Permission::ImapList)?;
request.parse_list(self.version)
} else {
// Validate access
self.assert_has_permission(Permission::ImapLsub)?;
request.parse_lsub()
}?;

View file

@ -6,6 +6,7 @@
use crate::core::Session;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
protocol::{namespace::Response, ImapResponse},
receiver::Request,
@ -14,6 +15,9 @@ use imap_proto::{
impl<T: SessionStream> Session<T> {
pub async fn handle_namespace(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapNamespace)?;
trc::event!(
Imap(trc::ImapEvent::Namespace),
SpanId = self.session_id,

View file

@ -11,6 +11,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
protocol::rename::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
};
@ -29,6 +30,9 @@ use super::ImapContext;
impl<T: SessionStream> Session<T> {
pub async fn handle_rename(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapRename)?;
let op_start = Instant::now();
let arguments = request.parse_rename(self.version)?;
let data = self.state.session_data();

View file

@ -7,6 +7,7 @@
use std::{sync::Arc, time::Instant};
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
protocol::{
search::{self, Arguments, Filter, Response, ResultOption},
@ -43,8 +44,14 @@ impl<T: SessionStream> Session<T> {
) -> trc::Result<()> {
let op_start = Instant::now();
let mut arguments = if !is_sort {
// Validate access
self.assert_has_permission(Permission::ImapSearch)?;
request.parse_search(self.version)
} else {
// Validate access
self.assert_has_permission(Permission::ImapSort)?;
request.parse_sort()
}?;

View file

@ -6,6 +6,7 @@
use std::{sync::Arc, time::Instant};
use directory::Permission;
use imap_proto::{
protocol::{
fetch,
@ -26,6 +27,13 @@ use super::{ImapContext, ToModSeq};
impl<T: SessionStream> Session<T> {
pub async fn handle_select(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(if request.command == Command::Select {
Permission::ImapSelect
} else {
Permission::ImapExamine
})?;
let op_start = Instant::now();
let is_select = request.command == Command::Select;
let command = request.command;

View file

@ -12,6 +12,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
parser::PushUnique,
protocol::status::{Status, StatusItem, StatusItemType},
@ -34,6 +35,9 @@ use super::ToModSeq;
impl<T: SessionStream> Session<T> {
pub async fn handle_status(&mut self, request: Request<Command>) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapStatus)?;
let op_start = Instant::now();
let arguments = request.parse_status(self.version)?;
let version = self.version;

View file

@ -12,6 +12,7 @@ use crate::{
};
use ahash::AHashSet;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
protocol::{
fetch::{DataItem, FetchItem},
@ -39,6 +40,9 @@ impl<T: SessionStream> Session<T> {
request: Request<Command>,
is_uid: bool,
) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapStore)?;
let op_start = Instant::now();
let arguments = request.parse_store()?;
let (data, mailbox) = self.state.select_data();

View file

@ -11,6 +11,7 @@ use crate::{
spawn_op,
};
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{receiver::Request, Command, ResponseCode, StatusResponse};
use jmap::mailbox::set::{MailboxSubscribe, SCHEMA};
use jmap_proto::{
@ -30,6 +31,9 @@ impl<T: SessionStream> Session<T> {
request: Request<Command>,
is_subscribe: bool,
) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapSubscribe)?;
let op_start = Instant::now();
let arguments = request.parse_subscribe(self.version)?;
let data = self.state.session_data();

View file

@ -12,6 +12,7 @@ use crate::{
};
use ahash::AHashMap;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::{
protocol::{
thread::{Arguments, Response},
@ -28,6 +29,9 @@ impl<T: SessionStream> Session<T> {
request: Request<Command>,
is_uid: bool,
) -> trc::Result<()> {
// Validate access
self.assert_has_permission(Permission::ImapThread)?;
let op_start = Instant::now();
let command = request.command;
let mut arguments = request.parse_thread()?;

View file

@ -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;
}
}

View file

@ -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(

View file

@ -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()),
}
}

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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())

View file

@ -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(),
}

View file

@ -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) => {

View file

@ -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!({

View file

@ -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));

View file

@ -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(),
)

View file

@ -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

View file

@ -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

View file

@ -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)?;

View file

@ -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,

View file

@ -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)),

View file

@ -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 {

View file

@ -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,
},
})
}

View file

@ -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")

View file

@ -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())

View file

@ -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()

View file

@ -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;
}

View file

@ -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,

View file

@ -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()
})
}

View file

@ -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.",
)));

View file

@ -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,

View file

@ -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();
}

View file

@ -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

View file

@ -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) => {

View file

@ -5,6 +5,7 @@
*/
use common::listener::{limiter::ConcurrencyLimiter, SessionStream};
use directory::Permission;
use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain};
use imap_proto::{
protocol::authenticate::Mechanism,
@ -116,6 +117,9 @@ impl<T: SessionStream> Session<T> {
}
};
// Validate access
access_token.assert_has_permission(Permission::SieveAuthenticate)?;
// Cache access token
let access_token = Arc::new(access_token);
self.jmap.cache_access_token(access_token.clone());

View file

@ -6,13 +6,17 @@
use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use tokio::io::{AsyncRead, AsyncWrite};
use crate::core::{Command, Session, StatusResponse};
impl<T: AsyncRead + AsyncWrite> Session<T> {
impl<T: SessionStream> Session<T> {
pub async fn handle_checkscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
// Validate access
self.assert_has_permission(Permission::SieveCheckScript)?;
let op_start = Instant::now();
if request.tokens.is_empty() {

View file

@ -6,16 +6,20 @@
use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use jmap_proto::types::collection::Collection;
use store::write::log::ChangeLogBuilder;
use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
impl<T: AsyncRead + AsyncWrite> Session<T> {
impl<T: SessionStream> Session<T> {
pub async fn handle_deletescript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
// Validate access
self.assert_has_permission(Permission::SieveDeleteScript)?;
let op_start = Instant::now();
let name = request

View file

@ -6,19 +6,23 @@
use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use jmap::sieve::set::ObjectBlobId;
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
};
use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
impl<T: AsyncRead + AsyncWrite> Session<T> {
impl<T: SessionStream> Session<T> {
pub async fn handle_getscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
// Validate access
self.assert_has_permission(Permission::SieveGetScript)?;
let op_start = Instant::now();
let name = request
.tokens

View file

@ -6,14 +6,18 @@
use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
impl<T: AsyncRead + AsyncWrite> Session<T> {
impl<T: SessionStream> Session<T> {
pub async fn handle_havespace(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
// Validate access
self.assert_has_permission(Permission::SieveHaveSpace)?;
let op_start = Instant::now();
let mut tokens = request.tokens.into_iter();
let name = tokens

View file

@ -6,17 +6,21 @@
use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
};
use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Session, StatusResponse};
impl<T: AsyncRead + AsyncWrite> Session<T> {
impl<T: SessionStream> Session<T> {
pub async fn handle_listscripts(&mut self) -> trc::Result<Vec<u8>> {
// Validate access
self.assert_has_permission(Permission::SieveListScripts)?;
let op_start = Instant::now();
let account_id = self.state.access_token().primary_id();
let document_ids = self

View file

@ -5,8 +5,9 @@
*/
use common::listener::SessionStream;
use directory::Permission;
use crate::core::{Session, StatusResponse};
use crate::core::{Session, State, StatusResponse};
pub mod authenticate;
pub mod capability;
@ -31,4 +32,13 @@ impl<T: SessionStream> Session<T> {
Ok(StatusResponse::ok("Begin TLS negotiation now").into_bytes())
}
pub fn assert_has_permission(&self, permission: Permission) -> trc::Result<()> {
match &self.state {
State::Authenticated { access_token, .. } => {
access_token.assert_has_permission(permission)
}
State::NotAuthenticated { .. } => Ok(()),
}
}
}

View file

@ -6,6 +6,8 @@
use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use jmap::sieve::set::{ObjectBlobId, SCHEMA};
use jmap_proto::{
@ -18,13 +20,15 @@ use store::{
write::{assert::HashedValue, log::LogInsert, BatchBuilder, BlobOp, DirectoryClass},
BlobClass,
};
use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
impl<T: AsyncRead + AsyncWrite> Session<T> {
impl<T: SessionStream> Session<T> {
pub async fn handle_putscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
// Validate access
self.assert_has_permission(Permission::SievePutScript)?;
let op_start = Instant::now();
let mut tokens = request.tokens.into_iter();
let name = tokens

View file

@ -6,6 +6,8 @@
use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use jmap::sieve::set::SCHEMA;
use jmap_proto::{
@ -13,13 +15,15 @@ use jmap_proto::{
types::{collection::Collection, property::Property, value::Value},
};
use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder};
use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
impl<T: AsyncRead + AsyncWrite> Session<T> {
impl<T: SessionStream> Session<T> {
pub async fn handle_renamescript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
// Validate access
self.assert_has_permission(Permission::SieveRenameScript)?;
let op_start = Instant::now();
let mut tokens = request.tokens.into_iter();
let name = tokens

View file

@ -6,16 +6,20 @@
use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use jmap_proto::types::collection::Collection;
use store::write::log::ChangeLogBuilder;
use tokio::io::{AsyncRead, AsyncWrite};
use trc::AddContext;
use crate::core::{Command, Session, StatusResponse};
impl<T: AsyncRead + AsyncWrite> Session<T> {
impl<T: SessionStream> Session<T> {
pub async fn handle_setactive(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> {
// Validate access
self.assert_has_permission(Permission::SieveSetActive)?;
let op_start = Instant::now();
let name = request
.tokens

View file

@ -7,6 +7,7 @@ resolver = "2"
[dependencies]
store = { path = "../store" }
common = { path = "../common" }
directory = { path = "../directory" }
jmap = { path = "../jmap" }
imap = { path = "../imap" }
utils = { path = "../utils" }

View file

@ -9,7 +9,7 @@ use mail_send::Credentials;
use trc::AddContext;
use crate::{
protocol::{request::Error, response::Response, Command, Mechanism},
protocol::{request::Error, Command, Mechanism},
Session, State,
};
@ -117,54 +117,11 @@ impl<T: SessionStream> Session<T> {
self.write_ok("NOOP").await.map(|_| SessionResult::Continue)
}
Command::Rset => self.handle_rset().await.map(|_| SessionResult::Continue),
Command::Capa => {
let mechanisms =
if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth {
vec![Mechanism::Plain, Mechanism::OAuthBearer]
} else {
vec![Mechanism::OAuthBearer]
};
trc::event!(
Pop3(trc::Pop3Event::Capabilities),
SpanId = self.session_id,
Tls = self.stream.is_tls(),
Strict = !self.jmap.core.imap.allow_plain_auth,
Elapsed = trc::Value::Duration(0)
);
self.write_bytes(
Response::Capability::<u32> {
mechanisms,
stls: !self.stream.is_tls(),
}
.serialize(),
)
.await
.map(|_| SessionResult::Continue)
}
Command::Capa => self.handle_capa().await.map(|_| SessionResult::Continue),
Command::Stls => {
trc::event!(
Pop3(trc::Pop3Event::StartTls),
SpanId = self.session_id,
Elapsed = trc::Value::Duration(0)
);
self.write_ok("Begin TLS negotiation now")
.await
.map(|_| SessionResult::UpgradeTls)
}
Command::Utf8 => {
trc::event!(
Pop3(trc::Pop3Event::Utf8),
SpanId = self.session_id,
Elapsed = trc::Value::Duration(0)
);
self.write_ok("UTF8 enabled")
.await
.map(|_| SessionResult::Continue)
self.handle_stls().await.map(|_| SessionResult::UpgradeTls)
}
Command::Utf8 => self.handle_utf8().await.map(|_| SessionResult::Continue),
Command::Auth { mechanism, params } => self
.handle_sasl(mechanism, params)
.await

View file

@ -8,7 +8,7 @@ use std::{net::IpAddr, sync::Arc};
use common::listener::{limiter::InFlight, ServerInstance, SessionStream};
use imap::core::{ImapInstance, Inner};
use jmap::JMAP;
use jmap::{auth::AccessToken, JMAP};
use mailbox::Mailbox;
use protocol::request::Parser;
@ -51,6 +51,7 @@ pub enum State {
Authenticated {
mailbox: Mailbox,
in_flight: Option<InFlight>,
access_token: Arc<AccessToken>,
},
}
@ -68,4 +69,11 @@ impl State {
_ => unreachable!(),
}
}
pub fn access_token(&self) -> &Arc<AccessToken> {
match self {
State::Authenticated { access_token, .. } => access_token,
_ => unreachable!(),
}
}
}

View file

@ -5,6 +5,7 @@
*/
use common::listener::{limiter::ConcurrencyLimiter, SessionStream};
use directory::Permission;
use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain};
use jmap::auth::rate_limit::ConcurrencyLimiters;
use mail_parser::decoders::base64::base64_decode;
@ -112,6 +113,9 @@ impl<T: SessionStream> Session<T> {
}
};
// Validate access
access_token.assert_has_permission(Permission::Pop3Authenticate)?;
// Cache access token
let access_token = Arc::new(access_token);
self.jmap.cache_access_token(access_token.clone());
@ -120,7 +124,11 @@ impl<T: SessionStream> Session<T> {
let mailbox = self.fetch_mailbox(access_token.primary_id()).await?;
// Create session
self.state = State::Authenticated { in_flight, mailbox };
self.state = State::Authenticated {
in_flight,
mailbox,
access_token,
};
self.write_ok("Authentication successful").await
}

View file

@ -7,6 +7,7 @@
use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use jmap_proto::types::{state::StateChange, type_state::DataType};
use store::roaring::RoaringBitmap;
use trc::AddContext;
@ -15,6 +16,11 @@ use crate::{protocol::response::Response, Session, State};
impl<T: SessionStream> Session<T> {
pub async fn handle_dele(&mut self, msgs: Vec<u32>) -> trc::Result<()> {
// Validate access
self.state
.access_token()
.assert_has_permission(Permission::Pop3Dele)?;
let op_start = Instant::now();
let mailbox = self.state.mailbox_mut();
let mut response = Vec::new();

View file

@ -7,6 +7,7 @@
use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use jmap::email::metadata::MessageMetadata;
use jmap_proto::types::{collection::Collection, property::Property};
use store::write::Bincode;
@ -16,6 +17,11 @@ use crate::{protocol::response::Response, Session};
impl<T: SessionStream> Session<T> {
pub async fn handle_fetch(&mut self, msg: u32, lines: Option<u32>) -> trc::Result<()> {
// Validate access
self.state
.access_token()
.assert_has_permission(Permission::Pop3Retr)?;
let op_start = Instant::now();
let mailbox = self.state.mailbox();
if let Some(message) = mailbox.messages.get(msg.saturating_sub(1) as usize) {

View file

@ -7,11 +7,17 @@
use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use crate::{protocol::response::Response, Session};
impl<T: SessionStream> Session<T> {
pub async fn handle_list(&mut self, msg: Option<u32>) -> trc::Result<()> {
// Validate access
self.state
.access_token()
.assert_has_permission(Permission::Pop3List)?;
let op_start = Instant::now();
let mailbox = self.state.mailbox();
if let Some(msg) = msg {
@ -48,6 +54,11 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_uidl(&mut self, msg: Option<u32>) -> trc::Result<()> {
// Validate access
self.state
.access_token()
.assert_has_permission(Permission::Pop3Uidl)?;
let op_start = Instant::now();
let mailbox = self.state.mailbox();
if let Some(msg) = msg {
@ -92,6 +103,11 @@ impl<T: SessionStream> Session<T> {
}
pub async fn handle_stat(&mut self) -> trc::Result<()> {
// Validate access
self.state
.access_token()
.assert_has_permission(Permission::Pop3Stat)?;
let op_start = Instant::now();
let mailbox = self.state.mailbox();

View file

@ -4,7 +4,61 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::listener::SessionStream;
use crate::{
protocol::{response::Response, Mechanism},
Session,
};
pub mod authenticate;
pub mod delete;
pub mod fetch;
pub mod list;
impl<T: SessionStream> Session<T> {
pub async fn handle_capa(&mut self) -> trc::Result<()> {
let mechanisms = if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth {
vec![Mechanism::Plain, Mechanism::OAuthBearer]
} else {
vec![Mechanism::OAuthBearer]
};
trc::event!(
Pop3(trc::Pop3Event::Capabilities),
SpanId = self.session_id,
Tls = self.stream.is_tls(),
Strict = !self.jmap.core.imap.allow_plain_auth,
Elapsed = trc::Value::Duration(0)
);
self.write_bytes(
Response::Capability::<u32> {
mechanisms,
stls: !self.stream.is_tls(),
}
.serialize(),
)
.await
}
pub async fn handle_stls(&mut self) -> trc::Result<()> {
trc::event!(
Pop3(trc::Pop3Event::StartTls),
SpanId = self.session_id,
Elapsed = trc::Value::Duration(0)
);
self.write_ok("Begin TLS negotiation now").await
}
pub async fn handle_utf8(&mut self) -> trc::Result<()> {
trc::event!(
Pop3(trc::Pop3Event::Utf8),
SpanId = self.session_id,
Elapsed = trc::Value::Duration(0)
);
self.write_ok("UTF8 enabled").await
}
}

View file

@ -5,6 +5,7 @@
*/
use common::listener::SessionStream;
use directory::backend::internal::PrincipalField;
use mail_parser::decoders::base64::base64_decode;
use mail_send::Credentials;
use smtp_proto::{IntoString, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2};
@ -177,10 +178,10 @@ impl<T: SessionStream> Session<T> {
.await
{
Ok(principal) => {
let todo = "check smtp auth permissions";
self.data.authenticated_as = authenticated_as.to_lowercase();
self.data.authenticated_emails = principal
.emails
.into_iter()
.iter_str(PrincipalField::Emails)
.map(|e| e.trim().to_lowercase())
.collect();
self.eval_post_auth_params().await;

View file

@ -308,7 +308,9 @@ impl<'x> RocksDBTransaction<'x> {
if !matches {
txn.rollback()?;
return Err(CommitError::Internal(trc::StoreEvent::AssertValueFailed.into()));
return Err(CommitError::Internal(
trc::StoreEvent::AssertValueFailed.into(),
));
}
}
}

View file

@ -7,6 +7,6 @@ edition = "2021"
proc-macro = true
[dependencies]
syn = { version = "1.0", features = ["full"] }
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"

View file

@ -1784,6 +1784,7 @@ impl SecurityEvent {
SecurityEvent::BruteForceBan => "Banned due to brute force attack",
SecurityEvent::LoiterBan => "Banned due to loitering",
SecurityEvent::IpBlocked => "Blocked IP address",
SecurityEvent::Unauthorized => "Unauthorized access",
}
}
@ -1797,6 +1798,7 @@ impl SecurityEvent {
}
SecurityEvent::LoiterBan => "IP address was banned due to multiple loitering events",
SecurityEvent::IpBlocked => "Rejected connection from blocked IP address",
SecurityEvent::Unauthorized => "Account does not have permission to access resource",
}
}
}

View file

@ -261,6 +261,7 @@ impl EventType {
EventType::Auth(cause) => cause.message(),
EventType::Config(_) => "Configuration error",
EventType::Resource(cause) => cause.message(),
EventType::Security(_) => "Insufficient permissions",
_ => "Internal server error",
}
}

View file

@ -51,6 +51,10 @@ impl<const N: usize> Bitset<N> {
}
true
}
pub fn inner(&self) -> &[usize; N] {
&self.0
}
}
impl<const N: usize> Default for Bitset<N> {

View file

@ -202,6 +202,7 @@ pub enum SecurityEvent {
BruteForceBan,
LoiterBan,
IpBlocked,
Unauthorized,
}
#[event_type]

View file

@ -858,6 +858,7 @@ impl EventType {
EventType::Security(SecurityEvent::BruteForceBan) => 549,
EventType::Security(SecurityEvent::LoiterBan) => 550,
EventType::Smtp(SmtpEvent::MailFromNotAllowed) => 551,
EventType::Security(SecurityEvent::Unauthorized) => 552,
}
}
@ -1455,6 +1456,7 @@ impl EventType {
549 => Some(EventType::Security(SecurityEvent::BruteForceBan)),
550 => Some(EventType::Security(SecurityEvent::LoiterBan)),
551 => Some(EventType::Smtp(SmtpEvent::MailFromNotAllowed)),
552 => Some(EventType::Security(SecurityEvent::Unauthorized)),
_ => None,
}
}

Some files were not shown because too many files have changed in this diff Show more