1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-08-07 17:03:01 +03:00

Gate account recovery behing a configuration flag

This commit is contained in:
Quentin Gliech
2024-06-26 11:41:23 +02:00
parent 09fca9fd75
commit f9f2f4a3be
14 changed files with 152 additions and 13 deletions

View File

@@ -170,6 +170,8 @@ pub fn site_config_from_config(
displayname_change_allowed: experimental_config.displayname_change_allowed,
password_change_allowed: password_config.enabled()
&& experimental_config.password_change_allowed,
account_recovery_allowed: password_config.enabled()
&& experimental_config.account_recovery_enabled,
captcha,
})
}

View File

@@ -36,6 +36,15 @@ const fn is_default_true(value: &bool) -> bool {
*value == default_true()
}
const fn default_false() -> bool {
false
}
#[allow(clippy::trivially_copy_pass_by_ref)]
const fn is_default_false(value: &bool) -> bool {
*value == default_false()
}
/// Configuration sections for experimental options
///
/// Do not change these options unless you know what you are doing.
@@ -80,6 +89,10 @@ pub struct ExperimentalConfig {
/// Whether users are allowed to change their passwords. Defaults to `true`.
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
pub password_change_allowed: bool,
/// Whether email-based account recovery is enabled. Defaults to `false`.
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
pub account_recovery_enabled: bool,
}
impl Default for ExperimentalConfig {
@@ -91,6 +104,7 @@ impl Default for ExperimentalConfig {
email_change_allowed: default_true(),
displayname_change_allowed: default_true(),
password_change_allowed: default_true(),
account_recovery_enabled: default_false(),
}
}
}
@@ -103,6 +117,7 @@ impl ExperimentalConfig {
&& is_default_true(&self.email_change_allowed)
&& is_default_true(&self.displayname_change_allowed)
&& is_default_true(&self.password_change_allowed)
&& is_default_false(&self.account_recovery_enabled)
}
}

View File

@@ -73,6 +73,9 @@ pub struct SiteConfig {
/// Whether users can change their password.
pub password_change_allowed: bool,
/// Whether users can recover their account via email.
pub account_recovery_allowed: bool,
/// Captcha configuration
pub captcha: Option<CaptchaConfig>,
}

View File

@@ -133,6 +133,7 @@ pub fn test_site_config() -> SiteConfig {
email_change_allowed: true,
displayname_change_allowed: true,
password_change_allowed: true,
account_recovery_allowed: true,
captcha: None,
}
}

View File

@@ -23,12 +23,13 @@ use mas_axum_utils::{
csrf::{CsrfExt, ProtectedForm},
FancyError,
};
use mas_data_model::SiteConfig;
use mas_policy::Policy;
use mas_router::UrlBuilder;
use mas_storage::{BoxClock, BoxRepository, BoxRng};
use mas_templates::{
ErrorContext, FieldError, FormState, RecoveryFinishContext, RecoveryFinishFormField,
TemplateContext, Templates,
EmptyContext, ErrorContext, FieldError, FormState, RecoveryFinishContext,
RecoveryFinishFormField, TemplateContext, Templates,
};
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
@@ -50,11 +51,18 @@ pub(crate) async fn get(
mut rng: BoxRng,
clock: BoxClock,
mut repo: BoxRepository,
State(site_config): State<SiteConfig>,
State(templates): State<Templates>,
PreferredLanguage(locale): PreferredLanguage,
cookie_jar: CookieJar,
Query(query): Query<RouteQuery>,
) -> Result<Response, FancyError> {
if !site_config.account_recovery_allowed {
let context = EmptyContext.with_language(locale);
let rendered = templates.render_recovery_disabled(&context)?;
return Ok((cookie_jar, Html(rendered)).into_response());
}
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let ticket = repo
@@ -117,6 +125,7 @@ pub(crate) async fn post(
clock: BoxClock,
mut repo: BoxRepository,
mut policy: Policy,
State(site_config): State<SiteConfig>,
State(password_manager): State<PasswordManager>,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
@@ -125,6 +134,12 @@ pub(crate) async fn post(
Query(query): Query<RouteQuery>,
Form(form): Form<ProtectedForm<RouteForm>>,
) -> Result<Response, FancyError> {
if !site_config.account_recovery_allowed {
let context = EmptyContext.with_language(locale);
let rendered = templates.render_recovery_disabled(&context)?;
return Ok((cookie_jar, Html(rendered)).into_response());
}
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let ticket = repo

View File

@@ -22,12 +22,13 @@ use mas_axum_utils::{
csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt,
};
use mas_data_model::SiteConfig;
use mas_router::UrlBuilder;
use mas_storage::{
job::{JobRepositoryExt, SendAccountRecoveryEmailsJob},
BoxClock, BoxRepository, BoxRng,
};
use mas_templates::{RecoveryProgressContext, TemplateContext, Templates};
use mas_templates::{EmptyContext, RecoveryProgressContext, TemplateContext, Templates};
use ulid::Ulid;
use crate::PreferredLanguage;
@@ -36,12 +37,19 @@ pub(crate) async fn get(
mut rng: BoxRng,
clock: BoxClock,
mut repo: BoxRepository,
State(site_config): State<SiteConfig>,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
PreferredLanguage(locale): PreferredLanguage,
cookie_jar: CookieJar,
Path(id): Path<Ulid>,
) -> Result<Response, FancyError> {
if !site_config.account_recovery_allowed {
let context = EmptyContext.with_language(locale);
let rendered = templates.render_recovery_disabled(&context)?;
return Ok((cookie_jar, Html(rendered)).into_response());
}
let (session_info, cookie_jar) = cookie_jar.session_info();
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
@@ -75,6 +83,7 @@ pub(crate) async fn post(
mut rng: BoxRng,
clock: BoxClock,
mut repo: BoxRepository,
State(site_config): State<SiteConfig>,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
PreferredLanguage(locale): PreferredLanguage,
@@ -82,6 +91,12 @@ pub(crate) async fn post(
Path(id): Path<Ulid>,
Form(form): Form<ProtectedForm<()>>,
) -> Result<Response, FancyError> {
if !site_config.account_recovery_allowed {
let context = EmptyContext.with_language(locale);
let rendered = templates.render_recovery_disabled(&context)?;
return Ok((cookie_jar, Html(rendered)).into_response());
}
let (session_info, cookie_jar) = cookie_jar.session_info();
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);

View File

@@ -25,14 +25,15 @@ use mas_axum_utils::{
csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt,
};
use mas_data_model::UserAgent;
use mas_data_model::{SiteConfig, UserAgent};
use mas_router::UrlBuilder;
use mas_storage::{
job::{JobRepositoryExt, SendAccountRecoveryEmailsJob},
BoxClock, BoxRepository, BoxRng,
};
use mas_templates::{
FieldError, FormState, RecoveryStartContext, RecoveryStartFormField, TemplateContext, Templates,
EmptyContext, FieldError, FormState, RecoveryStartContext, RecoveryStartFormField,
TemplateContext, Templates,
};
use serde::{Deserialize, Serialize};
@@ -47,11 +48,18 @@ pub(crate) async fn get(
mut rng: BoxRng,
clock: BoxClock,
mut repo: BoxRepository,
State(site_config): State<SiteConfig>,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
PreferredLanguage(locale): PreferredLanguage,
cookie_jar: CookieJar,
) -> Result<Response, FancyError> {
if !site_config.account_recovery_allowed {
let context = EmptyContext.with_language(locale);
let rendered = templates.render_recovery_disabled(&context)?;
return Ok((cookie_jar, Html(rendered)).into_response());
}
let (session_info, cookie_jar) = cookie_jar.session_info();
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
@@ -78,12 +86,19 @@ pub(crate) async fn post(
mut repo: BoxRepository,
user_agent: TypedHeader<headers::UserAgent>,
activity_tracker: BoundActivityTracker,
State(site_config): State<SiteConfig>,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
PreferredLanguage(locale): PreferredLanguage,
cookie_jar: CookieJar,
Form(form): Form<ProtectedForm<StartRecoveryForm>>,
) -> Result<impl IntoResponse, FancyError> {
if !site_config.account_recovery_allowed {
let context = EmptyContext.with_language(locale);
let rendered = templates.render_recovery_disabled(&context)?;
return Ok((cookie_jar, Html(rendered)).into_response());
}
let (session_info, cookie_jar) = cookie_jar.session_info();
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);

View File

@@ -54,6 +54,7 @@ impl SiteConfigExt for SiteConfig {
SiteFeatures {
password_registration: self.password_registration_enabled,
password_login: self.password_login_enabled,
account_recovery: self.account_recovery_allowed,
}
}
}

View File

@@ -27,6 +27,9 @@ pub struct SiteFeatures {
/// Whether local password-based login is enabled.
pub password_login: bool,
/// Whether email-based account recovery is enabled.
pub account_recovery: bool,
}
impl Object for SiteFeatures {
@@ -34,11 +37,16 @@ impl Object for SiteFeatures {
match field.as_str()? {
"password_registration" => Some(Value::from(self.password_registration)),
"password_login" => Some(Value::from(self.password_login)),
"account_recovery" => Some(Value::from(self.account_recovery)),
_ => None,
}
}
fn enumerate(self: &Arc<Self>) -> Enumerator {
Enumerator::Str(&["password_registration", "password_login"])
Enumerator::Str(&[
"password_registration",
"password_login",
"account_recovery",
])
}
}

View File

@@ -357,6 +357,9 @@ register_templates! {
/// Render the account recovery finish page
pub fn render_recovery_finish(WithLanguage<WithCsrf<RecoveryFinishContext>>) { "pages/recovery/finish.html" }
/// Render the account recovery disabled page
pub fn render_recovery_disabled(WithLanguage<EmptyContext>) { "pages/recovery/disabled.html" }
/// Render the re-authentication form
pub fn render_reauth(WithLanguage<WithCsrf<WithSession<ReauthContext>>>) { "pages/reauth.html" }
@@ -425,6 +428,7 @@ impl Templates {
check::render_recovery_start(self, now, rng)?;
check::render_recovery_progress(self, now, rng)?;
check::render_recovery_finish(self, now, rng)?;
check::render_recovery_disabled(self, now, rng)?;
check::render_reauth(self, now, rng)?;
check::render_form_post::<EmptyContext>(self, now, rng)?;
check::render_error(self, now, rng)?;
@@ -455,6 +459,7 @@ mod tests {
let features = SiteFeatures {
password_login: true,
password_registration: true,
account_recovery: true,
};
let vite_manifest_path =
Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../frontend/dist/manifest.json");