Directory implementation - part 3

This commit is contained in:
Mauro D
2023-06-01 17:08:48 +00:00
parent beffa408e6
commit 93e925a635
18 changed files with 1212 additions and 139 deletions

View File

@@ -7,7 +7,9 @@ resolver = "2"
[dependencies]
utils = { path = "../utils" }
smtp-proto = { git = "https://github.com/stalwartlabs/smtp-proto" }
mail-parser = { git = "https://github.com/stalwartlabs/mail-parser", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
mail-send = { git = "https://github.com/stalwartlabs/mail-send", default-features = false, features = ["cram-md5", "skip-ehlo"] }
mail-builder = { git = "https://github.com/stalwartlabs/mail-builder", features = ["ludicrous_mode"] }
tokio = { version = "1.23", features = ["net"] }
tokio-rustls = { version = "0.24.0"}
rustls = "0.21.0"
@@ -18,6 +20,14 @@ async-trait = "0.1.68"
ahash = { version = "0.8" }
tracing = "0.1"
lru-cache = "0.1.2"
pwhash = "1"
password-hash = "0.5.0"
argon2 = "0.5.0"
pbkdf2 = {version = "0.12.1", features = ["simple"] }
scrypt = "0.11.0"
sha1 = "0.10.5"
sha2 = "0.10.6"
md5 = "0.7.0"
[dev-dependencies]
tokio = { version = "1.23", features = ["full"] }

View File

@@ -1,37 +1,89 @@
use bb8::{ManageConnection, Pool};
use std::{sync::Arc, time::Duration};
use std::{
fs::File,
io::{BufRead, BufReader},
sync::Arc,
time::Duration,
};
use utils::config::Config;
use ahash::AHashMap;
use ahash::{AHashMap, AHashSet};
use crate::{
imap::ImapDirectory, ldap::LdapDirectory, smtp::SmtpDirectory, sql::SqlDirectory, Directory,
imap::ImapDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory,
sql::SqlDirectory, DirectoryConfig, Lookup,
};
pub trait ConfigDirectory {
fn parse_directory(&self) -> utils::config::Result<AHashMap<String, Arc<dyn Directory>>>;
fn parse_directory(&self) -> utils::config::Result<DirectoryConfig>;
}
impl ConfigDirectory for Config {
fn parse_directory(&self) -> utils::config::Result<AHashMap<String, Arc<dyn Directory>>> {
let mut directories = AHashMap::new();
fn parse_directory(&self) -> utils::config::Result<DirectoryConfig> {
let mut config = DirectoryConfig {
directories: AHashMap::new(),
lookups: AHashMap::new(),
};
for id in self.sub_keys("directory") {
directories.insert(
id.to_string(),
match self.value_require(("directory", id, "protocol"))? {
"ldap" => LdapDirectory::from_config(self, ("directory", id))?,
"sql" => SqlDirectory::from_config(self, ("directory", id))?,
"imap" => ImapDirectory::from_config(self, ("directory", id))?,
"smtp" => SmtpDirectory::from_config(self, ("directory", id), false)?,
"lmtp" => SmtpDirectory::from_config(self, ("directory", id), true)?,
unknown => {
return Err(format!("Unknown directory type: {unknown:?}"));
// Parse directory
let protocol = self.value_require(("directory", id, "protocol"))?;
let directory = match protocol {
"ldap" => LdapDirectory::from_config(self, ("directory", id))?,
"sql" => SqlDirectory::from_config(self, ("directory", id))?,
"imap" => ImapDirectory::from_config(self, ("directory", id))?,
"smtp" => SmtpDirectory::from_config(self, ("directory", id), false)?,
"lmtp" => SmtpDirectory::from_config(self, ("directory", id), true)?,
"memory" => MemoryDirectory::from_config(self, ("directory", id))?,
unknown => {
return Err(format!("Unknown directory type: {unknown:?}"));
}
};
// Parse lookups
let is_remote = protocol != "memory";
for lookup_id in self.sub_keys(("directory", id, "lookup")) {
let lookup = if is_remote {
Lookup::Directory {
directory: directory.clone(),
query: self
.value_require(("directory", id, "lookup", lookup_id))?
.to_string(),
}
},
);
} else {
let mut list = AHashSet::new();
for (_, value) in self.values(("directory", id, "lookup", lookup_id)) {
if let Some(path) = value.strip_prefix("file://") {
for line in BufReader::new(File::open(path).map_err(|err| {
format!(
"Failed to read file {path:?} for list {id}/{lookup_id}: {err}"
)
})?)
.lines()
{
let line_ = line.map_err(|err| {
format!("Failed to read file {path:?} for list {id}/{lookup_id}: {err}")
})?;
let line = line_.trim();
if !line.is_empty() {
list.insert(line.to_string());
}
}
} else {
list.insert(value.to_string());
}
}
Lookup::List { list }
};
config
.lookups
.insert(format!("{id}/{lookup_id}"), Arc::new(lookup));
}
config.directories.insert(id.to_string(), directory);
}
Ok(directories)
Ok(config)
}
}

View File

@@ -35,7 +35,7 @@ impl LdapDirectory {
);
let mut mappings = LdapMappings {
base_dn: config.value_require((&prefix, "address"))?.to_string(),
base_dn: config.value_require((&prefix, "base-dn"))?.to_string(),
filter_login: LdapFilter::from_config(config, (&prefix, "filter.login"))?,
filter_name: LdapFilter::from_config(config, (&prefix, "filter.name"))?,
filter_email: LdapFilter::from_config(config, (&prefix, "filter.email"))?,
@@ -73,7 +73,7 @@ impl LdapDirectory {
.map(|(_, v)| v.to_string())
.collect(),
attr_quota: config
.values((&prefix, "attributes."))
.values((&prefix, "attributes.quota"))
.map(|(_, v)| v.to_string())
.collect(),
attrs_principal: vec!["objectClass".to_string()],

View File

@@ -21,7 +21,7 @@ impl Directory for LdapDirectory {
.await
{
Ok(Some(principal)) => {
if principal.secret.as_ref().map_or(false, |s| s == secret) {
if principal.verify_secret(secret) {
Ok(Some(principal))
} else {
Ok(None)
@@ -49,7 +49,7 @@ impl Directory for LdapDirectory {
let mut ids = Vec::with_capacity(principal.member_of.len());
for group in &principal.member_of {
let (rs, _res) = if group.contains('=') {
conn.search(group, Scope::Base, "", &self.mappings.attr_id)
conn.search(group, Scope::Base, "objectClass=*", &self.mappings.attr_id)
.await?
.success()?
} else {
@@ -126,11 +126,12 @@ impl Directory for LdapDirectory {
let mut ids = Vec::new();
for entry in rs {
let entry = SearchEntry::construct(entry);
for attr in &self.mappings.attr_id {
'outer: for attr in &self.mappings.attr_id {
if let Some(values) = entry.attrs.get(attr) {
for id in values {
if let Ok(id) = id.parse() {
ids.push(id);
break 'outer;
}
}
}
@@ -279,16 +280,20 @@ impl LdapMappings {
..Default::default()
};
for (attr, value) in entry.attrs {
if self.attr_id.contains(&attr) {
if let Ok(id) = value.into_iter().next().unwrap_or_default().parse() {
principal.id = id;
if let Some(idx) = self.attr_id.iter().position(|a| a == &attr) {
if principal.id == u32::MAX || idx == 0 {
if let Ok(id) = value.into_iter().next().unwrap_or_default().parse() {
principal.id = id;
}
}
} else if self.attr_name.contains(&attr) {
principal.name = value.into_iter().next().unwrap_or_default();
} else if self.attr_secret.contains(&attr) {
principal.secret = value.into_iter().next();
} else if self.attr_description.contains(&attr) {
principal.description = value.into_iter().next();
principal.secrets.extend(value);
} else if let Some(idx) = self.attr_description.iter().position(|a| a == &attr) {
if principal.description.is_none() || idx == 0 {
principal.description = value.into_iter().next();
}
} else if self.attr_groups.contains(&attr) {
principal.member_of.extend(value);
} else if self.attr_quota.contains(&attr) {

View File

@@ -1,3 +1,6 @@
use std::{fmt::Debug, sync::Arc};
use ahash::{AHashMap, AHashSet};
use bb8::RunError;
use imap::ImapError;
use ldap3::LdapError;
@@ -6,21 +9,23 @@ use mail_send::Credentials;
pub mod config;
pub mod imap;
pub mod ldap;
pub mod memory;
pub mod secret;
pub mod smtp;
pub mod sql;
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Principal {
pub id: u32,
pub name: String,
pub secret: Option<String>,
pub secrets: Vec<String>,
pub typ: Type,
pub description: Option<String>,
pub quota: u32,
pub member_of: Vec<String>,
}
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Type {
Individual,
Group,
@@ -52,6 +57,72 @@ pub trait Directory: Sync + Send {
async fn vrfy(&self, address: &str) -> Result<Vec<String>>;
async fn expn(&self, address: &str) -> Result<Vec<String>>;
async fn query(&self, query: &str, params: &[&str]) -> Result<bool>;
fn type_name(&self) -> &'static str {
std::any::type_name::<Self>()
}
}
#[derive(Clone)]
pub enum Lookup {
Directory {
directory: Arc<dyn Directory>,
query: String,
},
List {
list: AHashSet<String>,
},
}
impl Lookup {
pub async fn contains(&self, item: &str) -> Option<bool> {
match self {
Lookup::Directory { directory, query } => match directory.query(query, &[item]).await {
Ok(result) => result.into(),
Err(_) => None,
},
Lookup::List { list } => list.contains(item).into(),
}
}
}
impl PartialEq for Lookup {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Lookup::Directory { query, .. }, Lookup::Directory { query: other, .. }) => {
query == other
}
(Lookup::List { list }, Lookup::List { list: other }) => list == other,
_ => false,
}
}
}
impl Eq for Lookup {}
impl Debug for dyn Directory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Directory")
.field("type", &self.type_name())
.finish()
}
}
impl Debug for Lookup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Directory { query, .. } => {
f.debug_struct("Directory").field("query", query).finish()
}
Self::List { list } => f.debug_struct("List").field("list", list).finish(),
}
}
}
#[derive(Default, Clone)]
pub struct DirectoryConfig {
pub directories: AHashMap<String, Arc<dyn Directory>>,
pub lookups: AHashMap<String, Arc<Lookup>>,
}
pub type Result<T> = std::result::Result<T, DirectoryError>;
@@ -161,51 +232,3 @@ impl DirectoryError {
DirectoryError::TimedOut
}
}
#[cfg(test)]
mod tests {
use ldap3::{LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
use crate::ldap::{Bind, LdapConnectionManager};
#[tokio::test]
async fn ldap() {
let manager = LdapConnectionManager::new(
"ldap://localhost:3893".to_string(),
LdapConnSettings::new(),
Bind::new(
"cn=serviceuser,ou=svcaccts,dc=example,dc=com".into(),
"mysecret".into(),
)
.into(),
);
let pool = bb8::Pool::builder()
.min_idle(None)
.max_size(10)
.max_lifetime(std::time::Duration::from_secs(30 * 60).into())
.idle_timeout(std::time::Duration::from_secs(10 * 60).into())
.connection_timeout(std::time::Duration::from_secs(30))
.test_on_check_out(true)
.build(manager)
.await
.unwrap();
let mut ldap = pool.get().await.unwrap();
let (rs, _res) = ldap
.search(
"dc=example,dc=com",
Scope::Subtree,
"(&(objectClass=posixAccount)(cn=johndoe))",
vec!["cocomiel", "cn", "uidNumber"], //Vec::<String>::new(),
)
.await
.unwrap()
.success()
.unwrap();
for entry in rs {
println!("{:#?}", SearchEntry::construct(entry));
}
ldap.unbind().await.unwrap()
}
}

View File

@@ -0,0 +1,100 @@
use std::sync::Arc;
use utils::config::{utils::AsKey, Config};
use crate::{Directory, Principal, Type};
use super::{EmailType, MemoryDirectory};
impl MemoryDirectory {
pub fn from_config(
config: &Config,
prefix: impl AsKey,
) -> utils::config::Result<Arc<dyn Directory>> {
let prefix = prefix.as_key();
let mut directory = MemoryDirectory::default();
for lookup_id in config.sub_keys((prefix.as_str(), "users")) {
let id = directory.principals.len() as u32;
let name = config
.value_require((prefix.as_str(), "users", lookup_id, "name"))?
.to_string();
directory.names.insert(name.clone(), id);
directory.principals.push(Principal {
id,
name,
secrets: config
.values((prefix.as_str(), "users", lookup_id, "secret"))
.map(|(_, v)| v.to_string())
.collect(),
typ: Type::Individual,
description: config
.value((prefix.as_str(), "users", lookup_id, "description"))
.map(|v| v.to_string()),
quota: config
.property((prefix.as_str(), "users", lookup_id, "quota"))?
.unwrap_or(0),
member_of: config
.values((prefix.as_str(), "users", lookup_id, "member-of"))
.map(|(_, v)| v.to_string())
.collect(),
});
let mut emails = Vec::new();
for (pos, (_, email)) in config
.values((prefix.as_str(), "users", lookup_id, "email"))
.enumerate()
{
directory
.emails_to_ids
.entry(email.to_string())
.or_default()
.push(if pos > 0 {
EmailType::Alias(id)
} else {
EmailType::Primary(id)
});
emails.push(if pos > 0 {
EmailType::Alias(email.to_string())
} else {
EmailType::Primary(email.to_string())
});
}
for (_, email) in config.values((prefix.as_str(), "users", lookup_id, "email-list")) {
directory
.emails_to_ids
.entry(email.to_string())
.or_default()
.push(EmailType::List(id));
emails.push(EmailType::List(email.to_string()));
}
directory.ids_to_email.insert(id, emails);
}
for lookup_id in config.sub_keys((prefix.as_str(), "groups")) {
let id = directory.principals.len() as u32;
let name = config
.value_require((prefix.as_str(), "groups", lookup_id, "name"))?
.to_string();
directory.names.insert(name.clone(), id);
directory.principals.push(Principal {
id,
name,
secrets: vec![],
typ: Type::Group,
description: config
.value((prefix.as_str(), "groups", lookup_id, "description"))
.map(|v| v.to_string()),
quota: config
.property((prefix.as_str(), "groups", lookup_id, "quota"))?
.unwrap_or(0),
member_of: config
.values((prefix.as_str(), "groups", lookup_id, "member-of"))
.map(|(_, v)| v.to_string())
.collect(),
});
}
Ok(Arc::new(directory))
}
}

View File

@@ -0,0 +1,115 @@
use mail_send::Credentials;
use crate::{Directory, DirectoryError, Principal};
use super::{EmailType, MemoryDirectory};
#[async_trait::async_trait]
impl Directory for MemoryDirectory {
async fn authenticate(
&self,
credentials: &Credentials<String>,
) -> crate::Result<Option<Principal>> {
let (username, secret) = match credentials {
Credentials::Plain { username, secret } => (username, secret),
Credentials::OAuthBearer { token } => (token, token),
Credentials::XOauth2 { username, secret } => (username, secret),
};
match self
.names
.get(username)
.and_then(|id| self.principals.get(*id as usize))
{
Some(principal) if principal.verify_secret(secret) => Ok(Some(principal.clone())),
_ => Ok(None),
}
}
async fn principal_by_name(&self, name: &str) -> crate::Result<Option<Principal>> {
Ok(self
.names
.get(name)
.and_then(|id| self.principals.get(*id as usize))
.cloned())
}
async fn principal_by_id(&self, id: u32) -> crate::Result<Option<Principal>> {
Ok(self.principals.get(id as usize).cloned())
}
async fn member_of(&self, principal: &Principal) -> crate::Result<Vec<u32>> {
let mut result = Vec::with_capacity(principal.member_of.len());
for member in &principal.member_of {
if let Some(id) = self.names.get(member) {
result.push(*id);
}
}
Ok(result)
}
async fn emails_by_id(&self, id: u32) -> crate::Result<Vec<String>> {
let mut result = Vec::new();
if let Some(emails) = self.ids_to_email.get(&id) {
for email in emails {
match email {
EmailType::Primary(email) | EmailType::Alias(email) => {
result.push(email.clone())
}
_ => {}
}
}
}
Ok(result)
}
async fn ids_by_email(&self, address: &str) -> crate::Result<Vec<u32>> {
Ok(self
.emails_to_ids
.get(address)
.map(|ids| {
ids.iter()
.map(|t| match t {
EmailType::Primary(id) | EmailType::Alias(id) | EmailType::List(id) => *id,
})
.collect::<Vec<_>>()
})
.unwrap_or_default())
}
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
Ok(self.emails_to_ids.get(address).is_some())
}
async fn vrfy(&self, address: &str) -> crate::Result<Vec<String>> {
let mut result = Vec::new();
for (key, value) in &self.emails_to_ids {
if key.contains(address) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) {
result.push(key.clone())
}
}
Ok(result)
}
async fn expn(&self, address: &str) -> crate::Result<Vec<String>> {
let mut result = Vec::new();
for (key, value) in &self.emails_to_ids {
if key == address {
for item in value {
if let EmailType::List(id) = item {
for addr in self.ids_to_email.get(id).unwrap() {
if let EmailType::Primary(addr) = addr {
result.push(addr.clone())
}
}
}
}
}
}
Ok(result)
}
async fn query(&self, _query: &str, _params: &[&str]) -> crate::Result<bool> {
Err(DirectoryError::unsupported("memory", "query"))
}
}

View File

@@ -0,0 +1,20 @@
use ahash::AHashMap;
use crate::Principal;
pub mod config;
pub mod lookup;
#[derive(Default)]
pub struct MemoryDirectory {
principals: Vec<Principal>,
names: AHashMap<String, u32>,
emails_to_ids: AHashMap<String, Vec<EmailType<u32>>>,
ids_to_email: AHashMap<u32, Vec<EmailType<String>>>,
}
enum EmailType<T> {
Primary(T),
Alias(T),
List(T),
}

View File

@@ -0,0 +1,159 @@
use argon2::Argon2;
use mail_builder::encoders::base64::base64_encode;
use mail_parser::decoders::base64::base64_decode;
use password_hash::PasswordHash;
use pbkdf2::Pbkdf2;
use pwhash::{bcrypt, bsdi_crypt, md5_crypt, sha1_crypt, sha256_crypt, sha512_crypt, unix_crypt};
use scrypt::Scrypt;
use sha1::Digest;
use sha1::Sha1;
use sha2::Sha256;
use sha2::Sha512;
use crate::Principal;
impl Principal {
pub fn verify_secret(&self, secret: &str) -> bool {
self.secrets.iter().any(|s| verify_secret_hash(s, secret))
}
}
fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
if hashed_secret.starts_with('$') {
if hashed_secret.starts_with("$argon2")
|| hashed_secret.starts_with("$pbkdf2")
|| hashed_secret.starts_with("$scrypt")
{
match PasswordHash::new(hashed_secret) {
Ok(hash) => hash
.verify_password(&[&Argon2::default(), &Pbkdf2, &Scrypt], secret)
.is_ok(),
Err(_) => {
tracing::warn!(
context = "directory",
event = "error",
hash = hashed_secret,
"Invalid password hash"
);
false
}
}
} else if hashed_secret.starts_with("$2") {
// Blowfish crypt
bcrypt::verify(secret, hashed_secret)
} else if hashed_secret.starts_with("$6$") {
// SHA-512 crypt
sha512_crypt::verify(secret, hashed_secret)
} else if hashed_secret.starts_with("$5$") {
// SHA-256 crypt
sha256_crypt::verify(secret, hashed_secret)
} else if hashed_secret.starts_with("$sha1") {
// SHA-1 crypt
sha1_crypt::verify(secret, hashed_secret)
} else if hashed_secret.starts_with("$1") {
// MD5 based hash
md5_crypt::verify(secret, hashed_secret)
} else {
// Unknown hash
tracing::warn!(
context = "directory",
event = "error",
hash = hashed_secret,
"Invalid password hash"
);
false
}
} else if hashed_secret.starts_with('_') {
// Enhanced DES-based hash
bsdi_crypt::verify(secret, hashed_secret)
} else if let Some(hashed_secret) = hashed_secret.strip_prefix('{') {
if let Some((algo, hashed_secret)) = hashed_secret.split_once('}') {
match algo {
"SHA" => {
// SHA-1
let mut hasher = Sha1::new();
hasher.update(secret.as_bytes());
String::from_utf8(base64_encode(&hasher.finalize()[..]).unwrap_or_default())
.unwrap()
== hashed_secret
}
"SSHA" => {
// Salted SHA-1
let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default();
let hash = decoded.get(..20).unwrap_or_default();
let salt = decoded.get(20..).unwrap_or_default();
let mut hasher = Sha1::new();
hasher.update(secret.as_bytes());
hasher.update(salt);
&hasher.finalize()[..] == hash
}
"SHA256" => {
// Verify hash
let mut hasher = Sha256::new();
hasher.update(secret.as_bytes());
String::from_utf8(base64_encode(&hasher.finalize()[..]).unwrap_or_default())
.unwrap()
== hashed_secret
}
"SSHA256" => {
// Salted SHA-256
let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default();
let hash = decoded.get(..32).unwrap_or_default();
let salt = decoded.get(32..).unwrap_or_default();
let mut hasher = Sha256::new();
hasher.update(secret.as_bytes());
hasher.update(salt);
&hasher.finalize()[..] == hash
}
"SHA512" => {
// SHA-512
let mut hasher = Sha512::new();
hasher.update(secret.as_bytes());
String::from_utf8(base64_encode(&hasher.finalize()[..]).unwrap_or_default())
.unwrap()
== hashed_secret
}
"SSHA512" => {
// Salted SHA-512
let decoded = base64_decode(hashed_secret.as_bytes()).unwrap_or_default();
let hash = decoded.get(..64).unwrap_or_default();
let salt = decoded.get(64..).unwrap_or_default();
let mut hasher = Sha512::new();
hasher.update(secret.as_bytes());
hasher.update(salt);
&hasher.finalize()[..] == hash
}
"MD5" => {
// MD5
let digest = md5::compute(secret.as_bytes());
String::from_utf8(base64_encode(&digest[..]).unwrap_or_default()).unwrap()
== hashed_secret
}
"CRYPT" | "crypt" => {
// Unix crypt
unix_crypt::verify(secret, hashed_secret)
}
"PLAIN" | "plain" | "CLEAR" | "clear" => hashed_secret == secret,
_ => {
tracing::warn!(
context = "directory",
event = "error",
algorithm = algo,
"Unsupported password hash algorithm"
);
false
}
}
} else {
tracing::warn!(
context = "directory",
event = "error",
hash = hashed_secret,
"Invalid password hash"
);
false
}
} else {
hashed_secret == secret
}
}

View File

@@ -1,6 +1,6 @@
use std::sync::Arc;
use sqlx::any::AnyPoolOptions;
use sqlx::any::{install_default_drivers, AnyPoolOptions};
use utils::config::{utils::AsKey, Config};
use crate::Directory;
@@ -14,7 +14,7 @@ impl SqlDirectory {
) -> utils::config::Result<Arc<dyn Directory>> {
let prefix = prefix.as_key();
let address = config.value_require((&prefix, "address"))?;
install_default_drivers();
let pool = AnyPoolOptions::new()
.max_connections(
config
@@ -64,27 +64,27 @@ impl SqlDirectory {
.unwrap_or_default()
.to_string(),
column_name: config
.value((&prefix, "column.name"))
.value((&prefix, "columns.name"))
.unwrap_or_default()
.to_string(),
column_description: config
.value((&prefix, "column.description"))
.value((&prefix, "columns.description"))
.unwrap_or_default()
.to_string(),
column_secret: config
.value((&prefix, "column.secret"))
.value((&prefix, "columns.secret"))
.unwrap_or_default()
.to_string(),
column_id: config
.value((&prefix, "column.id"))
.value((&prefix, "columns.id"))
.unwrap_or_default()
.to_string(),
column_quota: config
.value((&prefix, "column.quota"))
.value((&prefix, "columns.quota"))
.unwrap_or_default()
.to_string(),
column_type: config
.value((&prefix, "column.type"))
.value((&prefix, "columns.type"))
.unwrap_or_default()
.to_string(),
};

View File

@@ -23,7 +23,7 @@ impl Directory for SqlDirectory {
.await?
{
self.mappings.row_to_principal(row).map(|p| {
if p.secret.as_ref().map_or(false, |s| s == secret) {
if p.verify_secret(secret) {
Some(p)
} else {
None
@@ -85,7 +85,7 @@ impl Directory for SqlDirectory {
}
async fn rcpt(&self, address: &str) -> crate::Result<bool> {
sqlx::query_scalar::<_, i64>(&self.mappings.query_recipients)
sqlx::query(&self.mappings.query_recipients)
.bind(address)
.fetch_optional(&self.pool)
.await
@@ -136,7 +136,9 @@ impl SqlMappings {
} else if name.eq_ignore_ascii_case(&self.column_name) {
principal.name = row.try_get::<Option<String>, _>(idx)?.unwrap_or_default();
} else if name.eq_ignore_ascii_case(&self.column_secret) {
principal.secret = row.try_get::<Option<String>, _>(idx)?;
if let Some(secret) = row.try_get::<Option<String>, _>(idx)? {
principal.secrets.push(secret);
}
} else if name.eq_ignore_ascii_case(&self.column_type) {
if let Some(typ) = row.try_get::<Option<String>, _>(idx)? {
match typ.as_str() {
@@ -148,7 +150,7 @@ impl SqlMappings {
} else if name.eq_ignore_ascii_case(&self.column_description) {
principal.description = row.try_get::<Option<String>, _>(idx)?;
} else if name.eq_ignore_ascii_case(&self.column_quota) {
principal.quota = row.try_get::<i64, _>(idx)? as u32;
principal.quota = row.try_get::<i64, _>(idx).unwrap_or_default() as u32;
}
}

View File

@@ -8,6 +8,7 @@ pub struct SqlDirectory {
mappings: SqlMappings,
}
#[derive(Debug)]
pub(crate) struct SqlMappings {
query_login: String,
query_name: String,