diff --git a/src/actors/users_actor.rs b/src/actors/users_actor.rs index 0ed4438..7ed067e 100644 --- a/src/actors/users_actor.rs +++ b/src/actors/users_actor.rs @@ -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), } #[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; } -impl Handler for UsersActor { - type Result = MessageResult; +impl Handler for UsersActor { + type Result = MessageResult; - 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 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 for UsersActor { } } +impl Handler for UsersActor { + type Result = ::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 for UsersActor { type Result = ::Result; fn handle(&mut self, msg: SetGrantedClients, _ctx: &mut Self::Context) -> Self::Result { diff --git a/src/controllers/admin_controller.rs b/src/controllers/admin_controller.rs index 445abe0..5624fbe 100644 --- a/src/controllers/admin_controller.rs +++ b/src/controllers/admin_controller.rs @@ -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, two_factor_exemption_after_successful_login: Option, admin: Option, + allow_local_login: Option, 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, diff --git a/src/controllers/login_controller.rs b/src/controllers/login_controller.rs index 9237309..9dc0621 100644 --- a/src/controllers/login_controller.rs +++ b/src/controllers/login_controller.rs @@ -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()); } diff --git a/src/data/action_logger.rs b/src/data/action_logger.rs index f4189f5..e67163b 100644 --- a/src/data/action_logger.rs +++ b/src/data/action_logger.rs @@ -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") } diff --git a/src/data/user.rs b/src/data/user.rs index 1eb4ba7..917a079 100644 --- a/src/data/user.rs +++ b/src/data/user.rs @@ -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>, + + /// 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, } } } diff --git a/src/data/users_file_entity.rs b/src/data/users_file_entity.rs index b7c08c6..bc4d850 100644 --- a/src/data/users_file_entity.rs +++ b/src/data/users_file_entity.rs @@ -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 { 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(); diff --git a/templates/settings/edit_user.html b/templates/settings/edit_user.html index be18ed5..98ae901 100644 --- a/templates/settings/edit_user.html +++ b/templates/settings/edit_user.html @@ -112,28 +112,45 @@
    - {% for e in u.get_formatted_2fa_successful_logins() %} - {% if e.can_bypass_2fa %}
  • {{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA
  • - {% else %}
  • {{ e.ip }} - {{ e.fmt_time() }}
  • {% endif %} - {% endfor %} + {% for e in u.get_formatted_2fa_successful_logins() %} + {% if e.can_bypass_2fa %} +
  • {{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA
  • + {% else %} +
  • {{ e.ip }} - {{ e.fmt_time() }}
  • + {% endif %} + {% endfor %}
{% endif %} + +
+ Authorized authentication sources +
+ + +
+
+
Granted clients
@@ -155,7 +172,8 @@
@@ -231,6 +249,7 @@ form.submit(); }); + {% endblock content %} \ No newline at end of file