use crate::auth::proposed_credentials::ProposedCredentials;
use crate::auth::DeclaredAuthenticationMode;
use crate::backend::memory::MemoryBackend;
use crate::backend::{BackendType, PersistenceHandler};
use crate::client_account::ClientNetworkAccount;
use crate::external_services::{ServicesConfig, ServicesHandler};
use crate::misc::{AccountError, CNACMetadata};
use crate::prelude::ConnectionInfo;
use crate::server_misc_settings::ServerMiscSettings;
use citadel_crypt::argon::argon_container::{ArgonDefaultServerSettings, ArgonSettings};
use citadel_crypt::endpoint_crypto_container::PeerSessionCrypto;
use citadel_crypt::ratchets::mono::MonoRatchet;
use citadel_crypt::ratchets::stacked::StackedRatchet;
use citadel_crypt::ratchets::Ratchet;
use citadel_types::prelude::PeerInfo;
use citadel_types::user::MutualPeer;
use citadel_types::user::UserIdentifier;
use futures::stream::FuturesOrdered;
use futures::StreamExt;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
#[derive(Clone)]
pub struct AccountManager<R: Ratchet = StackedRatchet, Fcm: Ratchet = MonoRatchet> {
services_handler: ServicesHandler,
persistence_handler: PersistenceHandler<R, Fcm>,
node_argon_settings: ArgonSettings,
server_misc_settings: ServerMiscSettings,
backend_ty: BackendType,
registration_lock: std::sync::Arc<citadel_io::tokio::sync::Mutex<()>>,
}
impl<R: Ratchet, Fcm: Ratchet> AccountManager<R, Fcm> {
#[allow(unused_results)]
pub async fn new(
backend_type: BackendType,
server_argon_settings: Option<ArgonDefaultServerSettings>,
_services_cfg: Option<ServicesConfig>,
server_misc_settings: Option<ServerMiscSettings>,
) -> Result<Self, AccountError> {
#[cfg(feature = "google-services")]
let services_handler = _services_cfg
.unwrap_or_default()
.into_services_handler()
.await?;
#[cfg(not(feature = "google-services"))]
let services_handler = ServicesHandler;
let persistence_handler = match &backend_type {
BackendType::InMemory => {
let backend = MemoryBackend::default();
PersistenceHandler::create(backend).await?
}
#[cfg(all(feature = "filesystem", not(target_family = "wasm")))]
BackendType::Filesystem(dir) => {
use crate::backend::file_io_backend::FileIOBackend;
use crate::backend::std_file_io::StdFileIO;
let file_io = std::sync::Arc::new(StdFileIO);
let backend = FileIOBackend::new(dir.clone(), file_io);
PersistenceHandler::create(backend).await?
}
#[cfg(feature = "opfs")]
BackendType::Opfs(dir) => {
use crate::backend::file_io_backend::FileIOBackend;
use crate::backend::opfs_file_io::OpfsFileIO;
let file_io = std::sync::Arc::new(OpfsFileIO::new());
let backend = FileIOBackend::new(dir.clone(), file_io);
PersistenceHandler::create(backend).await?
}
#[cfg(all(feature = "sql", not(coverage)))]
BackendType::SQLDatabase(..) => {
use crate::backend::sql_backend::SqlBackend;
let backend = SqlBackend::try_from(backend_type.clone())
.map_err(|_| citadel_io::error!(citadel_io::ErrorCode::BackendUrlInvalid))?;
PersistenceHandler::create(backend).await?
}
#[cfg(all(feature = "redis", not(coverage)))]
BackendType::Redis(url, opts) => {
use crate::backend::redis_backend::RedisBackend;
let backend = RedisBackend::new(url.clone(), opts.clone());
PersistenceHandler::create(backend).await?
}
};
if !persistence_handler.is_connected().await? {
return Err(citadel_io::error!(
citadel_io::ErrorCode::BackendNotConnected
));
}
log::info!(target: "citadel", "Successfully established connection to backend {backend_type:?}...");
let this = Self {
backend_ty: backend_type,
persistence_handler,
services_handler,
node_argon_settings: server_argon_settings.unwrap_or_default().into(),
server_misc_settings: server_misc_settings.unwrap_or_default(),
registration_lock: std::sync::Arc::new(citadel_io::tokio::sync::Mutex::new(())),
};
this.setup_local_only_account().await?;
Ok(this)
}
pub fn services_handler(&self) -> &ServicesHandler {
&self.services_handler
}
pub async fn register_impersonal_hyperlan_client_network_account(
&self,
conn_info: ConnectionInfo,
creds: ProposedCredentials,
session_crypto_state: PeerSessionCrypto<R>,
) -> Result<ClientNetworkAccount<R, Fcm>, AccountError> {
let reserved_cid = self
.persistence_handler
.get_cid_by_username(creds.username());
if reserved_cid == 0 {
return Err(citadel_io::error!(citadel_io::ErrorCode::RegisterCidZero));
}
let auth_store = creds
.derive_server_container(&self.node_argon_settings, self.get_misc_settings())
.await?;
self.server_misc_settings
.credential_requirements
.check::<_, &str, _>(auth_store.username(), None, auth_store.full_name())?;
let pers = &self.persistence_handler;
let _registration_guard = self.registration_lock.lock().await;
log::trace!(target: "citadel", "Checking username {} for correspondence ...", auth_store.username());
let username = auth_store.username().to_string();
if pers.username_exists(&username).await? {
return Err(citadel_io::error!(
citadel_io::ErrorCode::UsernameExists,
username.clone()
));
}
let new_cnac = ClientNetworkAccount::<R, Fcm>::new(
reserved_cid,
false,
conn_info,
auth_store,
Some(session_crypto_state),
)
.await?;
log::trace!(target: "citadel", "Created impersonal CNAC ...");
self.persistence_handler.save_cnac(&new_cnac).await?;
Ok(new_cnac)
}
pub async fn register_personal_hyperlan_server(
&self,
session_crypto_state: PeerSessionCrypto<R>,
creds: ProposedCredentials,
conn_info: ConnectionInfo,
) -> Result<ClientNetworkAccount<R, Fcm>, AccountError> {
let valid_cid = self
.persistence_handler
.get_cid_by_username(creds.username());
if valid_cid == 0 {
return Err(citadel_io::error!(citadel_io::ErrorCode::RegisterCidZero));
}
let client_auth_store = creds.into_auth_store();
let cnac = ClientNetworkAccount::<R, Fcm>::new_from_network_personal(
valid_cid,
Some(session_crypto_state),
client_auth_store,
conn_info,
)
.await?;
self.persistence_handler.save_cnac(&cnac).await?;
Ok(cnac)
}
async fn setup_local_only_account(&self) -> Result<(), AccountError> {
let cnac = ClientNetworkAccount::<R, Fcm>::new_from_network_personal(
0,
None,
DeclaredAuthenticationMode::Transient {
username: Default::default(),
full_name: Default::default(),
},
ConnectionInfo::new("127.0.0.1:12345").expect("Should be valid addr"),
)
.await?;
self.persistence_handler.save_cnac(&cnac).await?;
Ok(())
}
pub async fn hyperlan_cid_is_registered(&self, cid: u64) -> Result<bool, AccountError> {
self.persistence_handler.cid_is_registered(cid).await
}
pub async fn get_registered_impersonal_cids(
&self,
limit: Option<i32>,
) -> Result<Option<Vec<u64>>, AccountError> {
self.persistence_handler
.get_registered_impersonal_cids(limit)
.await
}
pub async fn get_client_by_cid(
&self,
cid: u64,
) -> Result<Option<ClientNetworkAccount<R, Fcm>>, AccountError> {
self.persistence_handler.get_cnac_by_cid(cid).await
}
pub async fn get_username_by_cid(&self, cid: u64) -> Result<Option<String>, AccountError> {
self.persistence_handler.get_username_by_cid(cid).await
}
pub async fn get_full_name_by_cid(&self, cid: u64) -> Result<Option<String>, AccountError> {
self.persistence_handler.get_full_name_by_cid(cid).await
}
pub async fn get_peer_info_from_cids(&self, cids: &[u64]) -> HashMap<u64, Option<PeerInfo>> {
let mut peer_info = HashMap::new();
let mut queue = FuturesOrdered::<
Pin<Box<dyn Future<Output = Result<Option<CNACMetadata>, AccountError>>>>,
>::new();
for cid in cids {
queue.push_back(Box::pin(self.persistence_handler.get_client_metadata(*cid)))
}
let mut results = futures::executor::block_on(queue.collect::<Vec<_>>());
let metadata: Vec<&Option<CNACMetadata>> = results
.iter_mut()
.map(|result| result.as_ref().unwrap_or(&None))
.collect();
let _: Vec<_> = cids
.iter()
.zip(metadata)
.map(|(&cid, user_data)| {
peer_info.insert(
cid,
user_data.as_ref().map(|some| PeerInfo {
cid: some.cid,
username: some.username.clone(),
full_name: some.full_name.clone(),
}),
)
})
.collect();
peer_info
}
pub async fn get_client_by_username<T: AsRef<str>>(
&self,
username: T,
) -> Result<Option<ClientNetworkAccount<R, Fcm>>, AccountError> {
self.persistence_handler
.get_client_by_username(username.as_ref())
.await
}
pub async fn purge(&self) -> Result<usize, AccountError> {
self.persistence_handler.purge().await
}
pub async fn register_hyperlan_p2p_at_endpoints<T: Into<String>>(
&self,
session_cid: u64,
peer_cid: u64,
adjacent_username: T,
) -> Result<(), AccountError> {
let adjacent_username = adjacent_username.into();
log::trace!(target: "citadel", "Registering {} ({}) to {} (local/endpoints)", &adjacent_username, peer_cid, session_cid);
self.persistence_handler
.register_p2p_as_client(session_cid, peer_cid, adjacent_username)
.await
}
pub async fn register_hyperlan_p2p_as_server(
&self,
cid0: u64,
cid1: u64,
) -> Result<(), AccountError> {
self.persistence_handler
.register_p2p_as_server(cid0, cid1)
.await
}
#[allow(unused_results)]
pub async fn delete_client_by_cid(&self, cid: u64) -> Result<(), AccountError> {
self.persistence_handler.delete_cnac_by_cid(cid).await
}
pub async fn get_hyperlan_peer_list(
&self,
session_cid: u64,
) -> Result<Option<Vec<u64>>, AccountError> {
self.persistence_handler
.get_hyperlan_peer_list(session_cid)
.await
}
pub async fn find_target_information(
&self,
implicated_user: impl Into<UserIdentifier>,
target_user: impl Into<UserIdentifier>,
) -> Result<Option<(u64, MutualPeer)>, AccountError> {
let session_cid = match implicated_user.into() {
UserIdentifier::ID(id) => id,
UserIdentifier::Username(uname) => {
self.get_persistence_handler().get_cid_by_username(&uname)
}
};
match target_user.into() {
UserIdentifier::ID(peer_cid) => Ok(self
.persistence_handler
.get_hyperlan_peer_by_cid(session_cid, peer_cid)
.await?
.map(|r| (session_cid, r))),
UserIdentifier::Username(uname) => Ok(self
.persistence_handler
.get_hyperlan_peer_by_username(session_cid, &uname)
.await?
.map(|r| (session_cid, r))),
}
}
pub async fn find_local_user_information(
&self,
implicated_user: impl Into<UserIdentifier>,
) -> Result<Option<u64>, AccountError> {
match implicated_user.into() {
UserIdentifier::ID(cid) => Ok(Some(cid)),
UserIdentifier::Username(username) => {
let cid = self.persistence_handler.get_cid_by_username(&username);
Ok(self
.persistence_handler
.get_client_metadata(cid)
.await?
.map(|r| r.cid))
}
}
}
pub async fn find_cnac_by_identifier(
&self,
implicated_user: impl Into<UserIdentifier>,
) -> Result<Option<ClientNetworkAccount<R, Fcm>>, AccountError> {
match implicated_user.into() {
UserIdentifier::ID(cid) => self.get_client_by_cid(cid).await,
UserIdentifier::Username(username) => self.get_client_by_username(username).await,
}
}
#[doc(hidden)]
pub fn get_persistence_handler(&self) -> &PersistenceHandler<R, Fcm> {
&self.persistence_handler
}
pub fn get_misc_settings(&self) -> &ServerMiscSettings {
&self.server_misc_settings
}
pub fn get_backend_type(&self) -> &BackendType {
&self.backend_ty
}
}