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:
parent
c242a492fc
commit
d06c0352fc
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user