use std::collections::HashMap; use std::net::IpAddr; use crate::actors::users_actor::AuthorizedAuthenticationSources; use crate::constants::SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN; use crate::data::client::{Client, ClientID}; use crate::data::login_redirect::LoginRedirect; use crate::data::provider::{Provider, ProviderID}; use crate::data::totp_key::TotpKey; use crate::data::webauthn_manager::WebauthnPubKey; use crate::utils::time::{fmt_time, time}; #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct UserID(pub String); #[derive(Debug, Clone)] pub struct GeneralSettings { pub uid: UserID, pub username: String, pub first_name: String, pub last_name: String, pub email: String, pub enabled: bool, pub two_factor_exemption_after_successful_login: bool, pub is_admin: bool, } #[derive(Eq, PartialEq, Clone, Debug)] pub enum GrantedClients { AllClients, SomeClients(Vec), NoClient, } impl GrantedClients { pub fn to_user(self) -> Option> { match self { GrantedClients::AllClients => None, GrantedClients::SomeClients(users) => Some(users), GrantedClients::NoClient => Some(vec![]), } } } #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)] pub struct FactorID(pub String); #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum TwoFactorType { TOTP(TotpKey), WEBAUTHN(Box), } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct TwoFactor { pub id: FactorID, pub name: String, pub kind: TwoFactorType, } impl TwoFactor { pub fn quick_description(&self) -> String { format!( "#{} of type {} and name '{}'", self.id.0, self.type_str(), self.name ) } pub fn type_str(&self) -> &'static str { match self.kind { TwoFactorType::TOTP(_) => "Authenticator app", TwoFactorType::WEBAUTHN(_) => "Security key", } } pub fn description_str(&self) -> &'static str { match self.kind { TwoFactorType::TOTP(_) => "Login by entering an OTP code", TwoFactorType::WEBAUTHN(_) => "Login using a security key", } } pub fn type_image(&self) -> &'static str { match self.kind { TwoFactorType::TOTP(_) => "/assets/img/pin.svg", TwoFactorType::WEBAUTHN(_) => "/assets/img/key.svg", } } pub fn login_url(&self, redirect_uri: &LoginRedirect) -> String { match self.kind { TwoFactorType::TOTP(_) => format!("/2fa_otp?redirect={}", redirect_uri.get_encoded()), TwoFactorType::WEBAUTHN(_) => { format!("/2fa_webauthn?redirect={}", redirect_uri.get_encoded()) } } } pub fn is_webauthn(&self) -> bool { matches!(self.kind, TwoFactorType::WEBAUTHN(_)) } } #[derive(Debug)] pub struct Successful2FALogin { pub ip: IpAddr, pub time: u64, pub can_bypass_2fa: bool, } impl Successful2FALogin { pub fn fmt_time(&self) -> String { fmt_time(self.time) } } fn default_true() -> bool { true } #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct User { pub uid: UserID, pub first_name: String, pub last_name: String, pub username: String, pub email: String, pub password: String, pub need_reset_password: bool, pub enabled: bool, pub admin: bool, /// 2FA #[serde(default)] pub two_factor: Vec, /// Exempt the user from validating a second factor after a previous successful authentication /// for a defined amount of time #[serde(default)] pub two_factor_exemption_after_successful_login: bool, /// IP addresses of last successful logins #[serde(default)] pub last_successful_2fa: HashMap, /// None = all services /// Some([]) = no service pub authorized_clients: Option>, /// Authorize connection through local login #[serde(default = "default_true")] pub allow_local_login: bool, /// Allowed third party providers #[serde(default)] pub allow_login_from_providers: Vec, } impl User { pub fn full_name(&self) -> String { format!("{} {}", self.first_name, self.last_name) } pub fn quick_identity(&self) -> String { format!( "{} {} {} ({:?})", match self.admin { true => "admin", false => "user", }, self.username, self.email, self.uid ) } /// Get the list of sources from which a user can authenticate from pub fn authorized_authentication_sources(&self) -> AuthorizedAuthenticationSources { AuthorizedAuthenticationSources { local: self.allow_local_login, upstream: self.allow_login_from_providers.clone(), } } /// Check if a user can authenticate using a givne provider or not pub fn can_login_from_provider(&self, provider: &Provider) -> bool { self.allow_login_from_providers.contains(&provider.id) } pub fn granted_clients(&self) -> GrantedClients { match self.authorized_clients.as_deref() { None => GrantedClients::AllClients, Some(&[]) => GrantedClients::NoClient, Some(clients) => GrantedClients::SomeClients(clients.to_vec()), } } pub fn can_access_app(&self, client: &Client) -> bool { if client.granted_to_all_users { return true; } match self.granted_clients() { GrantedClients::AllClients => true, GrantedClients::SomeClients(c) => c.contains(&client.id), GrantedClients::NoClient => false, } } pub fn has_two_factor(&self) -> bool { !self.two_factor.is_empty() } pub fn can_bypass_two_factors_for_ip(&self, ip: IpAddr) -> bool { self.two_factor_exemption_after_successful_login && self.last_successful_2fa.get(&ip).unwrap_or(&0) + SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN > time() } pub fn update_general_settings(&mut self, settings: GeneralSettings) { self.username = settings.username; self.first_name = settings.first_name; self.last_name = settings.last_name; self.email = settings.email; self.enabled = settings.enabled; self.two_factor_exemption_after_successful_login = settings.two_factor_exemption_after_successful_login; self.admin = settings.is_admin; } pub fn add_factor(&mut self, factor: TwoFactor) { self.two_factor.push(factor); } pub fn find_factor(&self, factor_id: &FactorID) -> Option<&TwoFactor> { self.two_factor.iter().find(|f| f.id.eq(factor_id)) } pub fn has_webauthn_factor(&self) -> bool { self.two_factor.iter().any(TwoFactor::is_webauthn) } /// Get all registered OTP registered passwords pub fn get_otp_factors(&self) -> Vec { self.two_factor.iter().fold(vec![], |mut acc, factor| { if let TwoFactorType::TOTP(key) = &factor.kind { acc.push(key.clone()) } acc }) } /// Get all registered 2FA webauthn public keys pub fn get_webauthn_pub_keys(&self) -> Vec { self.two_factor.iter().fold(vec![], |mut acc, factor| { if let TwoFactorType::WEBAUTHN(key) = &factor.kind { acc.push(*key.clone()) } acc }) } /// Get the first factor of each kind of factors pub fn get_distinct_factors_types(&self) -> Vec<&TwoFactor> { let mut urls = vec![]; self.two_factor .iter() .filter(|f| { if urls.contains(&f.type_str()) { false } else { urls.push(f.type_str()); true } }) .collect::>() } pub fn remove_outdated_successful_2fa_attempts(&mut self) { self.last_successful_2fa .retain(|_, t| *t + SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN > time()); } pub fn get_formatted_2fa_successful_logins(&self) -> Vec { self.last_successful_2fa .iter() .map(|(ip, time)| Successful2FALogin { ip: *ip, time: *time, can_bypass_2fa: self.can_bypass_two_factors_for_ip(*ip), }) .collect::>() } } impl PartialEq for User { fn eq(&self, other: &Self) -> bool { self.uid.eq(&other.uid) } } impl Eq for User {} impl Default for User { fn default() -> Self { Self { uid: UserID("".to_string()), first_name: "".to_string(), last_name: "".to_string(), username: "".to_string(), email: "".to_string(), password: "".to_string(), need_reset_password: false, enabled: true, admin: false, two_factor: vec![], two_factor_exemption_after_successful_login: false, last_successful_2fa: Default::default(), authorized_clients: Some(Vec::new()), allow_local_login: true, allow_login_from_providers: vec![], } } }