Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
202 lines
7.4 KiB
Rust
202 lines
7.4 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use super::{SqlDirectory, SqlMappings};
|
|
use crate::{Account, Credentials, Recipient, core::secret::verify_secret_hash};
|
|
use store::{NamedRows, Rows, Value};
|
|
use trc::AddContext;
|
|
use utils::sanitize_email;
|
|
|
|
impl SqlDirectory {
|
|
pub async fn authenticate(&self, credentials: &Credentials) -> trc::Result<Account> {
|
|
let (username, secret) = match credentials {
|
|
Credentials::Basic {
|
|
username, secret, ..
|
|
} => (username, secret),
|
|
Credentials::Bearer { .. } => {
|
|
return Err(trc::AuthEvent::Error
|
|
.into_err()
|
|
.details("Unsupported credentials type for SQL authentication"));
|
|
}
|
|
};
|
|
if secret.is_empty() {
|
|
return Err(trc::AuthEvent::Failed
|
|
.into_err()
|
|
.details("Empty secret rejected")
|
|
.ctx(trc::Key::AccountName, username.to_string()));
|
|
}
|
|
|
|
let Recipient::Account(mut account) = self.mappings.row_to_account(
|
|
self.sql_store
|
|
.sql_query::<NamedRows>(&self.mappings.query_login, vec![username.into()])
|
|
.await
|
|
.caused_by(trc::location!())?,
|
|
) else {
|
|
return Err(trc::AuthEvent::Failed
|
|
.into_err()
|
|
.details("SQL login query did not return an account")
|
|
.ctx(trc::Key::AccountName, username.to_string()));
|
|
};
|
|
|
|
// Validate secret
|
|
if let Some(account_secret) = &account.secret {
|
|
if !verify_secret_hash(account_secret, secret.as_bytes()).await? {
|
|
return Err(trc::AuthEvent::Failed
|
|
.into_err()
|
|
.details("Invalid credentials")
|
|
.ctx(trc::Key::AccountName, username.to_string()));
|
|
}
|
|
} else {
|
|
return Err(trc::AuthEvent::Error
|
|
.into_err()
|
|
.details("Account does not have a secret")
|
|
.ctx(trc::Key::AccountName, username.to_string()));
|
|
}
|
|
|
|
// Obtain members
|
|
if let Some(query) = &self.mappings.query_member_of {
|
|
let members = account.groups.get_or_insert_default();
|
|
for row in self
|
|
.sql_store
|
|
.sql_query::<Rows>(query, vec![username.into()])
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.rows
|
|
{
|
|
if let Some(Value::Text(address)) = row.values.first()
|
|
&& let Some(email) = sanitize_email(address)
|
|
{
|
|
members.push(email);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Obtain emails
|
|
if let Some(query) = &self.mappings.query_email_aliases {
|
|
account.email_aliases.extend(
|
|
self.sql_store
|
|
.sql_query::<Rows>(query, vec![username.into()])
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.rows
|
|
.into_iter()
|
|
.flat_map(|v| {
|
|
v.values
|
|
.into_iter()
|
|
.filter_map(|v| sanitize_email(v.to_str().as_ref()))
|
|
}),
|
|
);
|
|
}
|
|
|
|
if account.email.is_empty() {
|
|
account.email = sanitize_email(username).unwrap_or_else(|| username.to_lowercase());
|
|
}
|
|
|
|
Ok(account)
|
|
}
|
|
|
|
pub async fn recipient(&self, address: &str) -> trc::Result<Recipient> {
|
|
let recipient = self.mappings.row_to_account(
|
|
self.sql_store
|
|
.sql_query::<NamedRows>(&self.mappings.query_recipient, vec![address.into()])
|
|
.await
|
|
.caused_by(trc::location!())?,
|
|
);
|
|
|
|
match recipient {
|
|
Recipient::Account(mut account) => {
|
|
// Obtain members
|
|
if let Some(query) = &self.mappings.query_member_of {
|
|
let members = account.groups.get_or_insert_default();
|
|
for row in self
|
|
.sql_store
|
|
.sql_query::<Rows>(query, vec![account.email.as_str().into()])
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.rows
|
|
{
|
|
if let Some(Value::Text(address)) = row.values.first()
|
|
&& let Some(email) = sanitize_email(address)
|
|
{
|
|
members.push(email);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Obtain emails
|
|
if let Some(query) = &self.mappings.query_email_aliases {
|
|
account.email_aliases.extend(
|
|
self.sql_store
|
|
.sql_query::<Rows>(query, vec![account.email.as_str().into()])
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
.rows
|
|
.into_iter()
|
|
.flat_map(|v| {
|
|
v.values
|
|
.into_iter()
|
|
.filter_map(|v| sanitize_email(v.to_str().as_ref()))
|
|
}),
|
|
);
|
|
}
|
|
|
|
Ok(Recipient::Account(account))
|
|
}
|
|
Recipient::Group(group) => Ok(Recipient::Group(group)),
|
|
Recipient::Invalid => Ok(Recipient::Invalid),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl SqlMappings {
|
|
pub fn row_to_account(&self, rows: NamedRows) -> Recipient {
|
|
if rows.rows.is_empty() {
|
|
return Recipient::Invalid;
|
|
}
|
|
|
|
let mut account = Account::default();
|
|
let mut is_group = false;
|
|
|
|
if let Some(row) = rows.rows.into_iter().next() {
|
|
for (name, value) in rows.names.into_iter().zip(row.values) {
|
|
if name.eq_ignore_ascii_case(&self.column_email) {
|
|
if let Value::Text(text) = value
|
|
&& let Some(email) = sanitize_email(&text)
|
|
{
|
|
account.email = email;
|
|
}
|
|
} else if name.eq_ignore_ascii_case(&self.column_secret) {
|
|
if let Value::Text(text) = value
|
|
&& !text.is_empty()
|
|
{
|
|
account.secret = Some(text.into_owned());
|
|
}
|
|
} else if let Some(column_type) = &self.column_type
|
|
&& name.eq_ignore_ascii_case(column_type)
|
|
{
|
|
is_group = value.to_str().eq_ignore_ascii_case("group");
|
|
} else if let Some(column_description) = &self.column_description
|
|
&& name.eq_ignore_ascii_case(column_description)
|
|
&& let Value::Text(text) = value
|
|
&& !text.is_empty()
|
|
{
|
|
account.description = Some(text.into_owned());
|
|
}
|
|
}
|
|
}
|
|
|
|
if !is_group {
|
|
Recipient::Account(account)
|
|
} else {
|
|
Recipient::Group(crate::Group {
|
|
email: account.email,
|
|
email_aliases: account.email_aliases,
|
|
description: account.description,
|
|
})
|
|
}
|
|
}
|
|
}
|