mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2024-09-20 07:16:18 +08:00
Access token permissions
This commit is contained in:
parent
08a95ae58b
commit
fbcf55d8e1
13
Cargo.lock
generated
13
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
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,15 +297,22 @@ 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 {
|
||||
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
|
||||
.get_member_of(account_id)
|
||||
|
@ -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 {
|
||||
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)
|
||||
.get_account_name(account_id as u32)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
mapped.member_of.push(name);
|
||||
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)
|
||||
) -> 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!())?,
|
||||
description: principal.description,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
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);
|
||||
}
|
||||
for key in [PrincipalField::Secrets, PrincipalField::Emails] {
|
||||
for _ in 0..bytes.next_leb128::<usize>()? {
|
||||
principal.append_str(key, deserialize_string(&mut bytes)?);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if type_id != 4 {
|
||||
principal
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?,
|
||||
secrets: deserialize_string_list(&mut bytes)?,
|
||||
emails: deserialize_string_list(&mut bytes)?,
|
||||
member_of: Vec::new(),
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
} 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.append_str(PrincipalField::Secrets, item);
|
||||
}
|
||||
} else if self.attr_email_address.contains(&attr) {
|
||||
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
|
||||
|
|
|
@ -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,11 +37,12 @@ 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
|
||||
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()],
|
||||
)
|
||||
principal.set(
|
||||
PrincipalField::Emails,
|
||||
PrincipalValue::StringList(
|
||||
self.store
|
||||
.query::<Rows>(&self.mappings.query_emails, vec![principal.name().into()])
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
.into();
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -7,4 +7,5 @@
|
|||
pub mod cache;
|
||||
pub mod config;
|
||||
pub mod dispatch;
|
||||
pub mod principal;
|
||||
pub mod secret;
|
||||
|
|
490
crates/directory/src/core/principal.rs
Normal file
490
crates/directory/src/core/principal.rs
Normal 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",
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
{
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()))?,
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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()?;
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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()?;
|
||||
|
||||
|
|
|
@ -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, .. } => {
|
||||
|
|
|
@ -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()
|
||||
}?;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
}?;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()?;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
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)
|
||||
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(),
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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,9 +98,14 @@ 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!({
|
||||
(Some("spam-filter"), &Method::GET) => {
|
||||
// Validate the access token
|
||||
access_token.assert_has_permission(Permission::UpdateSpamFilter)?;
|
||||
|
||||
Ok(JsonResponse::new(json!({
|
||||
"data": self
|
||||
.core
|
||||
.storage
|
||||
|
@ -102,8 +113,12 @@ impl JMAP {
|
|||
.update_config_resource("spam-filter")
|
||||
.await?,
|
||||
}))
|
||||
.into_http_response()),
|
||||
.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!({
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
query::RequestArguments::Quota => {
|
||||
access_token.assert_is_member(req.account_id)?;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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_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)),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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.",
|
||||
)));
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -202,6 +202,7 @@ pub enum SecurityEvent {
|
|||
BruteForceBan,
|
||||
LoiterBan,
|
||||
IpBlocked,
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
#[event_type]
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue