Can block local login for an account
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			All checks were successful
		
		
	
	continuous-integration/drone/push Build is passing
				
			This commit is contained in:
		@@ -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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
 
 | 
			
		||||
@@ -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());
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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();
 | 
			
		||||
 
 | 
			
		||||
@@ -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 %}
 | 
			
		||||
		Reference in New Issue
	
	Block a user