Log all user actions on stdout
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2022-11-19 13:38:24 +01:00
parent c242a492fc
commit d06c0352fc
14 changed files with 323 additions and 21 deletions

View File

@ -1,7 +1,9 @@
use crate::actors::users_actor;
use actix::Addr;
use actix_web::{web, HttpResponse, Responder};
use crate::actors::users_actor::{DeleteUserRequest, FindUserByUsername, UsersActor};
use crate::data::action_logger::{Action, ActionLogger};
use crate::data::current_user::CurrentUser;
use crate::data::user::UserID;
@ -37,13 +39,25 @@ pub async fn delete_user(
user: CurrentUser,
req: web::Form<DeleteUserReq>,
users: web::Data<Addr<UsersActor>>,
action_logger: ActionLogger,
) -> impl Responder {
if user.uid == req.user_id {
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();
if res.0 {
action_logger.log(Action::AdminDeleteUser(&user));
HttpResponse::Ok().finish()
} else {
HttpResponse::InternalServerError().finish()

View File

@ -8,6 +8,7 @@ use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor;
use crate::constants::TEMPORARY_PASSWORDS_LEN;
use crate::controllers::settings_controller::BaseSettingsPage;
use crate::data::action_logger::{Action, ActionLogger};
use crate::data::client::{Client, ClientID, ClientManager};
use crate::data::current_user::CurrentUser;
use crate::data::user::{hash_password, User, UserID};
@ -67,6 +68,7 @@ pub async fn users_route(
user: CurrentUser,
users: web::Data<Addr<UsersActor>>,
update_query: Option<web::Form<UpdateUserQuery>>,
logger: ActionLogger,
) -> impl Responder {
let mut danger = 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() {
false => None,
true => {
logger.log(Action::AdminResetUserPassword(&user));
let temp_pass = rand_str(TEMPORARY_PASSWORDS_LEN);
user.password = hash_password(&temp_pass).expect("Failed to hash password");
user.need_reset_password = true;
@ -121,6 +125,7 @@ pub async fn users_route(
};
if update.0.clear_2fa_history.is_some() {
logger.log(Action::AdminClear2FAHistory(&user));
user.last_successful_2fa = Default::default();
}
@ -140,8 +145,14 @@ pub async fn users_route(
)
} else {
success = Some(match is_creating {
true => format!("User {} was successfully created!", user.full_name()),
false => format!("User {} was successfully updated!", user.full_name()),
true => {
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 {

View File

@ -1,5 +1,6 @@
use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor;
use crate::data::action_logger::{Action, ActionLogger};
use crate::data::remote_ip::RemoteIP;
use actix::Addr;
use actix_identity::Identity;
@ -22,6 +23,7 @@ pub async fn auth_webauthn(
http_req: HttpRequest,
remote_ip: RemoteIP,
users: web::Data<Addr<UsersActor>>,
logger: ActionLogger,
) -> impl Responder {
if !SessionIdentity(Some(&id)).need_2fa_auth() {
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) {
Ok(_) => {
users
.send(users_actor::AddSuccessful2FALogin(user_id, remote_ip.0))
.send(users_actor::AddSuccessful2FALogin(
user_id.clone(),
remote_ip.0,
))
.await
.unwrap();
SessionIdentity(Some(&id)).set_status(&http_req, SessionStatus::SignedIn);
logger.log(Action::LoginWebauthnAttempt {
success: true,
user_id,
});
HttpResponse::Ok().body("You are authenticated!")
}
Err(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!")
}
}

View File

@ -10,6 +10,7 @@ use crate::constants::{APP_NAME, MAX_FAILED_LOGIN_ATTEMPTS, MIN_PASS_LEN};
use crate::controllers::base_controller::{
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::remote_ip::RemoteIP;
use crate::data::session_identity::{SessionIdentity, SessionStatus};
@ -73,6 +74,7 @@ pub struct LoginRequestQuery {
}
/// Authenticate user
#[allow(clippy::too_many_arguments)]
pub async fn login_route(
remote_ip: RemoteIP,
users: web::Data<Addr<UsersActor>>,
@ -81,6 +83,7 @@ pub async fn login_route(
req: Option<web::Form<LoginRequestBody>>,
id: Option<Identity>,
http_req: HttpRequest,
logger: ActionLogger,
) -> impl Responder {
let mut danger = None;
let mut success = None;
@ -102,6 +105,7 @@ pub async fn login_route(
// Check if user session must be closed
if let Some(true) = query.logout {
if let Some(id) = id {
logger.log(Action::Signout);
id.logout();
}
success = Some("Goodbye!".to_string());
@ -138,11 +142,14 @@ pub async fn login_route(
match response {
LoginResult::Success(user) => {
let status = if user.need_reset_password {
logger.log(Action::UserNeedNewPasswordOnLogin(&user));
SessionStatus::NeedNewPassword
} else if user.has_two_factor() && !user.can_bypass_two_factors_for_ip(remote_ip.0)
{
logger.log(Action::UserNeed2FAOnLogin(&user));
SessionStatus::Need2FA
} else {
logger.log(Action::UserSuccessfullyAuthenticated(&user));
SessionStatus::SignedIn
};
@ -151,7 +158,8 @@ pub async fn login_route(
}
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());
}
@ -162,6 +170,7 @@ pub async fn login_route(
login,
c
);
logger.log(Action::FailedLoginWithBadCredentials(&login));
danger = Some("Login failed.".to_string());
bruteforce
@ -213,6 +222,7 @@ pub async fn reset_password_route(
req: Option<web::Form<ChangePasswordRequestBody>>,
users: web::Data<Addr<UsersActor>>,
http_req: HttpRequest,
logger: ActionLogger,
) -> impl Responder {
let mut danger = None;
@ -220,6 +230,8 @@ pub async fn reset_password_route(
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
if let Some(req) = &req {
if req.password.len() < MIN_PASS_LEN {
@ -227,7 +239,7 @@ pub async fn reset_password_route(
} else {
let res: ChangePasswordResult = users
.send(users_actor::ChangePasswordRequest {
user_id: SessionIdentity(id.as_ref()).user_id(),
user_id: user_id.clone(),
new_password: req.password.clone(),
temporary: false,
})
@ -238,6 +250,7 @@ pub async fn reset_password_route(
danger = Some("Failed to change password!".to_string());
} else {
SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
logger.log(Action::UserChangedPasswordOnLogin(&user_id));
return redirect_user(query.redirect.get());
}
}
@ -328,6 +341,7 @@ pub async fn login_with_otp(
users: web::Data<Addr<UsersActor>>,
http_req: HttpRequest,
remote_ip: RemoteIP,
logger: ActionLogger,
) -> impl Responder {
let mut danger = None;
@ -354,14 +368,25 @@ pub async fn login_with_otp(
.iter()
.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());
} else {
users
.send(users_actor::AddSuccessful2FALogin(user.uid, remote_ip.0))
.send(users_actor::AddSuccessful2FALogin(
user.uid.clone(),
remote_ip.0,
))
.await
.unwrap();
SessionIdentity(id.as_ref()).set_status(&http_req, SessionStatus::SignedIn);
logger.log(Action::OTPLoginAttempt {
success: true,
user: &user,
});
return redirect_user(query.redirect.get());
}
}

View File

@ -10,6 +10,7 @@ use crate::actors::users_actor::UsersActor;
use crate::actors::{openid_sessions_actor, users_actor};
use crate::constants::*;
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::client::{ClientID, ClientManager};
use crate::data::code_challenge::CodeChallenge;
@ -112,6 +113,7 @@ pub async fn authorize(
query: web::Query<AuthorizeQuery>,
clients: web::Data<ClientManager>,
sessions: web::Data<Addr<OpenIDSessionsActor>>,
logger: ActionLogger,
) -> impl Responder {
let client = match clients.find_by_id(&query.client_id) {
None => {
@ -171,7 +173,7 @@ pub async fn authorize(
// Save all authentication information in memory
let session = Session {
session_id: SessionID(rand_str(OPEN_ID_SESSION_LEN)),
client: client.id,
client: client.id.clone(),
user: user.uid.clone(),
auth_time: SessionIdentity(Some(&id)).auth_time(),
redirect_uri,
@ -190,6 +192,7 @@ pub async fn authorize(
.unwrap();
log::trace!("New OpenID session: {:#?}", session);
logger.log(Action::NewOpenIDSession { client: &client });
HttpResponse::Found()
.append_header((

View File

@ -6,6 +6,7 @@ use crate::actors::bruteforce_actor::BruteForceActor;
use crate::actors::users_actor::UsersActor;
use crate::actors::{bruteforce_actor, users_actor};
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::current_user::CurrentUser;
use crate::data::remote_ip::RemoteIP;
@ -82,6 +83,7 @@ pub async fn change_password_route(
req: Option<web::Form<PassChangeRequest>>,
bruteforce: web::Data<Addr<BruteForceActor>>,
remote_ip: RemoteIP,
logger: ActionLogger,
) -> impl Responder {
let mut danger = None;
let mut success = None;
@ -130,6 +132,7 @@ pub async fn change_password_route(
danger =
Some("An error occurred while trying to change your password!".to_string());
} else {
logger.log(Action::ChangedHisPassword);
success = Some("Your password was successfully changed!".to_string());
}
}

View File

@ -5,11 +5,22 @@ use webauthn_rs::prelude::RegisterPublicKeyCredential;
use crate::actors::users_actor;
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::totp_key::TotpKey;
use crate::data::user::{FactorID, TwoFactor, TwoFactorType, User};
use crate::data::webauthn_manager::WebAuthManagerReq;
fn preprocess_factor_name(name: &str) -> String {
name.replace('<', "&lt;")
.replace('>', "&gt;")
.chars()
.take(MAX_SECOND_FACTOR_NAME_LEN)
.filter(|c| *c != '\n' && *c != '\t' && *c != '\r' && c.is_ascii())
.collect()
}
#[derive(serde::Deserialize)]
pub struct AddTOTPRequest {
factor_name: String,
@ -21,6 +32,7 @@ pub async fn save_totp_factor(
user: CurrentUser,
form: web::Json<AddTOTPRequest>,
users: web::Data<Addr<UsersActor>>,
logger: ActionLogger,
) -> impl Responder {
let key = TotpKey::from_encoded_secret(&form.secret);
@ -32,16 +44,20 @@ pub async fn save_totp_factor(
));
}
if form.factor_name.is_empty() {
return HttpResponse::BadRequest().body("Please give a name to the factor!");
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 mut user = User::from(user);
user.add_factor(TwoFactor {
let factor = TwoFactor {
id: FactorID(Uuid::new_v4().to_string()),
name: form.0.factor_name,
name: factor_name,
kind: TwoFactorType::TOTP(key),
});
};
logger.log(Action::AddNewFactor(&factor));
let mut user = User::from(user);
user.add_factor(factor);
let res = users
.send(users_actor::UpdateUserRequest(user))
.await
@ -67,7 +83,13 @@ pub async fn save_webauthn_factor(
form: web::Json<AddWebauthnRequest>,
users: web::Data<Addr<UsersActor>>,
manager: WebAuthManagerReq,
logger: ActionLogger,
) -> 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) {
Ok(k) => k,
Err(e) => {
@ -76,12 +98,15 @@ pub async fn save_webauthn_factor(
}
};
let mut user = User::from(user);
user.add_factor(TwoFactor {
let factor = TwoFactor {
id: FactorID(Uuid::new_v4().to_string()),
name: form.0.factor_name,
name: factor_name,
kind: TwoFactorType::WEBAUTHN(Box::new(key)),
});
};
logger.log(Action::AddNewFactor(&factor));
let mut user = User::from(user);
user.add_factor(factor);
let res = users
.send(users_actor::UpdateUserRequest(user))
.await
@ -104,9 +129,10 @@ pub async fn delete_factor(
user: CurrentUser,
form: web::Json<DeleteFactorRequest>,
users: web::Data<Addr<UsersActor>>,
logger: ActionLogger,
) -> impl Responder {
let mut user = User::from(user);
user.remove_factor(form.0.id);
user.remove_factor(form.0.id.clone());
let res = users
.send(users_actor::UpdateUserRequest(user))
@ -117,6 +143,9 @@ pub async fn delete_factor(
if !res {
HttpResponse::InternalServerError().body("Failed to update user information!")
} else {
logger.log(Action::Removed2FAFactor {
factor_id: &form.0.id,
});
HttpResponse::Ok().body("Removed factor!")
}
}
@ -124,11 +153,13 @@ pub async fn delete_factor(
pub async fn clear_login_history(
user: CurrentUser,
users: web::Data<Addr<UsersActor>>,
logger: ActionLogger,
) -> impl Responder {
users
.send(users_actor::Clear2FALoginHistory(user.uid.clone()))
.await
.unwrap();
logger.log(Action::ClearedHisLoginHistory);
HttpResponse::Ok().body("History successfully cleared")
}

View File

@ -1,5 +1,6 @@
use std::ops::Deref;
use crate::constants::MAX_SECOND_FACTOR_NAME_LEN;
use actix_web::{HttpResponse, Responder};
use askama::Template;
use qrcode_generator::QrCodeEcc;
@ -25,6 +26,7 @@ struct AddTotpPage {
qr_code: String,
account_name: String,
secret_key: String,
max_name_len: usize,
}
#[derive(Template)]
@ -33,6 +35,7 @@ struct AddWebauhtnPage {
_p: BaseSettingsPage,
opaque_state: String,
challenge_json: String,
max_name_len: usize,
}
/// 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),
account_name: key.account_name(&user, AppConfig::get()),
secret_key: key.get_secret(),
max_name_len: MAX_SECOND_FACTOR_NAME_LEN,
}
.render()
.unwrap(),
@ -104,6 +108,7 @@ pub async fn add_webauthn_factor_route(
opaque_state: registration_request.opaque_state,
challenge_json: urlencoding::encode(&challenge_json).to_string(),
max_name_len: MAX_SECOND_FACTOR_NAME_LEN,
}
.render()
.unwrap(),