use std::future::Future; use std::net::IpAddr; use std::pin::Pin; use actix::Addr; use actix_identity::Identity; use actix_remote_ip::RemoteIP; use actix_web::dev::Payload; use actix_web::{Error, FromRequest, HttpRequest, web}; use crate::actors::providers_states_actor::ProviderLoginState; use crate::actors::users_actor; use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor}; use crate::data::app_config::{ActionLoggerFormat, AppConfig}; use crate::data::client::ClientID; use crate::data::provider::{Provider, ProviderID}; use crate::data::session_identity::SessionIdentity; use crate::data::user::{FactorID, GrantedClients, TwoFactor, TwoFactorType, User, UserID}; use crate::utils::time::time; #[derive(serde::Serialize)] pub struct LoggableUser { pub uid: UserID, pub username: String, pub email: String, pub admin: bool, } impl LoggableUser { pub fn quick_identity(&self) -> String { format!( "{} {} {} ({:?})", match self.admin { true => "admin", false => "user", }, self.username, self.email, self.uid ) } } impl User { pub fn loggable(&self) -> LoggableUser { LoggableUser { uid: self.uid.clone(), username: self.username.clone(), email: self.email.clone(), admin: self.admin, } } } #[derive(Debug, serde::Serialize)] pub enum LoggableFactorType { TOTP, WEBAUTHN, } #[derive(serde::Serialize)] pub struct LoggableFactor { pub id: FactorID, pub name: String, pub kind: LoggableFactorType, } impl LoggableFactor { pub fn quick_description(&self) -> String { format!( "#{} of type {:?} and name '{}'", self.id.0, self.kind, self.name ) } } impl TwoFactor { pub fn loggable(&self) -> LoggableFactor { LoggableFactor { id: self.id.clone(), name: self.name.to_string(), kind: match self.kind { TwoFactorType::TOTP(_) => LoggableFactorType::TOTP, TwoFactorType::WEBAUTHN(_) => LoggableFactorType::WEBAUTHN, }, } } } #[derive(serde::Serialize)] #[serde(tag = "type")] pub enum Action<'a> { AdminCreateUser { user: LoggableUser, }, AdminUpdateUser { user: LoggableUser, }, AdminDeleteUser { user: LoggableUser, }, AdminResetUserPassword { user: LoggableUser, }, AdminRemoveUserFactor { user: LoggableUser, factor: LoggableFactor, }, AdminSetAuthorizedAuthenticationSources { user: LoggableUser, sources: &'a AuthorizedAuthenticationSources, }, AdminSetNewGrantedClientsList { user: LoggableUser, clients: &'a GrantedClients, }, AdminClear2FAHistory { user: LoggableUser, }, LoginWebauthnAttempt { success: bool, user_id: UserID, }, StartLoginAttemptWithOpenIDProvider { provider_id: &'a ProviderID, state: &'a str, }, ProviderError { message: &'a str, }, ProviderCBInvalidState { state: &'a str, }, ProviderRateLimited, ProviderFailedGetToken { state: &'a ProviderLoginState, code: &'a str, }, ProviderFailedGetUserInfo { provider: &'a Provider, }, ProviderEmailNotValidated { provider: &'a Provider, }, ProviderMissingEmailInResponse { provider: &'a Provider, }, ProviderAccountNotFound { provider: &'a Provider, email: &'a str, }, ProviderAccountAutoCreated { provider: &'a Provider, user: LoggableUser, }, ProviderAccountDisabled { provider: &'a Provider, email: &'a str, }, ProviderAccountNotAllowedToLoginWithProvider { provider: &'a Provider, email: &'a str, }, ProviderLoginFailed { provider: &'a Provider, email: &'a str, }, ProviderLoginSuccessful { provider: &'a Provider, user: LoggableUser, }, SignOut, UserNeed2FAOnLogin { user: LoggableUser, }, UserSuccessfullyAuthenticated { user: LoggableUser, }, UserNeedNewPasswordOnLogin { user: LoggableUser, }, TryLoginWithDisabledAccount { login: &'a str, }, TryLocalLoginFromUnauthorizedAccount { login: &'a str, }, FailedLoginWithBadCredentials { login: &'a str, }, UserChangedPasswordOnLogin { user_id: &'a UserID, }, OTPLoginAttempt { user: LoggableUser, success: bool, }, NewOpenIDSession { client: &'a ClientID, }, NewOpenIDSuccessfulImplicitAuth { client: &'a ClientID, }, ChangedHisPassword, ClearedHisLoginHistory, AddNewFactor { factor: LoggableFactor, }, Removed2FAFactor { factor_id: &'a FactorID, }, } impl Action<'_> { pub fn as_string(&self) -> String { match self { Action::AdminDeleteUser { user } => { format!("deleted account of {}", user.quick_identity()) } Action::AdminCreateUser { user } => { format!("created account of {}", user.quick_identity()) } Action::AdminUpdateUser { user } => { format!("updated account of {}", user.quick_identity()) } Action::AdminResetUserPassword { user } => { format!( "set a temporary password for the account of {}", user.quick_identity() ) } Action::AdminRemoveUserFactor { user, factor } => format!( "removed 2 factor ({}) of user ({})", factor.quick_description(), user.quick_identity() ), Action::AdminClear2FAHistory { user } => { format!("cleared 2FA history of {}", user.quick_identity()) } Action::AdminSetAuthorizedAuthenticationSources { user, sources } => format!( "update authorized authentication sources ({:?}) for user ({})", sources, user.quick_identity() ), Action::AdminSetNewGrantedClientsList { user, clients } => format!( "set new granted clients list ({:?}) for user ({})", clients, user.quick_identity() ), Action::LoginWebauthnAttempt { success, user_id } => match success { true => format!("successfully performed webauthn attempt for user {user_id:?}"), false => format!("performed FAILED webauthn attempt for user {user_id:?}"), }, Action::StartLoginAttemptWithOpenIDProvider { provider_id, state } => format!( "started new authentication attempt through an OpenID provider (prov={} / state={state})", provider_id.0 ), Action::ProviderError { message } => { format!("failed provider authentication with message '{message}'") } Action::ProviderCBInvalidState { state } => { format!("provided invalid callback state after provider authentication: '{state}'") } Action::ProviderRateLimited => { "could not complete OpenID login because it has reached failed attempts rate limit!" .to_string() } Action::ProviderFailedGetToken { state, code } => format!( "could not complete login from provider because the id_token could not be retrieved! (state={state:?} code = {code})" ), Action::ProviderFailedGetUserInfo { provider } => format!( "could not get user information from userinfo endpoint of provider {}!", provider.id.0 ), Action::ProviderEmailNotValidated { provider } => format!( "could not login using provider {} because its email was marked as not validated!", provider.id.0 ), Action::ProviderMissingEmailInResponse { provider } => format!( "could not login using provider {} because the email was not provided by userinfo endpoint!", provider.id.0 ), Action::ProviderAccountNotFound { provider, email } => format!( "could not login using provider {} because the email {email} could not be associated to any account!", &provider.id.0 ), Action::ProviderAccountAutoCreated { provider, user } => format!( "triggered automatic account creation for {} from provider {} because it was not found in local accounts list!", user.quick_identity(), &provider.id.0 ), Action::ProviderAccountDisabled { provider, email } => format!( "could not login using provider {} because the account associated to the email {email} is disabled!", &provider.id.0 ), Action::ProviderAccountNotAllowedToLoginWithProvider { provider, email } => format!( "could not login using provider {} because the account associated to the email {email} is not allowed to authenticate using this provider!", &provider.id.0 ), Action::ProviderLoginFailed { provider, email } => format!( "could not login using provider {} with the email {email} for an unknown reason!", &provider.id.0 ), Action::ProviderLoginSuccessful { provider, user } => format!( "successfully authenticated using provider {} as {}", provider.id.0, user.quick_identity() ), Action::SignOut => "signed out".to_string(), Action::UserNeed2FAOnLogin { user } => { format!( "successfully authenticated as user {:?} but need to do 2FA authentication", user.quick_identity() ) } Action::UserSuccessfullyAuthenticated { user } => { format!("successfully authenticated as {}", user.quick_identity()) } Action::UserNeedNewPasswordOnLogin { user } => format!( "successfully authenticated as {}, but need to set a new password", user.quick_identity() ), Action::TryLoginWithDisabledAccount { login } => { format!("successfully authenticated as {login}, but this is a DISABLED ACCOUNT") } Action::TryLocalLoginFromUnauthorizedAccount { login } => { format!( "successfully locally authenticated as {login}, but this is a FORBIDDEN for this account!" ) } Action::FailedLoginWithBadCredentials { login } => { format!("attempted to authenticate as {login} but with a WRONG PASSWORD") } Action::UserChangedPasswordOnLogin { user_id } => { format!("set a new password at login as user {user_id:?}") } Action::OTPLoginAttempt { user, success } => match success { true => format!( "successfully performed OTP attempt for user {}", user.quick_identity() ), false => format!( "performed FAILED OTP attempt for user {}", user.quick_identity() ), }, Action::NewOpenIDSession { client } => { format!("opened a new OpenID session with {:?}", client) } Action::NewOpenIDSuccessfulImplicitAuth { client } => format!( "finished an implicit flow connection for client {:?}", client ), Action::ChangedHisPassword => "changed his password".to_string(), Action::ClearedHisLoginHistory => "cleared his login history".to_string(), Action::AddNewFactor { factor } => format!( "added a new factor to his account : {}", factor.quick_description(), ), Action::Removed2FAFactor { factor_id } => format!("Removed his factor {factor_id:?}"), } } } #[derive(serde::Serialize)] struct JsonActionData<'a> { time: u64, ip: IpAddr, user: Option, #[serde(flatten)] action: Action<'a>, } pub struct ActionLogger { ip: IpAddr, user: Option, } impl ActionLogger { pub fn log(&self, action: Action) { match AppConfig::get().action_logger_format { ActionLoggerFormat::Text => log::info!( "{} from {} has {}", match &self.user { None => "Anonymous user".to_string(), Some(u) => u.loggable().quick_identity(), }, self.ip, action.as_string() ), ActionLoggerFormat::Json => match serde_json::to_string(&JsonActionData { time: time(), ip: self.ip, user: self.user.as_ref().map(User::loggable), action, }) { Ok(j) => println!("{j}"), Err(e) => log::error!("Failed to serialize event to JSON! {e}"), }, ActionLoggerFormat::None => {} } } } impl FromRequest for ActionLogger { type Error = Error; type Future = Pin>>>; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let req = req.clone(); Box::pin(async move { let user_actor: &web::Data> = req.app_data().expect("UserActor undefined!"); let user_actor: Addr = user_actor.as_ref().clone(); let user_id = Identity::from_request(&req, &mut Payload::None) .into_inner() .ok() .and_then(|id| { let sess = SessionIdentity(Some(&id)); match sess.is_authenticated() { true => Some(sess.user_id()), false => None, } }); Ok(Self { ip: RemoteIP::from_request(&req, &mut Payload::None) .await .unwrap() .0, user: match user_id { None => None, Some(u) => { user_actor .send(users_actor::GetUserRequest(u)) .await .unwrap() .0 } }, }) }) } }