Internal directory implementation + Management REST API

This commit is contained in:
mdecimus
2023-12-16 10:35:15 +01:00
parent f21bce722e
commit 232d4d691a
64 changed files with 1933 additions and 621 deletions

View File

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

View 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)
}
}

View File

@@ -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
})
}
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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