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 clear_2fa_login_history(&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;
}
@ -28,12 +33,13 @@ pub enum LoginResult {
AccountNotFound,
InvalidPassword,
AccountDisabled,
LocalAuthForbidden,
Success(Box<User>),
}
#[derive(Message)]
#[rtype(LoginResult)]
pub struct LoginRequest {
pub struct LocalLoginRequest {
pub login: String,
pub password: String,
}
@ -88,6 +94,15 @@ pub struct AddSuccessful2FALogin(pub UserID, pub IpAddr);
#[rtype(result = "bool")]
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)]
#[rtype(result = "bool")]
pub struct SetGrantedClients(pub UserID, pub GrantedClients);
@ -119,10 +134,10 @@ impl Actor for UsersActor {
type Context = Context<Self>;
}
impl Handler<LoginRequest> for UsersActor {
type Result = MessageResult<LoginRequest>;
impl Handler<LocalLoginRequest> for UsersActor {
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) {
Err(e) => {
log::error!("Failed to find user! {}", e);
@ -142,6 +157,10 @@ impl Handler<LoginRequest> for UsersActor {
return MessageResult(LoginResult::AccountDisabled);
}
if !user.allow_local_login {
return MessageResult(LoginResult::LocalAuthForbidden);
}
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 {
type Result = <SetGrantedClients as actix::Message>::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 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::controllers::settings_controller::BaseSettingsPage;
use crate::data::action_logger::{Action, ActionLogger};
@ -84,6 +84,7 @@ pub struct UpdateUserQuery {
enabled: Option<String>,
two_factor_exemption_after_successful_login: Option<String>,
admin: Option<String>,
allow_local_login: Option<String>,
grant_type: String,
granted_clients: 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
let granted_clients = match update.0.grant_type.as_str() {
"all_clients" => GrantedClients::AllClients,

View File

@ -132,7 +132,7 @@ pub async fn login_route(
else if let Some(req) = &req {
login = req.login.clone();
let response: LoginResult = users
.send(users_actor::LoginRequest {
.send(users_actor::LocalLoginRequest {
login: login.clone(),
password: req.password.clone(),
})
@ -163,6 +163,12 @@ pub async fn login_route(
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 => {
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 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::remote_ip::RemoteIP;
use crate::data::session_identity::SessionIdentity;
@ -20,6 +20,7 @@ pub enum Action<'a> {
AdminDeleteUser(&'a User),
AdminResetUserPassword(&'a User),
AdminRemoveUserFactor(&'a User, &'a TwoFactor),
AdminSetAuthorizedAuthenticationSources(&'a User, &'a AuthorizedAuthenticationSources),
AdminSetNewGrantedClientsList(&'a User, &'a GrantedClients),
AdminClear2FAHistory(&'a User),
LoginWebauthnAttempt { success: bool, user_id: UserID },
@ -28,6 +29,7 @@ pub enum Action<'a> {
UserSuccessfullyAuthenticated(&'a User),
UserNeedNewPasswordOnLogin(&'a User),
TryLoginWithDisabledAccount(&'a str),
TryLocalLoginFromUnauthorizedAccount(&'a str),
FailedLoginWithBadCredentials(&'a str),
UserChangedPasswordOnLogin(&'a UserID),
OTPLoginAttempt { user: &'a User, success: bool },
@ -64,6 +66,11 @@ impl<'a> Action<'a> {
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,
@ -90,6 +97,9 @@ impl<'a> Action<'a> {
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")
}

View File

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

View File

@ -1,6 +1,6 @@
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::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID};
use crate::utils::err::{new_error, Res};
@ -143,6 +143,17 @@ impl UsersSyncBackend for EntityManager<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 {
self.update_user(id, |mut user| {
user.authorized_clients = clients.to_user();

View File

@ -112,28 +112,45 @@
</div>
<ul>
{% 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>
{% else %}<li>{{ e.ip }} - {{ e.fmt_time() }}</li>{% endif %}
{% endfor %}
{% 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>
{% else %}
<li>{{ e.ip }} - {{ e.fmt_time() }}</li>
{% endif %}
{% endfor %}
</ul>
</fieldset>
{% 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 -->
<fieldset class="form-group">
<legend class="mt-4">Granted clients</legend>
<div class="form-check">
<label class="form-check-label">
<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
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<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
</label>
</div>
@ -155,7 +172,8 @@
<div class="form-check">
<label class="form-check-label">
<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
</label>
</div>
@ -231,6 +249,7 @@
form.submit();
});
</script>
{% endblock content %}