Internal directory implementation + Management REST API
This commit is contained in:
@@ -23,16 +23,15 @@
|
||||
|
||||
use mail_send::Credentials;
|
||||
use smtp_proto::{AUTH_CRAM_MD5, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2};
|
||||
use store::Store;
|
||||
|
||||
use crate::{Directory, DirectoryError, Principal, QueryBy, QueryType};
|
||||
use crate::{Directory, DirectoryError, Principal, QueryBy};
|
||||
|
||||
use super::{ImapDirectory, ImapError};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Directory for ImapDirectory {
|
||||
async fn query(&self, query: QueryBy<'_>) -> crate::Result<Option<Principal>> {
|
||||
if let QueryType::Credentials(credentials) = query.t {
|
||||
async fn query(&self, query: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
|
||||
if let QueryBy::Credentials(credentials) = query {
|
||||
let mut client = self.pool.get().await?;
|
||||
let mechanism = match credentials {
|
||||
Credentials::Plain { .. }
|
||||
@@ -78,7 +77,7 @@ impl Directory for ImapDirectory {
|
||||
}
|
||||
}
|
||||
|
||||
async fn email_to_ids(&self, _address: &str, _store: &Store) -> crate::Result<Vec<u32>> {
|
||||
async fn email_to_ids(&self, _address: &str) -> crate::Result<Vec<u32>> {
|
||||
Err(DirectoryError::unsupported("imap", "email_to_ids"))
|
||||
}
|
||||
|
||||
|
||||
147
crates/directory/src/backend/internal/lookup.rs
Normal file
147
crates/directory/src/backend/internal/lookup.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use mail_send::Credentials;
|
||||
use store::{
|
||||
write::{DirectoryValue, ValueClass},
|
||||
IterateParams, Store, ValueKey,
|
||||
};
|
||||
|
||||
use crate::{Directory, Principal, QueryBy};
|
||||
|
||||
use super::manage::ManageDirectory;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Directory for Store {
|
||||
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
|
||||
let (username, secret) = match by {
|
||||
QueryBy::Name(name) => (name, None),
|
||||
QueryBy::Id(account_id) => {
|
||||
return self
|
||||
.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryValue::Principal(account_id),
|
||||
)))
|
||||
.await
|
||||
.map_err(Into::into);
|
||||
}
|
||||
QueryBy::Credentials(credentials) => match credentials {
|
||||
Credentials::Plain { username, secret } => {
|
||||
(username.as_str(), secret.as_str().into())
|
||||
}
|
||||
Credentials::OAuthBearer { token } => (token.as_str(), token.as_str().into()),
|
||||
Credentials::XOauth2 { username, secret } => {
|
||||
(username.as_str(), secret.as_str().into())
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(account_id) = self.get_account_id(username).await? {
|
||||
match (
|
||||
self.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryValue::Principal(account_id),
|
||||
)))
|
||||
.await?,
|
||||
secret,
|
||||
) {
|
||||
(Some(principal), Some(secret)) if principal.verify_secret(secret).await => {
|
||||
Ok(Some(principal))
|
||||
}
|
||||
(Some(principal), None) => Ok(Some(principal)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn email_to_ids(&self, email: &str) -> crate::Result<Vec<u32>> {
|
||||
self.get_value::<Vec<u32>>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryValue::EmailToId(email.as_bytes().to_vec()),
|
||||
)))
|
||||
.await
|
||||
.map(|ids| ids.unwrap_or_default())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool> {
|
||||
self.get_value::<()>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryValue::Domain(domain.as_bytes().to_vec()),
|
||||
)))
|
||||
.await
|
||||
.map(|ids| ids.is_some())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
|
||||
self.get_value::<()>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryValue::EmailToId(address.as_bytes().to_vec()),
|
||||
)))
|
||||
.await
|
||||
.map(|ids| ids.is_some())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||
let mut results = Vec::new();
|
||||
let address = address.split('@').next().unwrap_or(address);
|
||||
if address.len() > 3 {
|
||||
self.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey::from(ValueClass::Directory(DirectoryValue::EmailToId(vec![0u8]))),
|
||||
ValueKey::from(ValueClass::Directory(DirectoryValue::EmailToId(
|
||||
vec![u8::MAX; 10],
|
||||
))),
|
||||
)
|
||||
.no_values(),
|
||||
|key, _| {
|
||||
let key =
|
||||
std::str::from_utf8(key.get(1..).unwrap_or_default()).unwrap_or_default();
|
||||
if key.split('@').next().unwrap_or(key).contains(address) {
|
||||
results.push(key.to_string());
|
||||
}
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
|
||||
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(
|
||||
DirectoryValue::Principal(account_id),
|
||||
)))
|
||||
.await?
|
||||
.and_then(|p| p.emails.into_iter().next())
|
||||
{
|
||||
results.push(email);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
@@ -24,27 +24,70 @@
|
||||
use jmap_proto::types::collection::Collection;
|
||||
use store::{
|
||||
write::{assert::HashedValue, BatchBuilder, DirectoryValue, ValueClass},
|
||||
Serialize, Store, ValueKey,
|
||||
IterateParams, Serialize, Store, ValueKey,
|
||||
};
|
||||
|
||||
use crate::{Principal, Type};
|
||||
use crate::{Directory, DirectoryError, ManagementError, Principal, QueryBy, Type};
|
||||
|
||||
use super::{PrincipalAction, PrincipalField, PrincipalUpdate, PrincipalValue};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ManageDirectory {
|
||||
async fn delete_account_by_name(&self, name: &str) -> store::Result<bool>;
|
||||
async fn delete_account_by_id(&self, id: u32) -> store::Result<bool>;
|
||||
async fn rename_account(&self, name: &str, new_name: String) -> store::Result<bool>;
|
||||
async fn get_account_id(&self, name: &str) -> store::Result<Option<u32>>;
|
||||
async fn get_account_id(&self, name: &str) -> crate::Result<Option<u32>>;
|
||||
async fn get_or_create_account_id(&self, name: &str) -> crate::Result<u32>;
|
||||
async fn get_account_name(&self, account_id: u32) -> crate::Result<Option<String>>;
|
||||
async fn create_account(&self, principal: Principal<String>) -> crate::Result<u32>;
|
||||
async fn update_account(
|
||||
&self,
|
||||
by: QueryBy<'_>,
|
||||
changes: Vec<PrincipalUpdate>,
|
||||
) -> crate::Result<()>;
|
||||
async fn delete_account(&self, by: QueryBy<'_>) -> crate::Result<()>;
|
||||
async fn create_domain(&self, domain: &str) -> crate::Result<()>;
|
||||
async fn delete_domain(&self, domain: &str) -> crate::Result<()>;
|
||||
async fn list_accounts(
|
||||
&self,
|
||||
start_from: Option<&str>,
|
||||
limit: usize,
|
||||
) -> crate::Result<Vec<String>>;
|
||||
async fn map_group_ids(&self, principal: Principal<u32>) -> crate::Result<Principal<String>>;
|
||||
async fn map_group_names(
|
||||
&self,
|
||||
principal: Principal<String>,
|
||||
create_if_missing: bool,
|
||||
) -> crate::Result<Principal<u32>>;
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ManageDirectory for Store {
|
||||
async fn get_account_id(&self, name: &str) -> store::Result<Option<u32>> {
|
||||
async fn get_account_name(&self, account_id: u32) -> crate::Result<Option<String>> {
|
||||
self.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryValue::Principal(account_id),
|
||||
)))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.map(|v| {
|
||||
if let Some(v) = v {
|
||||
Some(v.name)
|
||||
} else {
|
||||
tracing::debug!(
|
||||
context = "directory",
|
||||
event = "not_found",
|
||||
account = account_id,
|
||||
"Principal not found for account id"
|
||||
);
|
||||
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_account_id(&self, name: &str) -> crate::Result<Option<u32>> {
|
||||
self.get_value::<u32>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryValue::NameToId(name.as_bytes().to_vec()),
|
||||
)))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
// Used by all directories except internal
|
||||
@@ -102,25 +145,101 @@ impl ManageDirectory for Store {
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_account_by_name(&self, name: &str) -> store::Result<bool> {
|
||||
if let Some(account_id) = self.get_account_id(name).await? {
|
||||
self.delete_account_by_id(account_id).await
|
||||
} else {
|
||||
Ok(false)
|
||||
async fn create_account(&self, principal: Principal<String>) -> crate::Result<u32> {
|
||||
// Make sure the principal has a name
|
||||
if principal.name.is_empty() {
|
||||
return Err(DirectoryError::Management(ManagementError::MissingField(
|
||||
PrincipalField::Name,
|
||||
)));
|
||||
}
|
||||
|
||||
// Map group names
|
||||
let mut principal = self.map_group_names(principal, false).await?;
|
||||
|
||||
// Make sure new name is not taken
|
||||
principal.name = principal.name.to_lowercase();
|
||||
if self.get_account_id(&principal.name).await?.is_some() {
|
||||
return Err(DirectoryError::Management(ManagementError::NotUniqueField(
|
||||
PrincipalField::Name,
|
||||
)));
|
||||
}
|
||||
|
||||
// Make sure the e-mail is not taken and validate domain
|
||||
for email in principal.emails.iter_mut() {
|
||||
*email = email.to_lowercase();
|
||||
if self.rcpt(email).await? {
|
||||
return Err(DirectoryError::Management(ManagementError::NotUniqueField(
|
||||
PrincipalField::Emails,
|
||||
)));
|
||||
}
|
||||
if let Some(domain) = email.split('@').nth(1) {
|
||||
if !self.is_local_domain(domain).await? {
|
||||
return Err(DirectoryError::Management(ManagementError::NotFound(
|
||||
domain.to_string(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Assign accountId
|
||||
let account_id = self
|
||||
.assign_document_id(u32::MAX, Collection::Principal)
|
||||
.await?;
|
||||
|
||||
// Write principal
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.assert_value(
|
||||
ValueClass::Directory(DirectoryValue::NameToId(
|
||||
principal.name.clone().into_bytes(),
|
||||
)),
|
||||
(),
|
||||
)
|
||||
.set(
|
||||
ValueClass::Directory(DirectoryValue::Principal(account_id)),
|
||||
(&principal).serialize(),
|
||||
)
|
||||
.set(
|
||||
ValueClass::Directory(DirectoryValue::NameToId(principal.name.into_bytes())),
|
||||
account_id.serialize(),
|
||||
);
|
||||
|
||||
// Write email to id mapping
|
||||
let ids = if matches!(principal.typ, Type::List) {
|
||||
principal.member_of
|
||||
} else {
|
||||
vec![account_id]
|
||||
};
|
||||
|
||||
for email in principal.emails {
|
||||
batch.set(
|
||||
ValueClass::Directory(DirectoryValue::EmailToId(email.into_bytes())),
|
||||
(&ids).serialize(),
|
||||
);
|
||||
}
|
||||
|
||||
self.write(batch.build()).await?;
|
||||
|
||||
Ok(account_id)
|
||||
}
|
||||
|
||||
async fn delete_account_by_id(&self, account_id: u32) -> store::Result<bool> {
|
||||
let principal = if let Some(principal) = self
|
||||
.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
|
||||
async fn delete_account(&self, by: QueryBy<'_>) -> crate::Result<()> {
|
||||
let account_id = match by {
|
||||
QueryBy::Name(name) => self.get_account_id(name).await?.ok_or_else(|| {
|
||||
DirectoryError::Management(ManagementError::NotFound(name.to_string()))
|
||||
})?,
|
||||
QueryBy::Id(account_id) => account_id,
|
||||
QueryBy::Credentials(_) => unreachable!(),
|
||||
};
|
||||
|
||||
let principal = self
|
||||
.get_value::<Principal<u32>>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryValue::Principal(account_id),
|
||||
)))
|
||||
.await?
|
||||
{
|
||||
principal
|
||||
} else {
|
||||
return Ok(false);
|
||||
};
|
||||
.ok_or_else(|| {
|
||||
DirectoryError::Management(ManagementError::NotFound(account_id.to_string()))
|
||||
})?;
|
||||
|
||||
// Unlink all account's blobs
|
||||
self.blob_hash_unlink_account(account_id).await?;
|
||||
@@ -128,102 +247,390 @@ impl ManageDirectory for Store {
|
||||
// Revoke ACLs
|
||||
self.acl_revoke_all(account_id).await?;
|
||||
|
||||
// Delete account data
|
||||
self.purge_account(account_id).await?;
|
||||
|
||||
// Delete account
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.clear(DirectoryValue::NameToId(principal.name.as_bytes().to_vec()))
|
||||
.clear(DirectoryValue::NameToId(principal.name.into_bytes()))
|
||||
.clear(DirectoryValue::Principal(account_id))
|
||||
.clear(DirectoryValue::UsedQuota(account_id));
|
||||
|
||||
for email in principal.emails {
|
||||
batch.clear(DirectoryValue::EmailToId(email.as_bytes().to_vec()));
|
||||
batch.clear(DirectoryValue::EmailToId(email.into_bytes()));
|
||||
}
|
||||
|
||||
self.write(batch.build()).await?;
|
||||
|
||||
// Delete account data
|
||||
self.purge_account(account_id).await?;
|
||||
|
||||
Ok(true)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rename_account(&self, name: &str, new_name: String) -> store::Result<bool> {
|
||||
if let Some(account_id) = self.get_account_id(name).await? {
|
||||
if let Some(mut principal) = self
|
||||
.get_value::<HashedValue<Principal>>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryValue::Principal(account_id),
|
||||
)))
|
||||
.await?
|
||||
{
|
||||
if principal.inner.name != name {
|
||||
return Ok(false);
|
||||
async fn update_account(
|
||||
&self,
|
||||
by: QueryBy<'_>,
|
||||
changes: Vec<PrincipalUpdate>,
|
||||
) -> crate::Result<()> {
|
||||
let account_id = match by {
|
||||
QueryBy::Name(name) => self.get_account_id(name).await?.ok_or_else(|| {
|
||||
DirectoryError::Management(ManagementError::NotFound(name.to_string()))
|
||||
})?,
|
||||
QueryBy::Id(account_id) => account_id,
|
||||
QueryBy::Credentials(_) => unreachable!(),
|
||||
};
|
||||
|
||||
// Fetch principal
|
||||
let mut principal = self
|
||||
.get_value::<HashedValue<Principal<u32>>>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryValue::Principal(account_id),
|
||||
)))
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
DirectoryError::Management(ManagementError::NotFound(account_id.to_string()))
|
||||
})?;
|
||||
|
||||
// Apply changes
|
||||
let mut batch = BatchBuilder::new();
|
||||
let is_list = matches!(principal.inner.typ, Type::List);
|
||||
let mut has_list_changes = false;
|
||||
batch.assert_value(
|
||||
ValueClass::Directory(DirectoryValue::Principal(account_id)),
|
||||
&principal,
|
||||
);
|
||||
for change in changes {
|
||||
match (change.action, change.field, change.value) {
|
||||
(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 self.get_account_id(&new_name).await?.is_some() {
|
||||
return Err(DirectoryError::Management(
|
||||
ManagementError::NotUniqueField(PrincipalField::Name),
|
||||
));
|
||||
}
|
||||
|
||||
batch.clear(ValueClass::Directory(DirectoryValue::NameToId(
|
||||
principal.inner.name.as_bytes().to_vec(),
|
||||
)));
|
||||
|
||||
principal.inner.name = new_name.clone();
|
||||
|
||||
batch.set(
|
||||
ValueClass::Directory(DirectoryValue::NameToId(new_name.into_bytes())),
|
||||
account_id.serialize(),
|
||||
);
|
||||
}
|
||||
}
|
||||
principal.inner.name = new_name.clone();
|
||||
(PrincipalAction::Set, PrincipalField::Type, PrincipalValue::Type(new_type))
|
||||
if principal.inner.typ != Type::List && new_type != Type::List =>
|
||||
{
|
||||
principal.inner.typ = new_type;
|
||||
}
|
||||
(
|
||||
PrincipalAction::Set,
|
||||
PrincipalField::Secrets,
|
||||
PrincipalValue::StringList(secrets),
|
||||
) => {
|
||||
principal.inner.secrets = secrets;
|
||||
}
|
||||
(
|
||||
PrincipalAction::Set,
|
||||
PrincipalField::Description,
|
||||
PrincipalValue::String(description),
|
||||
) => {
|
||||
if !description.is_empty() {
|
||||
principal.inner.description = Some(description);
|
||||
} else {
|
||||
principal.inner.description = None;
|
||||
}
|
||||
}
|
||||
(PrincipalAction::Set, PrincipalField::Quota, PrincipalValue::Integer(quota)) => {
|
||||
principal.inner.quota = quota;
|
||||
}
|
||||
(
|
||||
PrincipalAction::Set,
|
||||
PrincipalField::Emails,
|
||||
PrincipalValue::StringList(emails),
|
||||
) => {
|
||||
// Validate unique emails
|
||||
let emails = emails
|
||||
.into_iter()
|
||||
.map(|v| v.to_lowercase())
|
||||
.collect::<Vec<_>>();
|
||||
for email in &emails {
|
||||
if !principal.inner.emails.contains(email) {
|
||||
if self.rcpt(email).await? {
|
||||
return Err(DirectoryError::Management(
|
||||
ManagementError::NotUniqueField(PrincipalField::Emails),
|
||||
));
|
||||
}
|
||||
if let Some(domain) = email.split('@').nth(1) {
|
||||
if !self.is_local_domain(domain).await? {
|
||||
return Err(DirectoryError::Management(
|
||||
ManagementError::NotFound(domain.to_string()),
|
||||
));
|
||||
}
|
||||
}
|
||||
if !is_list {
|
||||
batch.set(
|
||||
ValueClass::Directory(DirectoryValue::EmailToId(
|
||||
email.as_bytes().to_vec(),
|
||||
)),
|
||||
vec![account_id].serialize(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !is_list {
|
||||
for email in &principal.inner.emails {
|
||||
if !emails.contains(email) {
|
||||
batch.clear(ValueClass::Directory(DirectoryValue::EmailToId(
|
||||
email.as_bytes().to_vec(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch
|
||||
.assert_value(
|
||||
ValueClass::Directory(DirectoryValue::Principal(account_id)),
|
||||
&principal,
|
||||
)
|
||||
.set(
|
||||
ValueClass::Directory(DirectoryValue::Principal(account_id)),
|
||||
principal.inner.serialize(),
|
||||
)
|
||||
.clear(ValueClass::Directory(DirectoryValue::NameToId(
|
||||
name.as_bytes().to_vec(),
|
||||
)))
|
||||
.set(
|
||||
ValueClass::Directory(DirectoryValue::NameToId(new_name.into_bytes())),
|
||||
account_id.serialize(),
|
||||
);
|
||||
self.write(batch.build()).await?;
|
||||
|
||||
return Ok(true);
|
||||
principal.inner.emails = emails;
|
||||
}
|
||||
(
|
||||
PrincipalAction::Set,
|
||||
PrincipalField::MemberOf,
|
||||
PrincipalValue::StringList(members),
|
||||
) => {
|
||||
if is_list {
|
||||
has_list_changes = true;
|
||||
}
|
||||
principal.inner.member_of = Vec::with_capacity(members.len());
|
||||
for member in members {
|
||||
let account_id = self.get_account_id(&member).await?.ok_or_else(|| {
|
||||
DirectoryError::Management(ManagementError::NotFound(member))
|
||||
})?;
|
||||
principal.inner.member_of.push(account_id);
|
||||
}
|
||||
}
|
||||
(
|
||||
PrincipalAction::AddItem,
|
||||
PrincipalField::MemberOf,
|
||||
PrincipalValue::String(member),
|
||||
) => {
|
||||
let account_id = self.get_account_id(&member).await?.ok_or_else(|| {
|
||||
DirectoryError::Management(ManagementError::NotFound(member))
|
||||
})?;
|
||||
if !principal.inner.member_of.contains(&account_id) {
|
||||
principal.inner.member_of.push(account_id);
|
||||
if is_list {
|
||||
has_list_changes = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
(
|
||||
PrincipalAction::AddItem,
|
||||
PrincipalField::Emails,
|
||||
PrincipalValue::String(email),
|
||||
) => {
|
||||
let email = email.to_lowercase();
|
||||
if !principal.inner.emails.contains(&email) {
|
||||
if self.rcpt(&email).await? {
|
||||
return Err(DirectoryError::Management(
|
||||
ManagementError::NotUniqueField(PrincipalField::Emails),
|
||||
));
|
||||
}
|
||||
if let Some(domain) = email.split('@').nth(1) {
|
||||
if !self.is_local_domain(domain).await? {
|
||||
return Err(DirectoryError::Management(ManagementError::NotFound(
|
||||
domain.to_string(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
if !is_list {
|
||||
batch.set(
|
||||
ValueClass::Directory(DirectoryValue::EmailToId(
|
||||
email.as_bytes().to_vec(),
|
||||
)),
|
||||
vec![account_id].serialize(),
|
||||
);
|
||||
}
|
||||
principal.inner.emails.push(email);
|
||||
}
|
||||
}
|
||||
(
|
||||
PrincipalAction::RemoveItem,
|
||||
PrincipalField::MemberOf,
|
||||
PrincipalValue::String(member),
|
||||
) => {
|
||||
if let Some(account_id) = self.get_account_id(&member).await? {
|
||||
if let Some(pos) = principal
|
||||
.inner
|
||||
.member_of
|
||||
.iter()
|
||||
.position(|v| *v == account_id)
|
||||
{
|
||||
principal.inner.member_of.remove(pos);
|
||||
if is_list {
|
||||
has_list_changes = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(
|
||||
PrincipalAction::RemoveItem,
|
||||
PrincipalField::Emails,
|
||||
PrincipalValue::String(email),
|
||||
) => {
|
||||
let email = email.to_lowercase();
|
||||
if let Some(pos) = principal.inner.emails.iter().position(|v| *v == email) {
|
||||
if !is_list {
|
||||
batch.clear(ValueClass::Directory(DirectoryValue::EmailToId(
|
||||
email.as_bytes().to_vec(),
|
||||
)));
|
||||
}
|
||||
principal.inner.emails.remove(pos);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(DirectoryError::Unsupported);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
if has_list_changes {
|
||||
for email in &principal.inner.emails {
|
||||
batch.set(
|
||||
ValueClass::Directory(DirectoryValue::EmailToId(email.as_bytes().to_vec())),
|
||||
(&principal.inner.member_of).serialize(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
batch.set(
|
||||
ValueClass::Directory(DirectoryValue::Principal(account_id)),
|
||||
principal.inner.serialize(),
|
||||
);
|
||||
|
||||
self.write(batch.build()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn create_domain(&self, domain: &str) -> crate::Result<()> {
|
||||
if !domain.contains('.') {
|
||||
return Err(DirectoryError::Management(ManagementError::MissingField(
|
||||
PrincipalField::Name,
|
||||
)));
|
||||
}
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.set(
|
||||
ValueClass::Directory(DirectoryValue::Domain(domain.to_lowercase().into_bytes())),
|
||||
vec![],
|
||||
);
|
||||
self.write(batch.build()).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn delete_domain(&self, domain: &str) -> crate::Result<()> {
|
||||
if !domain.contains('.') {
|
||||
return Err(DirectoryError::Management(ManagementError::MissingField(
|
||||
PrincipalField::Name,
|
||||
)));
|
||||
}
|
||||
let mut batch = BatchBuilder::new();
|
||||
batch.clear(ValueClass::Directory(DirectoryValue::Domain(
|
||||
domain.to_lowercase().into_bytes(),
|
||||
)));
|
||||
self.write(batch.build()).await.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn map_group_ids(&self, principal: Principal<u32>) -> crate::Result<Principal<String>> {
|
||||
let mut mapped = Principal {
|
||||
id: principal.id,
|
||||
typ: principal.typ,
|
||||
quota: principal.quota,
|
||||
name: principal.name,
|
||||
secrets: principal.secrets,
|
||||
emails: principal.emails,
|
||||
member_of: Vec::with_capacity(principal.member_of.len()),
|
||||
description: principal.description,
|
||||
};
|
||||
|
||||
for account_id in principal.member_of {
|
||||
if let Some(name) = self.get_account_name(account_id).await? {
|
||||
mapped.member_of.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(mapped)
|
||||
}
|
||||
|
||||
async fn map_group_names(
|
||||
&self,
|
||||
principal: Principal<String>,
|
||||
create_if_missing: bool,
|
||||
) -> crate::Result<Principal<u32>> {
|
||||
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 member in principal.member_of {
|
||||
let account_id = if create_if_missing {
|
||||
self.get_or_create_account_id(&member).await?
|
||||
} else {
|
||||
self.get_account_id(&member)
|
||||
.await?
|
||||
.ok_or_else(|| DirectoryError::Management(ManagementError::NotFound(member)))?
|
||||
};
|
||||
mapped.member_of.push(account_id);
|
||||
}
|
||||
|
||||
Ok(mapped)
|
||||
}
|
||||
|
||||
async fn list_accounts(
|
||||
&self,
|
||||
start_from: Option<&str>,
|
||||
limit: usize,
|
||||
) -> crate::Result<Vec<String>> {
|
||||
let from_key = ValueKey::from(ValueClass::Directory(DirectoryValue::NameToId(
|
||||
start_from.unwrap_or("").as_bytes().to_vec(),
|
||||
)));
|
||||
let to_key = ValueKey::from(ValueClass::Directory(DirectoryValue::NameToId(vec![
|
||||
u8::MAX;
|
||||
10
|
||||
])));
|
||||
|
||||
let mut results = Vec::with_capacity(limit);
|
||||
self.iterate(
|
||||
IterateParams::new(from_key, to_key).no_values().ascending(),
|
||||
|key, _| {
|
||||
results
|
||||
.push(String::from_utf8_lossy(key.get(1..).unwrap_or_default()).into_owned());
|
||||
Ok(limit == 0 || results.len() < limit)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
pub async fn try_get_account_id(store: &Store, name: &str) -> crate::Result<Option<u32>> {
|
||||
store
|
||||
.get_value::<u32>(NamedKey::Name(name))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(event = "error",
|
||||
context = "store",
|
||||
account_name = name,
|
||||
error = ?err,
|
||||
"Failed to retrieve account id");
|
||||
MethodError::ServerPartialFail
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub async fn map_member_of(store: &Store, names: Vec<String>) -> crate::Result<Vec<u32>> {
|
||||
let mut ids = Vec::with_capacity(names.len());
|
||||
for name in names {
|
||||
ids.push(self.get_account_id(&name).await?);
|
||||
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,
|
||||
}
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
pub async fn get_account_name(store: &Store, account_id: u32) -> crate::Result<Option<String>> {
|
||||
store
|
||||
.get_value::<String>(NamedKey::Id::<&[u8]>(account_id))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(event = "error",
|
||||
context = "store",
|
||||
account_id = account_id,
|
||||
error = ?err,
|
||||
"Failed to retrieve account name");
|
||||
MethodError::ServerPartialFail
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -21,101 +21,23 @@
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
pub mod lookup;
|
||||
pub mod manage;
|
||||
|
||||
use std::slice::Iter;
|
||||
|
||||
use mail_send::Credentials;
|
||||
use store::{
|
||||
write::{key::KeySerializer, DirectoryValue, ValueClass},
|
||||
Deserialize, Serialize, Store, ValueKey, U32_LEN,
|
||||
};
|
||||
use store::{write::key::KeySerializer, Deserialize, Serialize, U32_LEN};
|
||||
use utils::codec::leb128::Leb128Iterator;
|
||||
|
||||
use crate::{Principal, QueryBy, QueryType, Type};
|
||||
use crate::{Principal, Type};
|
||||
|
||||
use self::manage::ManageDirectory;
|
||||
|
||||
pub struct InternalDirectory {
|
||||
pub store: Store,
|
||||
}
|
||||
|
||||
impl<'x> QueryBy<'x> {
|
||||
pub fn name(name: &'x str) -> Self {
|
||||
Self {
|
||||
t: QueryType::Name(name),
|
||||
store: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(id: u32) -> Self {
|
||||
Self {
|
||||
t: QueryType::Id(id),
|
||||
store: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn credentials(credentials: &'x Credentials<String>) -> Self {
|
||||
Self {
|
||||
t: QueryType::Credentials(credentials),
|
||||
store: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_store(mut self, store: &'x Store) -> Self {
|
||||
self.store = Some(store);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn has_store(&self) -> bool {
|
||||
self.store.is_some()
|
||||
}
|
||||
|
||||
pub async fn account_name(&self, account_id: u32) -> crate::Result<Option<String>> {
|
||||
self.store
|
||||
.unwrap()
|
||||
.get_value::<Principal>(ValueKey::from(ValueClass::Directory(
|
||||
DirectoryValue::Principal(account_id),
|
||||
)))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
.map(|v| {
|
||||
if let Some(v) = v {
|
||||
Some(v.name)
|
||||
} else {
|
||||
tracing::debug!(
|
||||
context = "directory",
|
||||
event = "not_found",
|
||||
account = account_id,
|
||||
"Principal not found for account id"
|
||||
);
|
||||
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn account_id(&self, name: &str) -> crate::Result<u32> {
|
||||
self.store
|
||||
.unwrap()
|
||||
.get_or_create_account_id(name)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub async fn account_ids<I: AsRef<str>>(
|
||||
&self,
|
||||
items: impl Iterator<Item = I>,
|
||||
) -> crate::Result<Vec<u32>> {
|
||||
let mut ids = Vec::new();
|
||||
for item in items {
|
||||
ids.push(self.account_id(item.as_ref()).await?);
|
||||
}
|
||||
Ok(ids)
|
||||
impl Serialize for Principal<u32> {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
(&self).serialize()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Principal {
|
||||
impl Serialize for &Principal<u32> {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
let mut serializer = KeySerializer::new(
|
||||
U32_LEN * 3
|
||||
@@ -151,14 +73,14 @@ impl Serialize for Principal {
|
||||
}
|
||||
}
|
||||
|
||||
impl Deserialize for Principal {
|
||||
impl Deserialize for Principal<u32> {
|
||||
fn deserialize(bytes: &[u8]) -> store::Result<Self> {
|
||||
deserialize(bytes)
|
||||
.ok_or_else(|| store::Error::InternalError("Failed to deserialize principal".into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize(bytes: &[u8]) -> Option<Principal> {
|
||||
fn deserialize(bytes: &[u8]) -> Option<Principal<u32>> {
|
||||
let mut bytes = bytes.iter();
|
||||
if bytes.next()? != &1 {
|
||||
return None;
|
||||
@@ -183,6 +105,76 @@ fn deserialize(bytes: &[u8]) -> Option<Principal> {
|
||||
.into()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum PrincipalField {
|
||||
#[serde(rename = "name")]
|
||||
Name,
|
||||
#[serde(rename = "type")]
|
||||
Type,
|
||||
#[serde(rename = "quota")]
|
||||
Quota,
|
||||
#[serde(rename = "description")]
|
||||
Description,
|
||||
#[serde(rename = "secrets")]
|
||||
Secrets,
|
||||
#[serde(rename = "emails")]
|
||||
Emails,
|
||||
#[serde(rename = "memberOf")]
|
||||
MemberOf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PrincipalUpdate {
|
||||
action: PrincipalAction,
|
||||
field: PrincipalField,
|
||||
value: PrincipalValue,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum PrincipalAction {
|
||||
#[serde(rename = "set")]
|
||||
Set,
|
||||
#[serde(rename = "addItem")]
|
||||
AddItem,
|
||||
#[serde(rename = "removeItem")]
|
||||
RemoveItem,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum PrincipalValue {
|
||||
String(String),
|
||||
StringList(Vec<String>),
|
||||
Integer(u32),
|
||||
Type(Type),
|
||||
}
|
||||
|
||||
impl PrincipalUpdate {
|
||||
pub fn set(field: PrincipalField, value: PrincipalValue) -> PrincipalUpdate {
|
||||
PrincipalUpdate {
|
||||
action: PrincipalAction::Set,
|
||||
field,
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_item(field: PrincipalField, value: PrincipalValue) -> PrincipalUpdate {
|
||||
PrincipalUpdate {
|
||||
action: PrincipalAction::AddItem,
|
||||
field,
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_item(field: PrincipalField, value: PrincipalValue) -> PrincipalUpdate {
|
||||
PrincipalUpdate {
|
||||
action: PrincipalAction::RemoveItem,
|
||||
field,
|
||||
value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_string(bytes: &mut Iter<'_, u8>) -> Option<String> {
|
||||
let len = bytes.next_leb128()?;
|
||||
let mut string = Vec::with_capacity(len);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ldap3::LdapConnSettings;
|
||||
use store::Store;
|
||||
use utils::config::{utils::AsKey, Config};
|
||||
|
||||
use crate::{cache::CachedDirectory, config::build_pool, Directory, DirectoryOptions};
|
||||
@@ -34,6 +35,7 @@ impl LdapDirectory {
|
||||
pub fn from_config(
|
||||
config: &Config,
|
||||
prefix: impl AsKey,
|
||||
id_store: Option<Store>,
|
||||
) -> utils::config::Result<Arc<dyn Directory>> {
|
||||
let prefix = prefix.as_key();
|
||||
let bind_dn = if let Some(dn) = config.value((&prefix, "bind.dn")) {
|
||||
@@ -127,6 +129,7 @@ impl LdapDirectory {
|
||||
pool: build_pool(config, &prefix, manager)?,
|
||||
opt: DirectoryOptions::from_config(config, prefix.as_str())?,
|
||||
auth_bind,
|
||||
id_store,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,21 +26,20 @@ use mail_send::Credentials;
|
||||
use store::Store;
|
||||
|
||||
use crate::{
|
||||
backend::internal::manage::ManageDirectory, Directory, DirectoryError, Principal, QueryBy,
|
||||
QueryType, Type,
|
||||
backend::internal::manage::ManageDirectory, Directory, DirectoryError, Principal, QueryBy, Type,
|
||||
};
|
||||
|
||||
use super::{LdapDirectory, LdapMappings};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Directory for LdapDirectory {
|
||||
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal>> {
|
||||
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
|
||||
let mut conn = self.pool.get().await?;
|
||||
let mut account_id = None;
|
||||
let account_name;
|
||||
|
||||
let principal = match by.t {
|
||||
QueryType::Name(username) => {
|
||||
let principal = match by {
|
||||
QueryBy::Name(username) => {
|
||||
account_name = username.to_string();
|
||||
|
||||
if let Some(principal) = self
|
||||
@@ -52,8 +51,8 @@ impl Directory for LdapDirectory {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
QueryType::Id(uid) => {
|
||||
if let Some(username) = by.account_name(uid).await? {
|
||||
QueryBy::Id(uid) => {
|
||||
if let Some(username) = self.unwrap_id_store().get_account_name(uid).await? {
|
||||
account_name = username;
|
||||
} else {
|
||||
return Ok(None);
|
||||
@@ -69,7 +68,7 @@ impl Directory for LdapDirectory {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
QueryType::Credentials(credentials) => {
|
||||
QueryBy::Credentials(credentials) => {
|
||||
let (username, secret) = match credentials {
|
||||
Credentials::Plain { username, secret } => (username, secret),
|
||||
Credentials::OAuthBearer { token } => (token, token),
|
||||
@@ -105,7 +104,7 @@ impl Directory for LdapDirectory {
|
||||
.find_principal(&mut conn, &self.mappings.filter_name.build(username))
|
||||
.await?
|
||||
{
|
||||
if principal.principal.verify_secret(secret).await {
|
||||
if principal.verify_secret(secret).await {
|
||||
principal
|
||||
} else {
|
||||
tracing::debug!(
|
||||
@@ -122,25 +121,26 @@ impl Directory for LdapDirectory {
|
||||
}
|
||||
}
|
||||
};
|
||||
let groups = principal.groups;
|
||||
let mut principal = principal.principal;
|
||||
let mut principal = principal;
|
||||
|
||||
// Obtain account ID if not available
|
||||
if let Some(account_id) = account_id {
|
||||
principal.id = account_id;
|
||||
} else if by.has_store() {
|
||||
principal.id = by.account_id(&account_name).await?;
|
||||
} else if self.has_id_store() {
|
||||
principal.id = self
|
||||
.unwrap_id_store()
|
||||
.get_or_create_account_id(&account_name)
|
||||
.await?;
|
||||
}
|
||||
principal.name = account_name;
|
||||
|
||||
// Obtain groups
|
||||
if by.has_store() && !groups.is_empty() {
|
||||
principal.member_of = Vec::with_capacity(groups.len());
|
||||
for group in groups {
|
||||
if group.contains('=') {
|
||||
if !principal.member_of.is_empty() && self.has_id_store() {
|
||||
for member_of in principal.member_of.iter_mut() {
|
||||
if member_of.contains('=') {
|
||||
let (rs, _res) = conn
|
||||
.search(
|
||||
&group,
|
||||
member_of,
|
||||
Scope::Base,
|
||||
"objectClass=*",
|
||||
&self.mappings.attr_name,
|
||||
@@ -150,25 +150,29 @@ impl Directory for LdapDirectory {
|
||||
for entry in rs {
|
||||
'outer: for (attr, value) in SearchEntry::construct(entry).attrs {
|
||||
if self.mappings.attr_name.contains(&attr) {
|
||||
if let Some(group) = value.first() {
|
||||
if let Some(group) = value.into_iter().next() {
|
||||
if !group.is_empty() {
|
||||
principal.member_of.push(by.account_id(group).await?);
|
||||
*member_of = group;
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
principal.member_of.push(by.account_id(&group).await?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(principal))
|
||||
// Map ids
|
||||
self.unwrap_id_store()
|
||||
.map_group_names(principal, true)
|
||||
.await
|
||||
.map(Some)
|
||||
} else {
|
||||
Ok(Some(principal.into()))
|
||||
}
|
||||
}
|
||||
|
||||
async fn email_to_ids(&self, address: &str, store: &Store) -> crate::Result<Vec<u32>> {
|
||||
async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
|
||||
let mut rs = self
|
||||
.pool
|
||||
.get()
|
||||
@@ -212,7 +216,11 @@ impl Directory for LdapDirectory {
|
||||
'outer: for attr in &self.mappings.attr_name {
|
||||
if let Some(name) = entry.attrs.get(attr).and_then(|v| v.first()) {
|
||||
if !name.is_empty() {
|
||||
ids.push(store.get_or_create_account_id(name).await?);
|
||||
ids.push(
|
||||
self.unwrap_id_store()
|
||||
.get_or_create_account_id(name)
|
||||
.await?,
|
||||
);
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
@@ -355,7 +363,7 @@ impl LdapDirectory {
|
||||
&self,
|
||||
conn: &mut Ldap,
|
||||
filter: &str,
|
||||
) -> crate::Result<Option<PrincipalWithGroups>> {
|
||||
) -> crate::Result<Option<Principal<String>>> {
|
||||
conn.search(
|
||||
&self.mappings.base_dn,
|
||||
Scope::Subtree,
|
||||
@@ -372,16 +380,18 @@ impl LdapDirectory {
|
||||
})
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
struct PrincipalWithGroups {
|
||||
principal: Principal,
|
||||
groups: Vec<String>,
|
||||
pub fn has_id_store(&self) -> bool {
|
||||
self.id_store.is_some()
|
||||
}
|
||||
|
||||
pub fn unwrap_id_store(&self) -> &Store {
|
||||
self.id_store.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl LdapMappings {
|
||||
fn entry_to_principal(&self, entry: SearchEntry) -> PrincipalWithGroups {
|
||||
let mut groups = Vec::new();
|
||||
fn entry_to_principal(&self, entry: SearchEntry) -> Principal<String> {
|
||||
let mut principal = Principal::default();
|
||||
|
||||
for (attr, value) in entry.attrs {
|
||||
@@ -404,7 +414,7 @@ impl LdapMappings {
|
||||
principal.description = value.into_iter().next();
|
||||
}
|
||||
} else if self.attr_groups.contains(&attr) {
|
||||
groups.extend(value);
|
||||
principal.member_of.extend(value);
|
||||
} else if self.attr_quota.contains(&attr) {
|
||||
if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse() {
|
||||
principal.quota = quota;
|
||||
@@ -426,6 +436,6 @@ impl LdapMappings {
|
||||
}
|
||||
}
|
||||
|
||||
PrincipalWithGroups { principal, groups }
|
||||
principal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
use deadpool::managed::Pool;
|
||||
use ldap3::{ldap_escape, LdapConnSettings};
|
||||
use store::Store;
|
||||
|
||||
use crate::DirectoryOptions;
|
||||
|
||||
@@ -35,6 +36,7 @@ pub struct LdapDirectory {
|
||||
mappings: LdapMappings,
|
||||
opt: DirectoryOptions,
|
||||
auth_bind: Option<LdapFilter>,
|
||||
id_store: Option<Store>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
@@ -22,31 +22,30 @@
|
||||
*/
|
||||
|
||||
use mail_send::Credentials;
|
||||
use store::Store;
|
||||
|
||||
use crate::{Directory, Principal, QueryBy, QueryType};
|
||||
use crate::{Directory, Principal, QueryBy};
|
||||
|
||||
use super::{EmailType, MemoryDirectory};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Directory for MemoryDirectory {
|
||||
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal>> {
|
||||
match by.t {
|
||||
QueryType::Name(name) => {
|
||||
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
|
||||
match by {
|
||||
QueryBy::Name(name) => {
|
||||
for principal in &self.principals {
|
||||
if principal.name == name {
|
||||
return Ok(Some(principal.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
QueryType::Id(uid) => {
|
||||
QueryBy::Id(uid) => {
|
||||
for principal in &self.principals {
|
||||
if principal.id == uid {
|
||||
return Ok(Some(principal.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
QueryType::Credentials(credentials) => {
|
||||
QueryBy::Credentials(credentials) => {
|
||||
let (username, secret) = match credentials {
|
||||
Credentials::Plain { username, secret } => (username, secret),
|
||||
Credentials::OAuthBearer { token } => (token, token),
|
||||
@@ -67,7 +66,7 @@ impl Directory for MemoryDirectory {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn email_to_ids(&self, address: &str, _: &Store) -> crate::Result<Vec<u32>> {
|
||||
async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
|
||||
Ok(self
|
||||
.emails_to_ids
|
||||
.get(self.opt.subaddressing.to_subaddress(address).as_ref())
|
||||
|
||||
@@ -30,7 +30,7 @@ pub mod lookup;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct MemoryDirectory {
|
||||
principals: Vec<Principal>,
|
||||
principals: Vec<Principal<u32>>,
|
||||
emails_to_ids: AHashMap<String, Vec<EmailType>>,
|
||||
names_to_ids: AHashMap<String, u32>,
|
||||
domains: AHashSet<String>,
|
||||
|
||||
@@ -23,23 +23,22 @@
|
||||
|
||||
use mail_send::{smtp::AssertReply, Credentials};
|
||||
use smtp_proto::Severity;
|
||||
use store::Store;
|
||||
|
||||
use crate::{Directory, DirectoryError, Principal, QueryBy, QueryType};
|
||||
use crate::{Directory, DirectoryError, Principal, QueryBy};
|
||||
|
||||
use super::{SmtpClient, SmtpDirectory};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Directory for SmtpDirectory {
|
||||
async fn query(&self, query: QueryBy<'_>) -> crate::Result<Option<Principal>> {
|
||||
if let QueryType::Credentials(credentials) = query.t {
|
||||
async fn query(&self, query: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
|
||||
if let QueryBy::Credentials(credentials) = query {
|
||||
self.pool.get().await?.authenticate(credentials).await
|
||||
} else {
|
||||
Err(DirectoryError::unsupported("smtp", "query"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn email_to_ids(&self, _address: &str, _store: &Store) -> crate::Result<Vec<u32>> {
|
||||
async fn email_to_ids(&self, _address: &str) -> crate::Result<Vec<u32>> {
|
||||
Err(DirectoryError::unsupported("smtp", "email_to_ids"))
|
||||
}
|
||||
|
||||
@@ -96,7 +95,7 @@ impl SmtpClient {
|
||||
async fn authenticate(
|
||||
&mut self,
|
||||
credentials: &Credentials<String>,
|
||||
) -> crate::Result<Option<Principal>> {
|
||||
) -> crate::Result<Option<Principal<u32>>> {
|
||||
match self
|
||||
.client
|
||||
.authenticate(credentials, &self.capabilities)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use store::Stores;
|
||||
use store::{Store, Stores};
|
||||
use utils::config::{utils::AsKey, Config};
|
||||
|
||||
use crate::{cache::CachedDirectory, Directory, DirectoryOptions};
|
||||
@@ -35,6 +35,7 @@ impl SqlDirectory {
|
||||
config: &Config,
|
||||
prefix: impl AsKey,
|
||||
stores: &Stores,
|
||||
id_store: Option<Store>,
|
||||
) -> utils::config::Result<Arc<dyn Directory>> {
|
||||
let prefix = prefix.as_key();
|
||||
let store_id = config.value_require((&prefix, "store"))?;
|
||||
@@ -87,6 +88,7 @@ impl SqlDirectory {
|
||||
store,
|
||||
mappings,
|
||||
opt: DirectoryOptions::from_config(config, prefix.as_str())?,
|
||||
id_store,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,29 +24,27 @@
|
||||
use mail_send::Credentials;
|
||||
use store::{NamedRows, Rows, Store, Value};
|
||||
|
||||
use crate::{
|
||||
backend::internal::manage::ManageDirectory, Directory, Principal, QueryBy, QueryType, Type,
|
||||
};
|
||||
use crate::{backend::internal::manage::ManageDirectory, Directory, Principal, QueryBy, Type};
|
||||
|
||||
use super::{SqlDirectory, SqlMappings};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Directory for SqlDirectory {
|
||||
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal>> {
|
||||
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
|
||||
let mut account_id = None;
|
||||
let account_name;
|
||||
let mut secret = None;
|
||||
|
||||
let result = match by.t {
|
||||
QueryType::Name(username) => {
|
||||
let result = match by {
|
||||
QueryBy::Name(username) => {
|
||||
account_name = username.to_string();
|
||||
|
||||
self.store
|
||||
.query::<NamedRows>(&self.mappings.query_name, vec![username.into()])
|
||||
.await?
|
||||
}
|
||||
QueryType::Id(uid) => {
|
||||
if let Some(username) = by.account_name(uid).await? {
|
||||
QueryBy::Id(uid) => {
|
||||
if let Some(username) = self.unwrap_id_store().get_account_name(uid).await? {
|
||||
account_name = username;
|
||||
} else {
|
||||
return Ok(None);
|
||||
@@ -60,7 +58,7 @@ impl Directory for SqlDirectory {
|
||||
)
|
||||
.await?
|
||||
}
|
||||
QueryType::Credentials(credentials) => {
|
||||
QueryBy::Credentials(credentials) => {
|
||||
let (username, secret_) = match credentials {
|
||||
Credentials::Plain { username, secret } => (username, secret),
|
||||
Credentials::OAuthBearer { token } => (token, token),
|
||||
@@ -99,12 +97,15 @@ impl Directory for SqlDirectory {
|
||||
// Obtain account ID if not available
|
||||
if let Some(account_id) = account_id {
|
||||
principal.id = account_id;
|
||||
} else if by.has_store() {
|
||||
principal.id = by.account_id(&account_name).await?;
|
||||
} else if self.has_id_store() {
|
||||
principal.id = self
|
||||
.unwrap_id_store()
|
||||
.get_or_create_account_id(&account_name)
|
||||
.await?;
|
||||
}
|
||||
principal.name = account_name;
|
||||
|
||||
if by.has_store() {
|
||||
if self.has_id_store() {
|
||||
// Obtain members
|
||||
if !self.mappings.query_members.is_empty() {
|
||||
for row in self
|
||||
@@ -117,7 +118,11 @@ impl Directory for SqlDirectory {
|
||||
.rows
|
||||
{
|
||||
if let Some(Value::Text(account_id)) = row.values.first() {
|
||||
principal.member_of.push(by.account_id(account_id).await?);
|
||||
principal.member_of.push(
|
||||
self.unwrap_id_store()
|
||||
.get_or_create_account_id(account_id)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,7 +143,7 @@ impl Directory for SqlDirectory {
|
||||
Ok(Some(principal))
|
||||
}
|
||||
|
||||
async fn email_to_ids(&self, address: &str, store: &Store) -> crate::Result<Vec<u32>> {
|
||||
async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
|
||||
let mut names = self
|
||||
.store
|
||||
.query::<Rows>(
|
||||
@@ -167,7 +172,11 @@ impl Directory for SqlDirectory {
|
||||
|
||||
for row in names.rows {
|
||||
if let Some(Value::Text(name)) = row.values.first() {
|
||||
ids.push(store.get_or_create_account_id(name).await?);
|
||||
ids.push(
|
||||
self.unwrap_id_store()
|
||||
.get_or_create_account_id(name)
|
||||
.await?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,8 +251,18 @@ impl Directory for SqlDirectory {
|
||||
}
|
||||
}
|
||||
|
||||
impl SqlDirectory {
|
||||
pub fn has_id_store(&self) -> bool {
|
||||
self.id_store.is_some()
|
||||
}
|
||||
|
||||
pub fn unwrap_id_store(&self) -> &Store {
|
||||
self.id_store.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl SqlMappings {
|
||||
pub fn row_to_principal(&self, rows: NamedRows) -> crate::Result<Principal> {
|
||||
pub fn row_to_principal(&self, rows: NamedRows) -> crate::Result<Principal<u32>> {
|
||||
let mut principal = Principal::default();
|
||||
|
||||
if let Some(row) = rows.rows.into_iter().next() {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use store::LookupStore;
|
||||
use store::{LookupStore, Store};
|
||||
|
||||
use crate::DirectoryOptions;
|
||||
|
||||
@@ -32,6 +32,7 @@ pub struct SqlDirectory {
|
||||
store: LookupStore,
|
||||
mappings: SqlMappings,
|
||||
opt: DirectoryOptions,
|
||||
id_store: Option<Store>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
|
||||
8
crates/directory/src/cache/lookup.rs
vendored
8
crates/directory/src/cache/lookup.rs
vendored
@@ -21,20 +21,18 @@
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use store::Store;
|
||||
|
||||
use crate::{Directory, Principal, QueryBy};
|
||||
|
||||
use super::CachedDirectory;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<T: Directory> Directory for CachedDirectory<T> {
|
||||
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal>> {
|
||||
async fn query(&self, by: QueryBy<'_>) -> crate::Result<Option<Principal<u32>>> {
|
||||
self.inner.query(by).await
|
||||
}
|
||||
|
||||
async fn email_to_ids(&self, address: &str, store: &Store) -> crate::Result<Vec<u32>> {
|
||||
self.inner.email_to_ids(address, store).await
|
||||
async fn email_to_ids(&self, address: &str) -> crate::Result<Vec<u32>> {
|
||||
self.inner.email_to_ids(address).await
|
||||
}
|
||||
|
||||
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
|
||||
|
||||
@@ -44,22 +44,31 @@ use crate::{
|
||||
};
|
||||
|
||||
pub trait ConfigDirectory {
|
||||
fn parse_directory(&self, stores: &Stores) -> utils::config::Result<Directories>;
|
||||
fn parse_directory(
|
||||
&self,
|
||||
stores: &Stores,
|
||||
id_store: Option<&str>,
|
||||
) -> utils::config::Result<Directories>;
|
||||
}
|
||||
|
||||
impl ConfigDirectory for Config {
|
||||
fn parse_directory(&self, stores: &Stores) -> utils::config::Result<Directories> {
|
||||
fn parse_directory(
|
||||
&self,
|
||||
stores: &Stores,
|
||||
id_store: Option<&str>,
|
||||
) -> utils::config::Result<Directories> {
|
||||
let mut config = Directories {
|
||||
directories: AHashMap::new(),
|
||||
};
|
||||
let id_store = id_store.and_then(|id| stores.stores.get(id).cloned());
|
||||
|
||||
for id in self.sub_keys("directory") {
|
||||
// Parse directory
|
||||
let protocol = self.value_require(("directory", id, "type"))?;
|
||||
let prefix = ("directory", id);
|
||||
let directory = match protocol {
|
||||
"ldap" => LdapDirectory::from_config(self, prefix)?,
|
||||
"sql" => SqlDirectory::from_config(self, prefix, stores)?,
|
||||
"ldap" => LdapDirectory::from_config(self, prefix, id_store.clone())?,
|
||||
"sql" => SqlDirectory::from_config(self, prefix, stores, id_store.clone())?,
|
||||
"imap" => ImapDirectory::from_config(self, prefix)?,
|
||||
"smtp" => SmtpDirectory::from_config(self, prefix, false)?,
|
||||
"lmtp" => SmtpDirectory::from_config(self, prefix, true)?,
|
||||
|
||||
@@ -24,11 +24,10 @@
|
||||
use std::{borrow::Cow, fmt::Debug, sync::Arc};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use backend::imap::ImapError;
|
||||
use backend::{imap::ImapError, internal::PrincipalField};
|
||||
use deadpool::managed::PoolError;
|
||||
use ldap3::LdapError;
|
||||
use mail_send::Credentials;
|
||||
use store::Store;
|
||||
use utils::config::DynValue;
|
||||
|
||||
pub mod backend;
|
||||
@@ -36,27 +35,42 @@ pub mod cache;
|
||||
pub mod config;
|
||||
pub mod secret;
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Principal {
|
||||
#[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: u32,
|
||||
pub name: String,
|
||||
#[serde(default, skip_serializing)]
|
||||
pub secrets: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub emails: Vec<String>,
|
||||
pub member_of: Vec<u32>,
|
||||
#[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, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum Type {
|
||||
#[serde(rename = "individual")]
|
||||
Individual = 0,
|
||||
#[serde(rename = "group")]
|
||||
Group = 1,
|
||||
#[serde(rename = "resource")]
|
||||
Resource = 2,
|
||||
#[serde(rename = "location")]
|
||||
Location = 3,
|
||||
#[serde(rename = "superuser")]
|
||||
Superuser = 4,
|
||||
#[serde(rename = "list")]
|
||||
List = 5,
|
||||
#[default]
|
||||
#[serde(rename = "other")]
|
||||
Other = 6,
|
||||
}
|
||||
|
||||
@@ -67,14 +81,22 @@ pub enum DirectoryError {
|
||||
Imap(ImapError),
|
||||
Smtp(mail_send::Error),
|
||||
Pool(String),
|
||||
Management(ManagementError),
|
||||
TimedOut,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum ManagementError {
|
||||
MissingField(PrincipalField),
|
||||
NotUniqueField(PrincipalField),
|
||||
NotFound(String),
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait Directory: Sync + Send {
|
||||
async fn query(&self, by: QueryBy<'_>) -> Result<Option<Principal>>;
|
||||
async fn email_to_ids(&self, email: &str, store: &Store) -> Result<Vec<u32>>;
|
||||
async fn query(&self, by: QueryBy<'_>) -> Result<Option<Principal<u32>>>;
|
||||
async fn email_to_ids(&self, email: &str) -> Result<Vec<u32>>;
|
||||
|
||||
async fn is_local_domain(&self, domain: &str) -> crate::Result<bool>;
|
||||
async fn rcpt(&self, address: &str) -> crate::Result<bool>;
|
||||
@@ -82,18 +104,13 @@ pub trait Directory: Sync + Send {
|
||||
async fn expn(&self, address: &str) -> Result<Vec<String>>;
|
||||
}
|
||||
|
||||
pub enum QueryType<'x> {
|
||||
pub enum QueryBy<'x> {
|
||||
Name(&'x str),
|
||||
Id(u32),
|
||||
Credentials(&'x Credentials<String>),
|
||||
}
|
||||
|
||||
pub struct QueryBy<'x> {
|
||||
pub t: QueryType<'x>,
|
||||
pub store: Option<&'x Store>,
|
||||
}
|
||||
|
||||
impl Principal {
|
||||
impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
@@ -310,3 +327,14 @@ impl AddressMapping {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for DirectoryError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::Store(l0), Self::Store(r0)) => l0 == r0,
|
||||
(Self::Pool(l0), Self::Pool(r0)) => l0 == r0,
|
||||
(Self::Management(l0), Self::Management(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ use tokio::sync::oneshot;
|
||||
|
||||
use crate::Principal;
|
||||
|
||||
impl Principal {
|
||||
impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
|
||||
pub async fn verify_secret(&self, secret: &str) -> bool {
|
||||
for hashed_secret in &self.secrets {
|
||||
if verify_secret_hash(hashed_secret, secret).await {
|
||||
|
||||
Reference in New Issue
Block a user