Log all user actions on stdout
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		@@ -19,6 +19,9 @@ pub const MAX_INACTIVITY_DURATION: u64 = 60 * 30;
 | 
				
			|||||||
/// Maximum session duration (6 hours)
 | 
					/// Maximum session duration (6 hours)
 | 
				
			||||||
pub const MAX_SESSION_DURATION: u64 = 3600 * 6;
 | 
					pub const MAX_SESSION_DURATION: u64 = 3600 * 6;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Maximum length of a second factor name
 | 
				
			||||||
 | 
					pub const MAX_SECOND_FACTOR_NAME_LEN: usize = 25;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// When the user successfully authenticate using 2FA, period of time during which the user is
 | 
					/// When the user successfully authenticate using 2FA, period of time during which the user is
 | 
				
			||||||
/// exempted from this IP address to use 2FA
 | 
					/// exempted from this IP address to use 2FA
 | 
				
			||||||
pub const SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN: u64 = 7 * 24 * 3600;
 | 
					pub const SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN: u64 = 7 * 24 * 3600;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,9 @@
 | 
				
			|||||||
 | 
					use crate::actors::users_actor;
 | 
				
			||||||
use actix::Addr;
 | 
					use actix::Addr;
 | 
				
			||||||
use actix_web::{web, HttpResponse, Responder};
 | 
					use actix_web::{web, HttpResponse, Responder};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::actors::users_actor::{DeleteUserRequest, FindUserByUsername, UsersActor};
 | 
					use crate::actors::users_actor::{DeleteUserRequest, FindUserByUsername, UsersActor};
 | 
				
			||||||
 | 
					use crate::data::action_logger::{Action, ActionLogger};
 | 
				
			||||||
use crate::data::current_user::CurrentUser;
 | 
					use crate::data::current_user::CurrentUser;
 | 
				
			||||||
use crate::data::user::UserID;
 | 
					use crate::data::user::UserID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -37,13 +39,25 @@ pub async fn delete_user(
 | 
				
			|||||||
    user: CurrentUser,
 | 
					    user: CurrentUser,
 | 
				
			||||||
    req: web::Form<DeleteUserReq>,
 | 
					    req: web::Form<DeleteUserReq>,
 | 
				
			||||||
    users: web::Data<Addr<UsersActor>>,
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
 | 
					    action_logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    if user.uid == req.user_id {
 | 
					    if user.uid == req.user_id {
 | 
				
			||||||
        return HttpResponse::BadRequest().body("You can not remove your own account!");
 | 
					        return HttpResponse::BadRequest().body("You can not remove your own account!");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let user = match users
 | 
				
			||||||
 | 
					        .send(users_actor::GetUserRequest(req.user_id.clone()))
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					        .0
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        None => return HttpResponse::NotFound().body("Could not find a user to remove!"),
 | 
				
			||||||
 | 
					        Some(u) => u,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let res = users.send(DeleteUserRequest(req.0.user_id)).await.unwrap();
 | 
					    let res = users.send(DeleteUserRequest(req.0.user_id)).await.unwrap();
 | 
				
			||||||
    if res.0 {
 | 
					    if res.0 {
 | 
				
			||||||
 | 
					        action_logger.log(Action::AdminDeleteUser(&user));
 | 
				
			||||||
        HttpResponse::Ok().finish()
 | 
					        HttpResponse::Ok().finish()
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        HttpResponse::InternalServerError().finish()
 | 
					        HttpResponse::InternalServerError().finish()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ use crate::actors::users_actor;
 | 
				
			|||||||
use crate::actors::users_actor::UsersActor;
 | 
					use crate::actors::users_actor::UsersActor;
 | 
				
			||||||
use crate::constants::TEMPORARY_PASSWORDS_LEN;
 | 
					use crate::constants::TEMPORARY_PASSWORDS_LEN;
 | 
				
			||||||
use crate::controllers::settings_controller::BaseSettingsPage;
 | 
					use crate::controllers::settings_controller::BaseSettingsPage;
 | 
				
			||||||
 | 
					use crate::data::action_logger::{Action, ActionLogger};
 | 
				
			||||||
use crate::data::client::{Client, ClientID, ClientManager};
 | 
					use crate::data::client::{Client, ClientID, ClientManager};
 | 
				
			||||||
use crate::data::current_user::CurrentUser;
 | 
					use crate::data::current_user::CurrentUser;
 | 
				
			||||||
use crate::data::user::{hash_password, User, UserID};
 | 
					use crate::data::user::{hash_password, User, UserID};
 | 
				
			||||||
@@ -67,6 +68,7 @@ pub async fn users_route(
 | 
				
			|||||||
    user: CurrentUser,
 | 
					    user: CurrentUser,
 | 
				
			||||||
    users: web::Data<Addr<UsersActor>>,
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
    update_query: Option<web::Form<UpdateUserQuery>>,
 | 
					    update_query: Option<web::Form<UpdateUserQuery>>,
 | 
				
			||||||
 | 
					    logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    let mut danger = None;
 | 
					    let mut danger = None;
 | 
				
			||||||
    let mut success = None;
 | 
					    let mut success = None;
 | 
				
			||||||
@@ -112,6 +114,8 @@ pub async fn users_route(
 | 
				
			|||||||
        let new_password = match update.0.gen_new_password.is_some() {
 | 
					        let new_password = match update.0.gen_new_password.is_some() {
 | 
				
			||||||
            false => None,
 | 
					            false => None,
 | 
				
			||||||
            true => {
 | 
					            true => {
 | 
				
			||||||
 | 
					                logger.log(Action::AdminResetUserPassword(&user));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN);
 | 
					                let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN);
 | 
				
			||||||
                user.password = hash_password(&temp_pass).expect("Failed to hash password");
 | 
					                user.password = hash_password(&temp_pass).expect("Failed to hash password");
 | 
				
			||||||
                user.need_reset_password = true;
 | 
					                user.need_reset_password = true;
 | 
				
			||||||
@@ -121,6 +125,7 @@ pub async fn users_route(
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if update.0.clear_2fa_history.is_some() {
 | 
					        if update.0.clear_2fa_history.is_some() {
 | 
				
			||||||
 | 
					            logger.log(Action::AdminClear2FAHistory(&user));
 | 
				
			||||||
            user.last_successful_2fa = Default::default();
 | 
					            user.last_successful_2fa = Default::default();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -140,8 +145,14 @@ pub async fn users_route(
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            success = Some(match is_creating {
 | 
					            success = Some(match is_creating {
 | 
				
			||||||
                true => format!("User {} was successfully created!", user.full_name()),
 | 
					                true => {
 | 
				
			||||||
                false => format!("User {} was successfully updated!", user.full_name()),
 | 
					                    logger.log(Action::AdminCreateUser(&user));
 | 
				
			||||||
 | 
					                    format!("User {} was successfully created!", user.full_name())
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                false => {
 | 
				
			||||||
 | 
					                    logger.log(Action::AdminUpdateUser(&user));
 | 
				
			||||||
 | 
					                    format!("User {} was successfully updated!", user.full_name())
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if let Some(pass) = new_password {
 | 
					            if let Some(pass) = new_password {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
use crate::actors::users_actor;
 | 
					use crate::actors::users_actor;
 | 
				
			||||||
use crate::actors::users_actor::UsersActor;
 | 
					use crate::actors::users_actor::UsersActor;
 | 
				
			||||||
 | 
					use crate::data::action_logger::{Action, ActionLogger};
 | 
				
			||||||
use crate::data::remote_ip::RemoteIP;
 | 
					use crate::data::remote_ip::RemoteIP;
 | 
				
			||||||
use actix::Addr;
 | 
					use actix::Addr;
 | 
				
			||||||
use actix_identity::Identity;
 | 
					use actix_identity::Identity;
 | 
				
			||||||
@@ -22,6 +23,7 @@ pub async fn auth_webauthn(
 | 
				
			|||||||
    http_req: HttpRequest,
 | 
					    http_req: HttpRequest,
 | 
				
			||||||
    remote_ip: RemoteIP,
 | 
					    remote_ip: RemoteIP,
 | 
				
			||||||
    users: web::Data<Addr<UsersActor>>,
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
 | 
					    logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    if !SessionIdentity(Some(&id)).need_2fa_auth() {
 | 
					    if !SessionIdentity(Some(&id)).need_2fa_auth() {
 | 
				
			||||||
        return HttpResponse::Unauthorized().json("No 2FA required!");
 | 
					        return HttpResponse::Unauthorized().json("No 2FA required!");
 | 
				
			||||||
@@ -32,15 +34,26 @@ pub async fn auth_webauthn(
 | 
				
			|||||||
    match manager.finish_authentication(&user_id, &req.opaque_state, &req.credential) {
 | 
					    match manager.finish_authentication(&user_id, &req.opaque_state, &req.credential) {
 | 
				
			||||||
        Ok(_) => {
 | 
					        Ok(_) => {
 | 
				
			||||||
            users
 | 
					            users
 | 
				
			||||||
                .send(users_actor::AddSuccessful2FALogin(user_id, remote_ip.0))
 | 
					                .send(users_actor::AddSuccessful2FALogin(
 | 
				
			||||||
 | 
					                    user_id.clone(),
 | 
				
			||||||
 | 
					                    remote_ip.0,
 | 
				
			||||||
 | 
					                ))
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .unwrap();
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            SessionIdentity(Some(&id)).set_status(&http_req, SessionStatus::SignedIn);
 | 
					            SessionIdentity(Some(&id)).set_status(&http_req, SessionStatus::SignedIn);
 | 
				
			||||||
 | 
					            logger.log(Action::LoginWebauthnAttempt {
 | 
				
			||||||
 | 
					                success: true,
 | 
				
			||||||
 | 
					                user_id,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
            HttpResponse::Ok().body("You are authenticated!")
 | 
					            HttpResponse::Ok().body("You are authenticated!")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
            log::error!("Failed to authenticate user using webauthn! {:?}", e);
 | 
					            log::error!("Failed to authenticate user using webauthn! {:?}", e);
 | 
				
			||||||
 | 
					            logger.log(Action::LoginWebauthnAttempt {
 | 
				
			||||||
 | 
					                success: false,
 | 
				
			||||||
 | 
					                user_id,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
            HttpResponse::InternalServerError().body("Failed to validate security key!")
 | 
					            HttpResponse::InternalServerError().body("Failed to validate security key!")
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
 | 
				
			|||||||
use crate::controllers::base_controller::{
 | 
					use crate::controllers::base_controller::{
 | 
				
			||||||
    build_fatal_error_page, redirect_user, redirect_user_for_login,
 | 
					    build_fatal_error_page, redirect_user, redirect_user_for_login,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					use crate::data::action_logger::{Action, ActionLogger};
 | 
				
			||||||
use crate::data::login_redirect::LoginRedirect;
 | 
					use crate::data::login_redirect::LoginRedirect;
 | 
				
			||||||
use crate::data::remote_ip::RemoteIP;
 | 
					use crate::data::remote_ip::RemoteIP;
 | 
				
			||||||
use crate::data::session_identity::{SessionIdentity, SessionStatus};
 | 
					use crate::data::session_identity::{SessionIdentity, SessionStatus};
 | 
				
			||||||
@@ -73,6 +74,7 @@ pub struct LoginRequestQuery {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Authenticate user
 | 
					/// Authenticate user
 | 
				
			||||||
 | 
					#[allow(clippy::too_many_arguments)]
 | 
				
			||||||
pub async fn login_route(
 | 
					pub async fn login_route(
 | 
				
			||||||
    remote_ip: RemoteIP,
 | 
					    remote_ip: RemoteIP,
 | 
				
			||||||
    users: web::Data<Addr<UsersActor>>,
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
@@ -81,6 +83,7 @@ pub async fn login_route(
 | 
				
			|||||||
    req: Option<web::Form<LoginRequestBody>>,
 | 
					    req: Option<web::Form<LoginRequestBody>>,
 | 
				
			||||||
    id: Option<Identity>,
 | 
					    id: Option<Identity>,
 | 
				
			||||||
    http_req: HttpRequest,
 | 
					    http_req: HttpRequest,
 | 
				
			||||||
 | 
					    logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    let mut danger = None;
 | 
					    let mut danger = None;
 | 
				
			||||||
    let mut success = None;
 | 
					    let mut success = None;
 | 
				
			||||||
@@ -102,6 +105,7 @@ pub async fn login_route(
 | 
				
			|||||||
    // Check if user session must be closed
 | 
					    // Check if user session must be closed
 | 
				
			||||||
    if let Some(true) = query.logout {
 | 
					    if let Some(true) = query.logout {
 | 
				
			||||||
        if let Some(id) = id {
 | 
					        if let Some(id) = id {
 | 
				
			||||||
 | 
					            logger.log(Action::Signout);
 | 
				
			||||||
            id.logout();
 | 
					            id.logout();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        success = Some("Goodbye!".to_string());
 | 
					        success = Some("Goodbye!".to_string());
 | 
				
			||||||
@@ -138,11 +142,14 @@ pub async fn login_route(
 | 
				
			|||||||
        match response {
 | 
					        match response {
 | 
				
			||||||
            LoginResult::Success(user) => {
 | 
					            LoginResult::Success(user) => {
 | 
				
			||||||
                let status = if user.need_reset_password {
 | 
					                let status = if user.need_reset_password {
 | 
				
			||||||
 | 
					                    logger.log(Action::UserNeedNewPasswordOnLogin(&user));
 | 
				
			||||||
                    SessionStatus::NeedNewPassword
 | 
					                    SessionStatus::NeedNewPassword
 | 
				
			||||||
                } else if user.has_two_factor() && !user.can_bypass_two_factors_for_ip(remote_ip.0)
 | 
					                } else if user.has_two_factor() && !user.can_bypass_two_factors_for_ip(remote_ip.0)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
 | 
					                    logger.log(Action::UserNeed2FAOnLogin(&user));
 | 
				
			||||||
                    SessionStatus::Need2FA
 | 
					                    SessionStatus::Need2FA
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
 | 
					                    logger.log(Action::UserSuccessfullyAuthenticated(&user));
 | 
				
			||||||
                    SessionStatus::SignedIn
 | 
					                    SessionStatus::SignedIn
 | 
				
			||||||
                };
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -151,7 +158,8 @@ pub async fn login_route(
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            LoginResult::AccountDisabled => {
 | 
					            LoginResult::AccountDisabled => {
 | 
				
			||||||
                log::warn!("Failed login for username {} : account is disabled", login);
 | 
					                log::warn!("Failed login for username {} : account is disabled", &login);
 | 
				
			||||||
 | 
					                logger.log(Action::TryLoginWithDisabledAccount(&login));
 | 
				
			||||||
                danger = Some("Your account is disabled!".to_string());
 | 
					                danger = Some("Your account is disabled!".to_string());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -162,6 +170,7 @@ pub async fn login_route(
 | 
				
			|||||||
                    login,
 | 
					                    login,
 | 
				
			||||||
                    c
 | 
					                    c
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					                logger.log(Action::FailedLoginWithBadCredentials(&login));
 | 
				
			||||||
                danger = Some("Login failed.".to_string());
 | 
					                danger = Some("Login failed.".to_string());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                bruteforce
 | 
					                bruteforce
 | 
				
			||||||
@@ -213,6 +222,7 @@ pub async fn reset_password_route(
 | 
				
			|||||||
    req: Option<web::Form<ChangePasswordRequestBody>>,
 | 
					    req: Option<web::Form<ChangePasswordRequestBody>>,
 | 
				
			||||||
    users: web::Data<Addr<UsersActor>>,
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
    http_req: HttpRequest,
 | 
					    http_req: HttpRequest,
 | 
				
			||||||
 | 
					    logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    let mut danger = None;
 | 
					    let mut danger = None;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -220,6 +230,8 @@ pub async fn reset_password_route(
 | 
				
			|||||||
        return redirect_user_for_login(query.redirect.get());
 | 
					        return redirect_user_for_login(query.redirect.get());
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let user_id = SessionIdentity(id.as_ref()).user_id();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Check if user is setting a new  password
 | 
					    // Check if user is setting a new  password
 | 
				
			||||||
    if let Some(req) = &req {
 | 
					    if let Some(req) = &req {
 | 
				
			||||||
        if req.password.len() < MIN_PASS_LEN {
 | 
					        if req.password.len() < MIN_PASS_LEN {
 | 
				
			||||||
@@ -227,7 +239,7 @@ pub async fn reset_password_route(
 | 
				
			|||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            let res: ChangePasswordResult = users
 | 
					            let res: ChangePasswordResult = users
 | 
				
			||||||
                .send(users_actor::ChangePasswordRequest {
 | 
					                .send(users_actor::ChangePasswordRequest {
 | 
				
			||||||
                    user_id: SessionIdentity(id.as_ref()).user_id(),
 | 
					                    user_id: user_id.clone(),
 | 
				
			||||||
                    new_password: req.password.clone(),
 | 
					                    new_password: req.password.clone(),
 | 
				
			||||||
                    temporary: false,
 | 
					                    temporary: false,
 | 
				
			||||||
                })
 | 
					                })
 | 
				
			||||||
@@ -238,6 +250,7 @@ pub async fn reset_password_route(
 | 
				
			|||||||
                danger = Some("Failed to change password!".to_string());
 | 
					                danger = Some("Failed to change password!".to_string());
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
 | 
					                SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
 | 
				
			||||||
 | 
					                logger.log(Action::UserChangedPasswordOnLogin(&user_id));
 | 
				
			||||||
                return redirect_user(query.redirect.get());
 | 
					                return redirect_user(query.redirect.get());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -328,6 +341,7 @@ pub async fn login_with_otp(
 | 
				
			|||||||
    users: web::Data<Addr<UsersActor>>,
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
    http_req: HttpRequest,
 | 
					    http_req: HttpRequest,
 | 
				
			||||||
    remote_ip: RemoteIP,
 | 
					    remote_ip: RemoteIP,
 | 
				
			||||||
 | 
					    logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    let mut danger = None;
 | 
					    let mut danger = None;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -354,14 +368,25 @@ pub async fn login_with_otp(
 | 
				
			|||||||
            .iter()
 | 
					            .iter()
 | 
				
			||||||
            .any(|k| k.check_code(&form.code).unwrap_or(false))
 | 
					            .any(|k| k.check_code(&form.code).unwrap_or(false))
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            logger.log(Action::OTPLoginAttempt {
 | 
				
			||||||
 | 
					                success: false,
 | 
				
			||||||
 | 
					                user: &user,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
            danger = Some("Specified code is invalid!".to_string());
 | 
					            danger = Some("Specified code is invalid!".to_string());
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            users
 | 
					            users
 | 
				
			||||||
                .send(users_actor::AddSuccessful2FALogin(user.uid, remote_ip.0))
 | 
					                .send(users_actor::AddSuccessful2FALogin(
 | 
				
			||||||
 | 
					                    user.uid.clone(),
 | 
				
			||||||
 | 
					                    remote_ip.0,
 | 
				
			||||||
 | 
					                ))
 | 
				
			||||||
                .await
 | 
					                .await
 | 
				
			||||||
                .unwrap();
 | 
					                .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
 | 
					            SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
 | 
				
			||||||
 | 
					            logger.log(Action::OTPLoginAttempt {
 | 
				
			||||||
 | 
					                success: true,
 | 
				
			||||||
 | 
					                user: &user,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
            return redirect_user(query.redirect.get());
 | 
					            return redirect_user(query.redirect.get());
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,7 @@ use crate::actors::users_actor::UsersActor;
 | 
				
			|||||||
use crate::actors::{openid_sessions_actor, users_actor};
 | 
					use crate::actors::{openid_sessions_actor, users_actor};
 | 
				
			||||||
use crate::constants::*;
 | 
					use crate::constants::*;
 | 
				
			||||||
use crate::controllers::base_controller::build_fatal_error_page;
 | 
					use crate::controllers::base_controller::build_fatal_error_page;
 | 
				
			||||||
 | 
					use crate::data::action_logger::{Action, ActionLogger};
 | 
				
			||||||
use crate::data::app_config::AppConfig;
 | 
					use crate::data::app_config::AppConfig;
 | 
				
			||||||
use crate::data::client::{ClientID, ClientManager};
 | 
					use crate::data::client::{ClientID, ClientManager};
 | 
				
			||||||
use crate::data::code_challenge::CodeChallenge;
 | 
					use crate::data::code_challenge::CodeChallenge;
 | 
				
			||||||
@@ -112,6 +113,7 @@ pub async fn authorize(
 | 
				
			|||||||
    query: web::Query<AuthorizeQuery>,
 | 
					    query: web::Query<AuthorizeQuery>,
 | 
				
			||||||
    clients: web::Data<ClientManager>,
 | 
					    clients: web::Data<ClientManager>,
 | 
				
			||||||
    sessions: web::Data<Addr<OpenIDSessionsActor>>,
 | 
					    sessions: web::Data<Addr<OpenIDSessionsActor>>,
 | 
				
			||||||
 | 
					    logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    let client = match clients.find_by_id(&query.client_id) {
 | 
					    let client = match clients.find_by_id(&query.client_id) {
 | 
				
			||||||
        None => {
 | 
					        None => {
 | 
				
			||||||
@@ -171,7 +173,7 @@ pub async fn authorize(
 | 
				
			|||||||
    // Save all authentication information in memory
 | 
					    // Save all authentication information in memory
 | 
				
			||||||
    let session = Session {
 | 
					    let session = Session {
 | 
				
			||||||
        session_id: SessionID(rand_str(OPEN_ID_SESSION_LEN)),
 | 
					        session_id: SessionID(rand_str(OPEN_ID_SESSION_LEN)),
 | 
				
			||||||
        client: client.id,
 | 
					        client: client.id.clone(),
 | 
				
			||||||
        user: user.uid.clone(),
 | 
					        user: user.uid.clone(),
 | 
				
			||||||
        auth_time: SessionIdentity(Some(&id)).auth_time(),
 | 
					        auth_time: SessionIdentity(Some(&id)).auth_time(),
 | 
				
			||||||
        redirect_uri,
 | 
					        redirect_uri,
 | 
				
			||||||
@@ -190,6 +192,7 @@ pub async fn authorize(
 | 
				
			|||||||
        .unwrap();
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log::trace!("New OpenID session: {:#?}", session);
 | 
					    log::trace!("New OpenID session: {:#?}", session);
 | 
				
			||||||
 | 
					    logger.log(Action::NewOpenIDSession { client: &client });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    HttpResponse::Found()
 | 
					    HttpResponse::Found()
 | 
				
			||||||
        .append_header((
 | 
					        .append_header((
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ use crate::actors::bruteforce_actor::BruteForceActor;
 | 
				
			|||||||
use crate::actors::users_actor::UsersActor;
 | 
					use crate::actors::users_actor::UsersActor;
 | 
				
			||||||
use crate::actors::{bruteforce_actor, users_actor};
 | 
					use crate::actors::{bruteforce_actor, users_actor};
 | 
				
			||||||
use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
 | 
					use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
 | 
				
			||||||
 | 
					use crate::data::action_logger::{Action, ActionLogger};
 | 
				
			||||||
use crate::data::app_config::AppConfig;
 | 
					use crate::data::app_config::AppConfig;
 | 
				
			||||||
use crate::data::current_user::CurrentUser;
 | 
					use crate::data::current_user::CurrentUser;
 | 
				
			||||||
use crate::data::remote_ip::RemoteIP;
 | 
					use crate::data::remote_ip::RemoteIP;
 | 
				
			||||||
@@ -82,6 +83,7 @@ pub async fn change_password_route(
 | 
				
			|||||||
    req: Option<web::Form<PassChangeRequest>>,
 | 
					    req: Option<web::Form<PassChangeRequest>>,
 | 
				
			||||||
    bruteforce: web::Data<Addr<BruteForceActor>>,
 | 
					    bruteforce: web::Data<Addr<BruteForceActor>>,
 | 
				
			||||||
    remote_ip: RemoteIP,
 | 
					    remote_ip: RemoteIP,
 | 
				
			||||||
 | 
					    logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    let mut danger = None;
 | 
					    let mut danger = None;
 | 
				
			||||||
    let mut success = None;
 | 
					    let mut success = None;
 | 
				
			||||||
@@ -130,6 +132,7 @@ pub async fn change_password_route(
 | 
				
			|||||||
                danger =
 | 
					                danger =
 | 
				
			||||||
                    Some("An error occurred while trying to change your password!".to_string());
 | 
					                    Some("An error occurred while trying to change your password!".to_string());
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
 | 
					                logger.log(Action::ChangedHisPassword);
 | 
				
			||||||
                success = Some("Your password was successfully changed!".to_string());
 | 
					                success = Some("Your password was successfully changed!".to_string());
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,11 +5,22 @@ use webauthn_rs::prelude::RegisterPublicKeyCredential;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use crate::actors::users_actor;
 | 
					use crate::actors::users_actor;
 | 
				
			||||||
use crate::actors::users_actor::UsersActor;
 | 
					use crate::actors::users_actor::UsersActor;
 | 
				
			||||||
 | 
					use crate::constants::MAX_SECOND_FACTOR_NAME_LEN;
 | 
				
			||||||
 | 
					use crate::data::action_logger::{Action, ActionLogger};
 | 
				
			||||||
use crate::data::current_user::CurrentUser;
 | 
					use crate::data::current_user::CurrentUser;
 | 
				
			||||||
use crate::data::totp_key::TotpKey;
 | 
					use crate::data::totp_key::TotpKey;
 | 
				
			||||||
use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User};
 | 
					use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User};
 | 
				
			||||||
use crate::data::webauthn_manager::WebAuthManagerReq;
 | 
					use crate::data::webauthn_manager::WebAuthManagerReq;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn preprocess_factor_name(name: &str) -> String {
 | 
				
			||||||
 | 
					    name.replace('<', "<")
 | 
				
			||||||
 | 
					        .replace('>', ">")
 | 
				
			||||||
 | 
					        .chars()
 | 
				
			||||||
 | 
					        .take(MAX_SECOND_FACTOR_NAME_LEN)
 | 
				
			||||||
 | 
					        .filter(|c| *c != '\n' && *c != '\t' && *c != '\r' && c.is_ascii())
 | 
				
			||||||
 | 
					        .collect()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(serde::Deserialize)]
 | 
					#[derive(serde::Deserialize)]
 | 
				
			||||||
pub struct AddTOTPRequest {
 | 
					pub struct AddTOTPRequest {
 | 
				
			||||||
    factor_name: String,
 | 
					    factor_name: String,
 | 
				
			||||||
@@ -21,6 +32,7 @@ pub async fn save_totp_factor(
 | 
				
			|||||||
    user: CurrentUser,
 | 
					    user: CurrentUser,
 | 
				
			||||||
    form: web::Json<AddTOTPRequest>,
 | 
					    form: web::Json<AddTOTPRequest>,
 | 
				
			||||||
    users: web::Data<Addr<UsersActor>>,
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
 | 
					    logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    let key = TotpKey::from_encoded_secret(&form.secret);
 | 
					    let key = TotpKey::from_encoded_secret(&form.secret);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,16 +44,20 @@ pub async fn save_totp_factor(
 | 
				
			|||||||
        ));
 | 
					        ));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if form.factor_name.is_empty() {
 | 
					    let factor_name = preprocess_factor_name(&form.factor_name);
 | 
				
			||||||
        return HttpResponse::BadRequest().body("Please give a name to the factor!");
 | 
					    if factor_name.is_empty() {
 | 
				
			||||||
 | 
					        return HttpResponse::BadRequest().body("Please give a valid name to the factor!");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let mut user = User::from(user);
 | 
					    let factor = TwoFactor {
 | 
				
			||||||
    user.add_factor(TwoFactor {
 | 
					 | 
				
			||||||
        id: FactorID(Uuid::new_v4().to_string()),
 | 
					        id: FactorID(Uuid::new_v4().to_string()),
 | 
				
			||||||
        name: form.0.factor_name,
 | 
					        name: factor_name,
 | 
				
			||||||
        kind: TwoFactorType::TOTP(key),
 | 
					        kind: TwoFactorType::TOTP(key),
 | 
				
			||||||
    });
 | 
					    };
 | 
				
			||||||
 | 
					    logger.log(Action::AddNewFactor(&factor));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut user = User::from(user);
 | 
				
			||||||
 | 
					    user.add_factor(factor);
 | 
				
			||||||
    let res = users
 | 
					    let res = users
 | 
				
			||||||
        .send(users_actor::UpdateUserRequest(user))
 | 
					        .send(users_actor::UpdateUserRequest(user))
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
@@ -67,7 +83,13 @@ pub async fn save_webauthn_factor(
 | 
				
			|||||||
    form: web::Json<AddWebauthnRequest>,
 | 
					    form: web::Json<AddWebauthnRequest>,
 | 
				
			||||||
    users: web::Data<Addr<UsersActor>>,
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
    manager: WebAuthManagerReq,
 | 
					    manager: WebAuthManagerReq,
 | 
				
			||||||
 | 
					    logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
 | 
					    let factor_name = preprocess_factor_name(&form.factor_name);
 | 
				
			||||||
 | 
					    if factor_name.is_empty() {
 | 
				
			||||||
 | 
					        return HttpResponse::BadRequest().body("Please give a valid name to the factor!");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let key = match manager.finish_registration(&user, &form.0.opaque_state, form.0.credential) {
 | 
					    let key = match manager.finish_registration(&user, &form.0.opaque_state, form.0.credential) {
 | 
				
			||||||
        Ok(k) => k,
 | 
					        Ok(k) => k,
 | 
				
			||||||
        Err(e) => {
 | 
					        Err(e) => {
 | 
				
			||||||
@@ -76,12 +98,15 @@ pub async fn save_webauthn_factor(
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let mut user = User::from(user);
 | 
					    let factor = TwoFactor {
 | 
				
			||||||
    user.add_factor(TwoFactor {
 | 
					 | 
				
			||||||
        id: FactorID(Uuid::new_v4().to_string()),
 | 
					        id: FactorID(Uuid::new_v4().to_string()),
 | 
				
			||||||
        name: form.0.factor_name,
 | 
					        name: factor_name,
 | 
				
			||||||
        kind: TwoFactorType::WEBAUTHN(Box::new(key)),
 | 
					        kind: TwoFactorType::WEBAUTHN(Box::new(key)),
 | 
				
			||||||
    });
 | 
					    };
 | 
				
			||||||
 | 
					    logger.log(Action::AddNewFactor(&factor));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let mut user = User::from(user);
 | 
				
			||||||
 | 
					    user.add_factor(factor);
 | 
				
			||||||
    let res = users
 | 
					    let res = users
 | 
				
			||||||
        .send(users_actor::UpdateUserRequest(user))
 | 
					        .send(users_actor::UpdateUserRequest(user))
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
@@ -104,9 +129,10 @@ pub async fn delete_factor(
 | 
				
			|||||||
    user: CurrentUser,
 | 
					    user: CurrentUser,
 | 
				
			||||||
    form: web::Json<DeleteFactorRequest>,
 | 
					    form: web::Json<DeleteFactorRequest>,
 | 
				
			||||||
    users: web::Data<Addr<UsersActor>>,
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
 | 
					    logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    let mut user = User::from(user);
 | 
					    let mut user = User::from(user);
 | 
				
			||||||
    user.remove_factor(form.0.id);
 | 
					    user.remove_factor(form.0.id.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let res = users
 | 
					    let res = users
 | 
				
			||||||
        .send(users_actor::UpdateUserRequest(user))
 | 
					        .send(users_actor::UpdateUserRequest(user))
 | 
				
			||||||
@@ -117,6 +143,9 @@ pub async fn delete_factor(
 | 
				
			|||||||
    if !res {
 | 
					    if !res {
 | 
				
			||||||
        HttpResponse::InternalServerError().body("Failed to update user information!")
 | 
					        HttpResponse::InternalServerError().body("Failed to update user information!")
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
 | 
					        logger.log(Action::Removed2FAFactor {
 | 
				
			||||||
 | 
					            factor_id: &form.0.id,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
        HttpResponse::Ok().body("Removed factor!")
 | 
					        HttpResponse::Ok().body("Removed factor!")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -124,11 +153,13 @@ pub async fn delete_factor(
 | 
				
			|||||||
pub async fn clear_login_history(
 | 
					pub async fn clear_login_history(
 | 
				
			||||||
    user: CurrentUser,
 | 
					    user: CurrentUser,
 | 
				
			||||||
    users: web::Data<Addr<UsersActor>>,
 | 
					    users: web::Data<Addr<UsersActor>>,
 | 
				
			||||||
 | 
					    logger: ActionLogger,
 | 
				
			||||||
) -> impl Responder {
 | 
					) -> impl Responder {
 | 
				
			||||||
    users
 | 
					    users
 | 
				
			||||||
        .send(users_actor::Clear2FALoginHistory(user.uid.clone()))
 | 
					        .send(users_actor::Clear2FALoginHistory(user.uid.clone()))
 | 
				
			||||||
        .await
 | 
					        .await
 | 
				
			||||||
        .unwrap();
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    logger.log(Action::ClearedHisLoginHistory);
 | 
				
			||||||
    HttpResponse::Ok().body("History successfully cleared")
 | 
					    HttpResponse::Ok().body("History successfully cleared")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
use std::ops::Deref;
 | 
					use std::ops::Deref;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::constants::MAX_SECOND_FACTOR_NAME_LEN;
 | 
				
			||||||
use actix_web::{HttpResponse, Responder};
 | 
					use actix_web::{HttpResponse, Responder};
 | 
				
			||||||
use askama::Template;
 | 
					use askama::Template;
 | 
				
			||||||
use qrcode_generator::QrCodeEcc;
 | 
					use qrcode_generator::QrCodeEcc;
 | 
				
			||||||
@@ -25,6 +26,7 @@ struct AddTotpPage {
 | 
				
			|||||||
    qr_code: String,
 | 
					    qr_code: String,
 | 
				
			||||||
    account_name: String,
 | 
					    account_name: String,
 | 
				
			||||||
    secret_key: String,
 | 
					    secret_key: String,
 | 
				
			||||||
 | 
					    max_name_len: usize,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Template)]
 | 
					#[derive(Template)]
 | 
				
			||||||
@@ -33,6 +35,7 @@ struct AddWebauhtnPage {
 | 
				
			|||||||
    _p: BaseSettingsPage,
 | 
					    _p: BaseSettingsPage,
 | 
				
			||||||
    opaque_state: String,
 | 
					    opaque_state: String,
 | 
				
			||||||
    challenge_json: String,
 | 
					    challenge_json: String,
 | 
				
			||||||
 | 
					    max_name_len: usize,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Manage two factors authentication methods route
 | 
					/// Manage two factors authentication methods route
 | 
				
			||||||
@@ -70,6 +73,7 @@ pub async fn add_totp_factor_route(user: CurrentUser) -> impl Responder {
 | 
				
			|||||||
            qr_code: base64::encode(qr_code),
 | 
					            qr_code: base64::encode(qr_code),
 | 
				
			||||||
            account_name: key.account_name(&user, AppConfig::get()),
 | 
					            account_name: key.account_name(&user, AppConfig::get()),
 | 
				
			||||||
            secret_key: key.get_secret(),
 | 
					            secret_key: key.get_secret(),
 | 
				
			||||||
 | 
					            max_name_len: MAX_SECOND_FACTOR_NAME_LEN,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .render()
 | 
					        .render()
 | 
				
			||||||
        .unwrap(),
 | 
					        .unwrap(),
 | 
				
			||||||
@@ -104,6 +108,7 @@ pub async fn add_webauthn_factor_route(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            opaque_state: registration_request.opaque_state,
 | 
					            opaque_state: registration_request.opaque_state,
 | 
				
			||||||
            challenge_json: urlencoding::encode(&challenge_json).to_string(),
 | 
					            challenge_json: urlencoding::encode(&challenge_json).to_string(),
 | 
				
			||||||
 | 
					            max_name_len: MAX_SECOND_FACTOR_NAME_LEN,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .render()
 | 
					        .render()
 | 
				
			||||||
        .unwrap(),
 | 
					        .unwrap(),
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										180
									
								
								src/data/action_logger.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								src/data/action_logger.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,180 @@
 | 
				
			|||||||
 | 
					use std::future::Future;
 | 
				
			||||||
 | 
					use std::net::IpAddr;
 | 
				
			||||||
 | 
					use std::pin::Pin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use actix::Addr;
 | 
				
			||||||
 | 
					use actix_identity::Identity;
 | 
				
			||||||
 | 
					use actix_web::dev::Payload;
 | 
				
			||||||
 | 
					use actix_web::{web, Error, FromRequest, HttpRequest};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::actors::users_actor;
 | 
				
			||||||
 | 
					use crate::actors::users_actor::UsersActor;
 | 
				
			||||||
 | 
					use crate::data::client::Client;
 | 
				
			||||||
 | 
					use crate::data::remote_ip::RemoteIP;
 | 
				
			||||||
 | 
					use crate::data::session_identity::SessionIdentity;
 | 
				
			||||||
 | 
					use crate::data::user::{FactorID, TwoFactor, User, UserID};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub enum Action<'a> {
 | 
				
			||||||
 | 
					    AdminCreateUser(&'a User),
 | 
				
			||||||
 | 
					    AdminUpdateUser(&'a User),
 | 
				
			||||||
 | 
					    AdminDeleteUser(&'a User),
 | 
				
			||||||
 | 
					    AdminResetUserPassword(&'a User),
 | 
				
			||||||
 | 
					    AdminClear2FAHistory(&'a User),
 | 
				
			||||||
 | 
					    LoginWebauthnAttempt { success: bool, user_id: UserID },
 | 
				
			||||||
 | 
					    Signout,
 | 
				
			||||||
 | 
					    UserNeed2FAOnLogin(&'a User),
 | 
				
			||||||
 | 
					    UserSuccessfullyAuthenticated(&'a User),
 | 
				
			||||||
 | 
					    UserNeedNewPasswordOnLogin(&'a User),
 | 
				
			||||||
 | 
					    TryLoginWithDisabledAccount(&'a str),
 | 
				
			||||||
 | 
					    FailedLoginWithBadCredentials(&'a str),
 | 
				
			||||||
 | 
					    UserChangedPasswordOnLogin(&'a UserID),
 | 
				
			||||||
 | 
					    OTPLoginAttempt { user: &'a User, success: bool },
 | 
				
			||||||
 | 
					    NewOpenIDSession { client: &'a Client },
 | 
				
			||||||
 | 
					    ChangedHisPassword,
 | 
				
			||||||
 | 
					    ClearedHisLoginHistory,
 | 
				
			||||||
 | 
					    AddNewFactor(&'a TwoFactor),
 | 
				
			||||||
 | 
					    Removed2FAFactor { factor_id: &'a FactorID },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<'a> Action<'a> {
 | 
				
			||||||
 | 
					    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::AdminClear2FAHistory(user) => {
 | 
				
			||||||
 | 
					                format!("cleared 2FA history of {}", 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::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 {}, but this is a DISABLED ACCOUNT",
 | 
				
			||||||
 | 
					                login
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            Action::FailedLoginWithBadCredentials(login) => format!(
 | 
				
			||||||
 | 
					                "attempted to authenticate as {} but with a WRONG PASSWORD",
 | 
				
			||||||
 | 
					                login
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            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.id)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Action::ChangedHisPassword => "changed his password".to_string(),
 | 
				
			||||||
 | 
					            Action::ClearedHisLoginHistory => "cleared his login history".to_string(),
 | 
				
			||||||
 | 
					            Action::AddNewFactor(factor) => format!(
 | 
				
			||||||
 | 
					                "added a new {} factor with name {} and id {:?} to his account",
 | 
				
			||||||
 | 
					                factor.type_str(),
 | 
				
			||||||
 | 
					                factor.name,
 | 
				
			||||||
 | 
					                factor.id,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            Action::Removed2FAFactor { factor_id } => format!("Removed his factor {:?}", factor_id),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct ActionLogger {
 | 
				
			||||||
 | 
					    ip: IpAddr,
 | 
				
			||||||
 | 
					    user: Option<User>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ActionLogger {
 | 
				
			||||||
 | 
					    pub fn log(&self, action: Action) {
 | 
				
			||||||
 | 
					        log::info!(
 | 
				
			||||||
 | 
					            "{} from {} has {}",
 | 
				
			||||||
 | 
					            match &self.user {
 | 
				
			||||||
 | 
					                None => "Anonymous user".to_string(),
 | 
				
			||||||
 | 
					                Some(u) => u.quick_identity(),
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            self.ip.to_string(),
 | 
				
			||||||
 | 
					            action.as_string()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl FromRequest for ActionLogger {
 | 
				
			||||||
 | 
					    type Error = Error;
 | 
				
			||||||
 | 
					    type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[inline]
 | 
				
			||||||
 | 
					    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
 | 
				
			||||||
 | 
					        let req = req.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Box::pin(async move {
 | 
				
			||||||
 | 
					            let user_actor: &web::Data<Addr<UsersActor>> =
 | 
				
			||||||
 | 
					                req.app_data().expect("UserActor undefined!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let user_actor: Addr<UsersActor> = 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
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
pub mod access_token;
 | 
					pub mod access_token;
 | 
				
			||||||
 | 
					pub mod action_logger;
 | 
				
			||||||
pub mod app_config;
 | 
					pub mod app_config;
 | 
				
			||||||
pub mod client;
 | 
					pub mod client;
 | 
				
			||||||
pub mod code_challenge;
 | 
					pub mod code_challenge;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -113,6 +113,19 @@ impl User {
 | 
				
			|||||||
        format!("{} {}", self.first_name, self.last_name)
 | 
					        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
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn can_access_app(&self, id: &ClientID) -> bool {
 | 
					    pub fn can_access_app(&self, id: &ClientID) -> bool {
 | 
				
			||||||
        match &self.authorized_clients {
 | 
					        match &self.authorized_clients {
 | 
				
			||||||
            None => true,
 | 
					            None => true,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,8 +28,8 @@
 | 
				
			|||||||
            <label for="inputDevName" class="form-label mt-4">Device name</label>
 | 
					            <label for="inputDevName" class="form-label mt-4">Device name</label>
 | 
				
			||||||
            <input type="text" class="form-control" id="inputDevName"
 | 
					            <input type="text" class="form-control" id="inputDevName"
 | 
				
			||||||
                   placeholder="Device / Authenticator app name"
 | 
					                   placeholder="Device / Authenticator app name"
 | 
				
			||||||
                   value="Authenticator app" minlength="1" required/>
 | 
					                   value="Authenticator app" minlength="1" maxlength="{{ max_name_len }}" required/>
 | 
				
			||||||
            <small class="form-text text-muted">Please give a name to your device to identity it more easily
 | 
					            <small class="form-text text-muted">Please give a name to your device to identify it more easily
 | 
				
			||||||
                later.</small>
 | 
					                later.</small>
 | 
				
			||||||
            <div class="invalid-feedback">Please give a name to this authenticator app</div>
 | 
					            <div class="invalid-feedback">Please give a name to this authenticator app</div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@
 | 
				
			|||||||
        <label for="inputKeyName" class="form-label mt-4">Key name</label>
 | 
					        <label for="inputKeyName" class="form-label mt-4">Key name</label>
 | 
				
			||||||
        <input type="text" class="form-control" id="inputKeyName"
 | 
					        <input type="text" class="form-control" id="inputKeyName"
 | 
				
			||||||
               placeholder="Device / Authenticator app name"
 | 
					               placeholder="Device / Authenticator app name"
 | 
				
			||||||
               value="Security key" minlength="1" required/>
 | 
					               value="Security key" minlength="1" maxlength="{{ max_name_len }}" required/>
 | 
				
			||||||
        <small class="form-text text-muted">Please give a name to your key to identify it more easily later.</small>
 | 
					        <small class="form-text text-muted">Please give a name to your key to identify it more easily later.</small>
 | 
				
			||||||
        <div class="invalid-feedback">Please give a name to this security key</div>
 | 
					        <div class="invalid-feedback">Please give a name to this security key</div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user