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