Can block local login for an account
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Pierre HUBERT 2023-04-24 18:46:21 +02:00
parent 96ffc669d7
commit f64f01a958
7 changed files with 140 additions and 15 deletions

View File

@ -19,6 +19,11 @@ pub trait UsersSyncBackend {
fn save_new_successful_2fa_authentication(&mut self, id: &UserID, ip: IpAddr) -> Res; fn save_new_successful_2fa_authentication(&mut self, id: &UserID, ip: IpAddr) -> Res;
fn clear_2fa_login_history(&mut self, id: &UserID) -> Res; fn clear_2fa_login_history(&mut self, id: &UserID) -> Res;
fn delete_account(&mut self, id: &UserID) -> Res; fn delete_account(&mut self, id: &UserID) -> Res;
fn set_authorized_authentication_sources(
&mut self,
id: &UserID,
sources: AuthorizedAuthenticationSources,
) -> Res;
fn set_granted_2fa_clients(&mut self, id: &UserID, clients: GrantedClients) -> Res; fn set_granted_2fa_clients(&mut self, id: &UserID, clients: GrantedClients) -> Res;
} }
@ -28,12 +33,13 @@ pub enum LoginResult {
AccountNotFound, AccountNotFound,
InvalidPassword, InvalidPassword,
AccountDisabled, AccountDisabled,
LocalAuthForbidden,
Success(Box<User>), Success(Box<User>),
} }
#[derive(Message)] #[derive(Message)]
#[rtype(LoginResult)] #[rtype(LoginResult)]
pub struct LoginRequest { pub struct LocalLoginRequest {
pub login: String, pub login: String,
pub password: String, pub password: String,
} }
@ -88,6 +94,15 @@ pub struct AddSuccessful2FALogin(pub UserID, pub IpAddr);
#[rtype(result = "bool")] #[rtype(result = "bool")]
pub struct Clear2FALoginHistory(pub UserID); pub struct Clear2FALoginHistory(pub UserID);
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct AuthorizedAuthenticationSources {
pub local: bool,
}
#[derive(Message)]
#[rtype(result = "bool")]
pub struct SetAuthorizedAuthenticationSources(pub UserID, pub AuthorizedAuthenticationSources);
#[derive(Message)] #[derive(Message)]
#[rtype(result = "bool")] #[rtype(result = "bool")]
pub struct SetGrantedClients(pub UserID, pub GrantedClients); pub struct SetGrantedClients(pub UserID, pub GrantedClients);
@ -119,10 +134,10 @@ impl Actor for UsersActor {
type Context = Context<Self>; type Context = Context<Self>;
} }
impl Handler<LoginRequest> for UsersActor { impl Handler<LocalLoginRequest> for UsersActor {
type Result = MessageResult<LoginRequest>; type Result = MessageResult<LocalLoginRequest>;
fn handle(&mut self, msg: LoginRequest, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: LocalLoginRequest, _ctx: &mut Self::Context) -> Self::Result {
match self.manager.find_by_username_or_email(&msg.login) { match self.manager.find_by_username_or_email(&msg.login) {
Err(e) => { Err(e) => {
log::error!("Failed to find user! {}", e); log::error!("Failed to find user! {}", e);
@ -142,6 +157,10 @@ impl Handler<LoginRequest> for UsersActor {
return MessageResult(LoginResult::AccountDisabled); return MessageResult(LoginResult::AccountDisabled);
} }
if !user.allow_local_login {
return MessageResult(LoginResult::LocalAuthForbidden);
}
MessageResult(LoginResult::Success(Box::new(user))) MessageResult(LoginResult::Success(Box::new(user)))
} }
} }
@ -241,6 +260,29 @@ impl Handler<Clear2FALoginHistory> for UsersActor {
} }
} }
impl Handler<SetAuthorizedAuthenticationSources> for UsersActor {
type Result = <SetAuthorizedAuthenticationSources as actix::Message>::Result;
fn handle(
&mut self,
msg: SetAuthorizedAuthenticationSources,
_ctx: &mut Self::Context,
) -> Self::Result {
match self
.manager
.set_authorized_authentication_sources(&msg.0, msg.1)
{
Ok(_) => true,
Err(e) => {
log::error!(
"Failed to set authorized authentication sources for user! {}",
e
);
false
}
}
}
}
impl Handler<SetGrantedClients> for UsersActor { impl Handler<SetGrantedClients> for UsersActor {
type Result = <SetGrantedClients as actix::Message>::Result; type Result = <SetGrantedClients as actix::Message>::Result;
fn handle(&mut self, msg: SetGrantedClients, _ctx: &mut Self::Context) -> Self::Result { fn handle(&mut self, msg: SetGrantedClients, _ctx: &mut Self::Context) -> Self::Result {

View File

@ -6,7 +6,7 @@ use actix_web::{web, HttpResponse, Responder};
use askama::Template; use askama::Template;
use crate::actors::users_actor; use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor; use crate::actors::users_actor::{AuthorizedAuthenticationSources, 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::action_logger::{Action, ActionLogger};
@ -84,6 +84,7 @@ pub struct UpdateUserQuery {
enabled: Option<String>, enabled: Option<String>,
two_factor_exemption_after_successful_login: Option<String>, two_factor_exemption_after_successful_login: Option<String>,
admin: Option<String>, admin: Option<String>,
allow_local_login: Option<String>,
grant_type: String, grant_type: String,
granted_clients: String, granted_clients: String,
two_factor: String, two_factor: String,
@ -158,6 +159,25 @@ pub async fn users_route(
} }
} }
// Update the list of authorized authentication sources
let auth_sources = AuthorizedAuthenticationSources {
local: update.0.allow_local_login.is_some(),
};
if edited_user.authorized_authentication_sources() != auth_sources {
logger.log(Action::AdminSetAuthorizedAuthenticationSources(
&edited_user,
&auth_sources,
));
users
.send(users_actor::SetAuthorizedAuthenticationSources(
edited_user.uid.clone(),
auth_sources,
))
.await
.unwrap();
}
// Update list of granted clients // Update list of granted clients
let granted_clients = match update.0.grant_type.as_str() { let granted_clients = match update.0.grant_type.as_str() {
"all_clients" => GrantedClients::AllClients, "all_clients" => GrantedClients::AllClients,

View File

@ -132,7 +132,7 @@ pub async fn login_route(
else if let Some(req) = &req { else if let Some(req) = &req {
login = req.login.clone(); login = req.login.clone();
let response: LoginResult = users let response: LoginResult = users
.send(users_actor::LoginRequest { .send(users_actor::LocalLoginRequest {
login: login.clone(), login: login.clone(),
password: req.password.clone(), password: req.password.clone(),
}) })
@ -163,6 +163,12 @@ pub async fn login_route(
danger = Some("Your account is disabled!".to_string()); danger = Some("Your account is disabled!".to_string());
} }
LoginResult::LocalAuthForbidden => {
log::warn!("Failed login for username {} : attempted to use local auth, but it is forbidden", &login);
logger.log(Action::TryLocalLoginFromUnauthorizedAccount(&login));
danger = Some("You cannot login from local auth with your account!".to_string());
}
LoginResult::Error => { LoginResult::Error => {
danger = Some("An unkown error occured while trying to sign you in!".to_string()); danger = Some("An unkown error occured while trying to sign you in!".to_string());
} }

View File

@ -8,7 +8,7 @@ use actix_web::dev::Payload;
use actix_web::{web, Error, FromRequest, HttpRequest}; use actix_web::{web, Error, FromRequest, HttpRequest};
use crate::actors::users_actor; use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor; use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor};
use crate::data::client::Client; use crate::data::client::Client;
use crate::data::remote_ip::RemoteIP; use crate::data::remote_ip::RemoteIP;
use crate::data::session_identity::SessionIdentity; use crate::data::session_identity::SessionIdentity;
@ -20,6 +20,7 @@ pub enum Action<'a> {
AdminDeleteUser(&'a User), AdminDeleteUser(&'a User),
AdminResetUserPassword(&'a User), AdminResetUserPassword(&'a User),
AdminRemoveUserFactor(&'a User, &'a TwoFactor), AdminRemoveUserFactor(&'a User, &'a TwoFactor),
AdminSetAuthorizedAuthenticationSources(&'a User, &'a AuthorizedAuthenticationSources),
AdminSetNewGrantedClientsList(&'a User, &'a GrantedClients), AdminSetNewGrantedClientsList(&'a User, &'a GrantedClients),
AdminClear2FAHistory(&'a User), AdminClear2FAHistory(&'a User),
LoginWebauthnAttempt { success: bool, user_id: UserID }, LoginWebauthnAttempt { success: bool, user_id: UserID },
@ -28,6 +29,7 @@ pub enum Action<'a> {
UserSuccessfullyAuthenticated(&'a User), UserSuccessfullyAuthenticated(&'a User),
UserNeedNewPasswordOnLogin(&'a User), UserNeedNewPasswordOnLogin(&'a User),
TryLoginWithDisabledAccount(&'a str), TryLoginWithDisabledAccount(&'a str),
TryLocalLoginFromUnauthorizedAccount(&'a str),
FailedLoginWithBadCredentials(&'a str), FailedLoginWithBadCredentials(&'a str),
UserChangedPasswordOnLogin(&'a UserID), UserChangedPasswordOnLogin(&'a UserID),
OTPLoginAttempt { user: &'a User, success: bool }, OTPLoginAttempt { user: &'a User, success: bool },
@ -64,6 +66,11 @@ impl<'a> Action<'a> {
Action::AdminClear2FAHistory(user) => { Action::AdminClear2FAHistory(user) => {
format!("cleared 2FA history of {}", user.quick_identity()) 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!( Action::AdminSetNewGrantedClientsList(user, clients) => format!(
"set new granted clients list ({:?}) for user ({})", "set new granted clients list ({:?}) for user ({})",
clients, clients,
@ -90,6 +97,9 @@ impl<'a> Action<'a> {
Action::TryLoginWithDisabledAccount(login) => { Action::TryLoginWithDisabledAccount(login) => {
format!("successfully authenticated as {login}, but this is a DISABLED ACCOUNT") 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) => { Action::FailedLoginWithBadCredentials(login) => {
format!("attempted to authenticate as {login} but with a WRONG PASSWORD") format!("attempted to authenticate as {login} but with a WRONG PASSWORD")
} }

View File

@ -1,3 +1,4 @@
use crate::actors::users_actor::AuthorizedAuthenticationSources;
use std::collections::HashMap; use std::collections::HashMap;
use std::net::IpAddr; use std::net::IpAddr;
@ -114,6 +115,10 @@ impl Successful2FALogin {
} }
} }
fn default_true() -> bool {
true
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct User { pub struct User {
pub uid: UserID, pub uid: UserID,
@ -142,6 +147,10 @@ pub struct User {
/// None = all services /// None = all services
/// Some([]) = no service /// Some([]) = no service
pub authorized_clients: Option<Vec<ClientID>>, pub authorized_clients: Option<Vec<ClientID>>,
/// Authorize connection through local login
#[serde(default = "default_true")]
pub allow_local_login: bool,
} }
impl User { impl User {
@ -162,6 +171,13 @@ impl User {
) )
} }
/// Get the list of sources from which a user can authenticate from
pub fn authorized_authentication_sources(&self) -> AuthorizedAuthenticationSources {
AuthorizedAuthenticationSources {
local: self.allow_local_login,
}
}
pub fn granted_clients(&self) -> GrantedClients { pub fn granted_clients(&self) -> GrantedClients {
match self.authorized_clients.as_deref() { match self.authorized_clients.as_deref() {
None => GrantedClients::AllClients, None => GrantedClients::AllClients,
@ -296,6 +312,7 @@ impl Default for User {
two_factor_exemption_after_successful_login: false, two_factor_exemption_after_successful_login: false,
last_successful_2fa: Default::default(), last_successful_2fa: Default::default(),
authorized_clients: Some(Vec::new()), authorized_clients: Some(Vec::new()),
allow_local_login: true,
} }
} }
} }

View File

@ -1,6 +1,6 @@
use std::net::IpAddr; use std::net::IpAddr;
use crate::actors::users_actor::UsersSyncBackend; use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersSyncBackend};
use crate::data::entity_manager::EntityManager; use crate::data::entity_manager::EntityManager;
use crate::data::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID}; use crate::data::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID};
use crate::utils::err::{new_error, Res}; use crate::utils::err::{new_error, Res};
@ -143,6 +143,17 @@ impl UsersSyncBackend for EntityManager<User> {
self.remove(&user) self.remove(&user)
} }
fn set_authorized_authentication_sources(
&mut self,
id: &UserID,
sources: AuthorizedAuthenticationSources,
) -> Res {
self.update_user(id, |mut user| {
user.allow_local_login = sources.local;
user
})
}
fn set_granted_2fa_clients(&mut self, id: &UserID, clients: GrantedClients) -> Res { fn set_granted_2fa_clients(&mut self, id: &UserID, clients: GrantedClients) -> Res {
self.update_user(id, |mut user| { self.update_user(id, |mut user| {
user.authorized_clients = clients.to_user(); user.authorized_clients = clients.to_user();

View File

@ -113,27 +113,44 @@
<ul> <ul>
{% for e in u.get_formatted_2fa_successful_logins() %} {% for e in u.get_formatted_2fa_successful_logins() %}
{% if e.can_bypass_2fa %}<li style="font-weight: bold;">{{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA</li> {% if e.can_bypass_2fa %}
{% else %}<li>{{ e.ip }} - {{ e.fmt_time() }}</li>{% endif %} <li style="font-weight: bold;">{{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA</li>
{% else %}
<li>{{ e.ip }} - {{ e.fmt_time() }}</li>
{% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
</fieldset> </fieldset>
{% endif %} {% endif %}
<!-- Authorized authentication sources -->
<fieldset class="form-group">
<legend class="mt-4">Authorized authentication sources</legend>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_local_login" id="allow_local_login"
{% if u.allow_local_login %} checked="" {% endif %}>
<label class="form-check-label" for="allow_local_login">
Allow local login
</label>
</div>
</fieldset>
<!-- Granted clients --> <!-- Granted clients -->
<fieldset class="form-group"> <fieldset class="form-group">
<legend class="mt-4">Granted clients</legend> <legend class="mt-4">Granted clients</legend>
<div class="form-check"> <div class="form-check">
<label class="form-check-label"> <label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type" <input type="radio" class="form-check-input" name="grant_type"
value="all_clients" {% if u.granted_clients() == GrantedClients::AllClients %} checked="" {% endif %}> value="all_clients" {% if u.granted_clients()== GrantedClients::AllClients %} checked="" {% endif
%}>
Grant all clients Grant all clients
</label> </label>
</div> </div>
<div class="form-check"> <div class="form-check">
<label class="form-check-label"> <label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type" <input type="radio" class="form-check-input" name="grant_type"
value="custom_clients" {% if matches!(self.u.granted_clients(), GrantedClients::SomeClients(_)) %} checked="checked" {% endif %}> value="custom_clients" {% if matches!(self.u.granted_clients(), GrantedClients::SomeClients(_))
%} checked="checked" {% endif %}>
Manually specify allowed clients Manually specify allowed clients
</label> </label>
</div> </div>
@ -155,7 +172,8 @@
<div class="form-check"> <div class="form-check">
<label class="form-check-label"> <label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type" <input type="radio" class="form-check-input" name="grant_type"
value="no_client" {% if u.granted_clients() == GrantedClients::NoClient %} checked="checked" {% endif %}> value="no_client" {% if u.granted_clients()== GrantedClients::NoClient %} checked="checked" {%
endif %}>
Do not grant any client Do not grant any client
</label> </label>
</div> </div>
@ -231,6 +249,7 @@
form.submit(); form.submit();
}); });
</script> </script>
{% endblock content %} {% endblock content %}