Skip to content

Commit

Permalink
Fail2ban and IP address blocking support (closes #164)
Browse files Browse the repository at this point in the history
  • Loading branch information
mdecimus committed Jan 13, 2024
1 parent b7c0344 commit 6aeadb9
Show file tree
Hide file tree
Showing 98 changed files with 1,538 additions and 752 deletions.
24 changes: 24 additions & 0 deletions crates/cli/src/modules/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,32 @@ pub enum ExportCommands {
pub enum ServerCommands {
/// Perform database maintenance
DatabaseMaintenance {},

/// Reload TLS certificates
ReloadCertificates {},

/// Reload configuration
ReloadConfig {},

/// Create a new configuration key
AddConfig {
/// Key to add
key: String,
/// Value to set
value: Option<String>,
},

/// Delete a configuration key or prefix
DeleteConfig {
/// Configuration key or prefix to delete
key: String,
},

/// List all configuration entries
ListConfig {
/// Prefix to filter configuration entries by
prefix: Option<String>,
},
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
Expand Down
60 changes: 59 additions & 1 deletion crates/cli/src/modules/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* for more details.
*/

use prettytable::{Attr, Cell, Row, Table};
use reqwest::Method;
use serde_json::Value;

Expand All @@ -37,10 +38,67 @@ impl ServerCommands {
}
ServerCommands::ReloadCertificates {} => {
client
.http_request::<Value, String>(Method::GET, "/admin/certificates/reload", None)
.http_request::<Value, String>(Method::GET, "/admin/reload/certificates", None)
.await;
eprintln!("Success.");
}
ServerCommands::ReloadConfig {} => {
client
.http_request::<Value, String>(Method::GET, "/admin/reload/config", None)
.await;
eprintln!("Success.");
}
ServerCommands::AddConfig { key, value } => {
client
.http_request::<Value, _>(
Method::POST,
"/admin/config",
Some(vec![(key.clone(), value.unwrap_or_default())]),
)
.await;
eprintln!("Successfully added key {key}.");
}
ServerCommands::DeleteConfig { key } => {
client
.http_request::<Value, String>(
Method::DELETE,
&format!("/admin/config/{key}"),
None,
)
.await;
eprintln!("Successfully deleted key {key}.");
}
ServerCommands::ListConfig { prefix } => {
let results = client
.http_request::<Vec<(String, String)>, String>(
Method::GET,
&format!("/admin/config/{}", prefix.unwrap_or_default()),
None,
)
.await;

if !results.is_empty() {
let mut table = Table::new();
table.add_row(Row::new(vec![
Cell::new("Key").with_style(Attr::Bold),
Cell::new("Value").with_style(Attr::Bold),
]));

for (key, value) in &results {
table.add_row(Row::new(vec![Cell::new(key), Cell::new(value)]));
}

eprintln!();
table.printstd();
eprintln!();
}

eprintln!(
"\n\n{} key{} found.\n",
results.len(),
if results.len() == 1 { "" } else { "s" }
);
}
}
}
}
8 changes: 7 additions & 1 deletion crates/directory/src/backend/imap/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@
*/

use mail_send::smtp::tls::build_tls_connector;
use store::Store;
use utils::config::{utils::AsKey, Config};

use crate::core::config::build_pool;

use super::{ImapConnectionManager, ImapDirectory};

impl ImapDirectory {
pub fn from_config(config: &Config, prefix: impl AsKey) -> utils::config::Result<Self> {
pub fn from_config(
config: &Config,
prefix: impl AsKey,
data_store: Store,
) -> utils::config::Result<Self> {
let prefix = prefix.as_key();
let address = config.value_require((&prefix, "address"))?;
let tls_implicit: bool = config.property_or_static((&prefix, "tls.implicit"), "false")?;
Expand All @@ -53,6 +58,7 @@ impl ImapDirectory {
.values((&prefix, "lookup.domains"))
.map(|(_, v)| v.to_lowercase())
.collect(),
data_store,
})
}
}
2 changes: 2 additions & 0 deletions crates/directory/src/backend/imap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ use std::{fmt::Display, sync::atomic::AtomicU64, time::Duration};

use ahash::AHashSet;
use deadpool::managed::Pool;
use store::Store;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_rustls::TlsConnector;

pub struct ImapDirectory {
pool: Pool<ImapConnectionManager>,
domains: AHashSet<String>,
pub(crate) data_store: Store,
}

pub struct ImapConnectionManager {
Expand Down
4 changes: 2 additions & 2 deletions crates/directory/src/backend/ldap/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl LdapDirectory {
pub fn from_config(
config: &Config,
prefix: impl AsKey,
id_store: Option<Store>,
data_store: Store,
) -> utils::config::Result<Self> {
let prefix = prefix.as_key();
let bind_dn = if let Some(dn) = config.value((&prefix, "bind.dn")) {
Expand Down Expand Up @@ -123,7 +123,7 @@ impl LdapDirectory {
mappings,
pool: build_pool(config, &prefix, manager)?,
auth_bind,
id_store,
data_store,
})
}
}
Expand Down
25 changes: 6 additions & 19 deletions crates/directory/src/backend/ldap/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

use ldap3::{Ldap, LdapConnAsync, LdapError, Scope, SearchEntry};
use mail_send::Credentials;
use store::Store;

use crate::{backend::internal::manage::ManageDirectory, DirectoryError, Principal, QueryBy, Type};

Expand Down Expand Up @@ -53,7 +52,7 @@ impl LdapDirectory {
}
}
QueryBy::Id(uid) => {
if let Some(username) = self.unwrap_id_store().get_account_name(uid).await? {
if let Some(username) = self.data_store.get_account_name(uid).await? {
account_name = username;
} else {
return Ok(None);
Expand Down Expand Up @@ -127,16 +126,16 @@ impl LdapDirectory {
// Obtain account ID if not available
if let Some(account_id) = account_id {
principal.id = account_id;
} else if self.has_id_store() {
} else {
principal.id = self
.unwrap_id_store()
.data_store
.get_or_create_account_id(&account_name)
.await?;
}
principal.name = account_name;

// Obtain groups
if return_member_of && !principal.member_of.is_empty() && self.has_id_store() {
if return_member_of && !principal.member_of.is_empty() {
for member_of in principal.member_of.iter_mut() {
if member_of.contains('=') {
let (rs, _res) = conn
Expand Down Expand Up @@ -164,7 +163,7 @@ impl LdapDirectory {
}

// Map ids
self.unwrap_id_store()
self.data_store
.map_group_names(principal, true)
.await
.map(Some)
Expand Down Expand Up @@ -195,11 +194,7 @@ impl 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(
self.unwrap_id_store()
.get_or_create_account_id(name)
.await?,
);
ids.push(self.data_store.get_or_create_account_id(name).await?);
break 'outer;
}
}
Expand Down Expand Up @@ -326,14 +321,6 @@ impl LdapDirectory {
})
.map_err(Into::into)
}

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 {
Expand Down
2 changes: 1 addition & 1 deletion crates/directory/src/backend/ldap/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub struct LdapDirectory {
pool: Pool<LdapConnectionManager>,
mappings: LdapMappings,
auth_bind: Option<LdapFilter>,
id_store: Option<Store>,
pub(crate) data_store: Store,
}

#[derive(Debug, Default)]
Expand Down
36 changes: 21 additions & 15 deletions crates/directory/src/backend/memory/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,23 +24,25 @@
use store::Store;
use utils::config::{utils::AsKey, Config};

use crate::{Principal, Type};
use crate::{backend::internal::manage::ManageDirectory, Principal, Type};

use super::{EmailType, MemoryDirectory};

impl MemoryDirectory {
pub async fn from_config(
config: &Config,
prefix: impl AsKey,
id_store: Option<Store>,
data_store: Store,
) -> utils::config::Result<Self> {
let prefix = prefix.as_key();
let mut directory = MemoryDirectory {
names_to_ids: id_store.into(),
..Default::default()
data_store,
principals: Default::default(),
emails_to_ids: Default::default(),
domains: Default::default(),
};

for lookup_id in config.sub_keys((prefix.as_str(), "principals")) {
for lookup_id in config.sub_keys((prefix.as_str(), "principals"), ".name") {
let name = config
.value_require((prefix.as_str(), "principals", lookup_id, "name"))?
.to_string();
Expand All @@ -53,8 +55,8 @@ impl MemoryDirectory {

// Obtain id
let id = directory
.names_to_ids
.get_or_insert(&name)
.data_store
.get_or_create_account_id(&name)
.await
.map_err(|err| {
format!(
Expand All @@ -67,14 +69,18 @@ impl MemoryDirectory {
let mut member_of = Vec::new();
for (_, group) in config.values((prefix.as_str(), "principals", lookup_id, "member-of"))
{
member_of.push(directory.names_to_ids.get_or_insert(group).await.map_err(
|err| {
format!(
"Failed to obtain id for principal {} ({}): {:?}",
name, lookup_id, err
)
},
)?);
member_of.push(
directory
.data_store
.get_or_create_account_id(group)
.await
.map_err(|err| {
format!(
"Failed to obtain id for principal {} ({}): {:?}",
name, lookup_id, err
)
})?,
);
}

// Parse email addresses
Expand Down
47 changes: 2 additions & 45 deletions crates/directory/src/backend/memory/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,60 +26,17 @@ use store::Store;

use crate::Principal;

use super::internal::manage::ManageDirectory;

pub mod config;
pub mod lookup;

#[derive(Default, Debug)]
#[derive(Debug)]
pub struct MemoryDirectory {
principals: Vec<Principal<u32>>,
emails_to_ids: AHashMap<String, Vec<EmailType>>,
names_to_ids: NameToId,
pub(crate) data_store: Store,
domains: AHashSet<String>,
}

pub enum NameToId {
Internal(AHashMap<String, u32>),
Store(Store),
}

impl Default for NameToId {
fn default() -> Self {
Self::Internal(AHashMap::new())
}
}

impl std::fmt::Debug for NameToId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Internal(arg0) => f.debug_tuple("Internal").field(arg0).finish(),
Self::Store(_) => f.debug_tuple("Store").finish(),
}
}
}

impl From<Option<Store>> for NameToId {
fn from(store: Option<Store>) -> Self {
match store {
Some(store) => Self::Store(store),
None => Self::Internal(AHashMap::new()),
}
}
}

impl NameToId {
pub async fn get_or_insert(&mut self, name: &str) -> crate::Result<u32> {
match self {
Self::Internal(map) => {
let next_id = map.len() as u32;
Ok(*map.entry(name.to_string()).or_insert(next_id))
}
Self::Store(store) => store.get_or_create_account_id(name).await,
}
}
}

#[derive(Debug)]
enum EmailType {
Primary(u32),
Expand Down
Loading

0 comments on commit 6aeadb9

Please sign in to comment.