338 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			338 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
| use bincode::{Decode, Encode};
 | |
| 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, Encode, Decode)]
 | |
| 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<ClientID>),
 | |
|     NoClient,
 | |
| }
 | |
| 
 | |
| impl GrantedClients {
 | |
|     pub fn to_user(self) -> Option<Vec<ClientID>> {
 | |
|         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<WebauthnPubKey>),
 | |
| }
 | |
| 
 | |
| #[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, force_2fa: bool) -> String {
 | |
|         match self.kind {
 | |
|             TwoFactorType::TOTP(_) => format!(
 | |
|                 "/2fa_otp?redirect={}&force_2fa={force_2fa}",
 | |
|                 redirect_uri.get_encoded()
 | |
|             ),
 | |
|             TwoFactorType::WEBAUTHN(_) => {
 | |
|                 format!(
 | |
|                     "/2fa_webauthn?redirect={}&force_2fa={force_2fa}",
 | |
|                     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<TwoFactor>,
 | |
| 
 | |
|     /// 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<IpAddr, u64>,
 | |
| 
 | |
|     /// None = all services
 | |
|     /// Some([]) = no service
 | |
|     pub authorized_clients: Option<Vec<ClientID>>,
 | |
| 
 | |
|     /// 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<ProviderID>,
 | |
| }
 | |
| 
 | |
| 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<TotpKey> {
 | |
|         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<WebauthnPubKey> {
 | |
|         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::<Vec<_>>()
 | |
|     }
 | |
| 
 | |
|     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<Successful2FALogin> {
 | |
|         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::<Vec<_>>()
 | |
|     }
 | |
| }
 | |
| 
 | |
| 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(uuid::Uuid::new_v4().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![],
 | |
|         }
 | |
|     }
 | |
| }
 |