use crate::model::{macros, AppConfig, Config, ProxyUserCredentials, TargetUser}; use crate::repository::{backup_api_user_db_file, get_api_user_db_path, load_api_user, merge_api_user}; use crate::utils; use crate::utils::file_exists_async; use arc_swap::access::Access; use arc_swap::ArcSwap; use log::debug; use shared::model::{ApiProxyConfigDto, ApiProxyServerInfoDto, ConfigPaths, TargetUserDto}; use std::cmp::PartialEq; use std::io::ErrorKind; use std::sync::Arc; const API_USER: &str = "api"; const TEST_USER: &str = "test"; #[derive(Debug, Clone)] pub struct ApiProxyServerInfo { pub name: String, pub protocol: String, pub host: String, pub port: Option, pub timezone: String, pub message: String, pub path: Option, } macros::from_impl!(ApiProxyServerInfo); impl From<&ApiProxyServerInfoDto> for ApiProxyServerInfo { fn from(dto: &ApiProxyServerInfoDto) -> Self { Self { name: dto.name.clone(), protocol: dto.protocol.clone(), host: dto.host.clone(), port: dto.port.clone(), timezone: dto.timezone.clone(), message: dto.message.clone(), path: dto.path.clone(), } } } impl From<&ApiProxyServerInfo> for ApiProxyServerInfoDto { fn from(instance: &ApiProxyServerInfo) -> Self { Self { name: instance.name.clone(), protocol: instance.protocol.clone(), host: instance.host.clone(), port: instance.port.clone(), timezone: instance.timezone.clone(), message: instance.message.clone(), path: instance.path.clone(), } } } impl ApiProxyServerInfo { pub fn get_base_url(&self) -> String { let base_url = if let Some(port) = self.port.as_ref() { format!("{}://{}:{port}", self.protocol, self.host) } else { format!("{}://{}", self.protocol, self.host) }; match &self.path { None => base_url, Some(path) => format!("{base_url}/{}", path.trim_matches('/')), } } } #[derive(Debug, Clone, Default)] pub struct ApiProxyConfig { pub server: Vec, pub user: Vec, pub use_user_db: bool, /// HTTP status code for auth failures. 0 means default (403). pub auth_error_status: u16, } macros::from_impl!(ApiProxyConfig); impl From<&ApiProxyConfigDto> for ApiProxyConfig { fn from(dto: &ApiProxyConfigDto) -> Self { Self { server: dto.server.iter().map(ApiProxyServerInfo::from).collect(), user: dto.user.iter().map(TargetUser::from).collect(), use_user_db: dto.use_user_db, auth_error_status: dto.auth_error_status, } } } impl From<&ApiProxyConfig> for ApiProxyConfigDto { fn from(instance: &ApiProxyConfig) -> Self { Self { server: instance.server.iter().map(ApiProxyServerInfoDto::from).collect(), user: instance.user.iter().map(TargetUserDto::from).collect(), use_user_db: instance.use_user_db, auth_error_status: instance.auth_error_status, } } } fn serialize_api_proxy_config(config: &ApiProxyConfigDto) -> Result { let mut serialized = String::new(); let options = serde_saphyr::ser_options! {prefer_block_scalars: false}; serde_saphyr::to_fmt_writer_with_options(&mut serialized, config, options) .map_err(|err| format!("Could not serialize api proxy config: {err}"))?; Ok(serialized) } async fn api_proxy_file_would_change(api_proxy_file: &str, config: &ApiProxyConfigDto) -> Result { let serialized = serialize_api_proxy_config(config)?; match tokio::fs::read_to_string(api_proxy_file).await { Ok(existing) => Ok(existing != serialized), Err(err) if err.kind() == ErrorKind::NotFound => Ok(true), Err(err) => Err(format!("Could not read api proxy file {api_proxy_file}: {err}")), } } impl ApiProxyConfig { async fn backfill_output_clusters_to_file(&self, cfg: &AppConfig, errors: &mut Vec) { if self.user.is_empty() { return; } let paths = > as Access>::load(&cfg.paths); let api_proxy_file = paths.api_proxy_file_path.as_str(); let dto = ApiProxyConfigDto::from(self); match api_proxy_file_would_change(api_proxy_file, &dto).await { Ok(true) => {} Ok(false) => return, Err(err) => { errors.push(err); return; } } let config = > as Access>::load(&cfg.config); let backup_dir = config.get_backup_dir(); if let Err(err) = utils::save_api_proxy(api_proxy_file, backup_dir.as_ref(), &dto).await { errors.push(format!("Error saving api proxy file: {err}")); } } // we have the option to store user in the config file or in the user_db // When we switch from one to other we need to migrate the existing data. /// # Panics pub async fn migrate_api_user(&mut self, cfg: &AppConfig, errors: &mut Vec) { let paths = > as Access>::load(&cfg.paths); let api_proxy_file = paths.api_proxy_file_path.as_str(); if self.use_user_db { // we have user defined in config file. // we migrate them to the db and delete them from the config file if !&self.user.is_empty() { if let Err(err) = merge_api_user(cfg, &self.user).await { errors.push(err.to_string()); } else { let config = > as Access>::load(&cfg.config); let backup_dir = config.get_backup_dir(); self.user = vec![]; if let Err(err) = utils::save_api_proxy(api_proxy_file, backup_dir.as_ref(), &ApiProxyConfigDto::from(&*self)) .await { errors.push(format!("Error saving api proxy file: {err}")); } } } match load_api_user(cfg).await { Ok(users) => { self.user = users; } Err(err) => { println!("{err}"); errors.push(err.to_string()); } } } else { self.backfill_output_clusters_to_file(cfg, errors).await; let user_db_path = get_api_user_db_path(cfg); if file_exists_async(&user_db_path).await { // we can't have user defined in db file. // we need to load them and save them into the config file if let Ok(stored_users) = load_api_user(cfg).await { for stored_user in stored_users { if let Some(target_user) = self.user.iter_mut().find(|t| t.target == stored_user.target) { for stored_credential in &stored_user.credentials { if !target_user.credentials.iter().any(|c| c.username == stored_credential.username) { target_user.credentials.push(stored_credential.clone()); } } } else { self.user.push(stored_user); } } } let config = > as Access>::load(&cfg.config); let backup_dir = config.get_backup_dir(); let dto = ApiProxyConfigDto::from(&*self); match api_proxy_file_would_change(api_proxy_file, &dto).await { Ok(true) => { if let Err(err) = utils::save_api_proxy(api_proxy_file, backup_dir.as_ref(), &dto).await { errors.push(format!("Error saving api proxy file: {err}")); } else { backup_api_user_db_file(cfg, &user_db_path).await; let _ = tokio::fs::remove_file(&user_db_path).await; } } Ok(false) => { backup_api_user_db_file(cfg, &user_db_path).await; let _ = tokio::fs::remove_file(&user_db_path).await; } Err(err) => errors.push(err), } } } } pub fn get_target_name(&self, username: &str, password: &str) -> Option<(Arc, String)> { for target_user in &self.user { if let Some((credentials, target_name)) = target_user.get_target_name(username, password) { return Some((Arc::clone(&credentials), target_name.to_string())); } } if log::log_enabled!(log::Level::Debug) && !username.eq(API_USER) { debug!("Could not find any target for user {username}"); } None } pub fn get_target_name_by_token(&self, token: &str) -> Option<(Arc, String)> { for target_user in &self.user { if let Some((credentials, target_name)) = target_user.get_target_name_by_token(token) { return Some((Arc::clone(&credentials), target_name.to_string())); } } None } pub fn get_user_credentials(&self, username: &str) -> Option> { let result = self .user .iter() .flat_map(|target_user| target_user.credentials.iter()) .find(|credential| credential.username == username) .map(Arc::clone); if result.is_none() && (username != TEST_USER && username != API_USER) { debug!("Could not find any user credentials for: {username}"); } result } }