You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-08-06 06:02:40 +03:00
Gate account recovery behing a configuration flag
This commit is contained in:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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>,
|
||||
}
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
@@ -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");
|
||||
|
@@ -2038,6 +2038,10 @@
|
||||
"password_change_allowed": {
|
||||
"description": "Whether users are allowed to change their passwords. Defaults to `true`.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"account_recovery_enabled": {
|
||||
"description": "Whether email-based account recovery is enabled. Defaults to `false`.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
{% from "components/idp_brand.html" import logo %}
|
||||
|
||||
{% block content %}
|
||||
<main class="flex flex-col gap-6">
|
||||
<main class="flex flex-col gap-10">
|
||||
{% if features.password_login %}
|
||||
<header class="page-heading">
|
||||
<div class="icon">
|
||||
@@ -59,6 +59,10 @@ limitations under the License.
|
||||
<input {{ field.attributes(f) }} class="cpd-text-control" type="password" autocomplete="password" required />
|
||||
{% endcall %}
|
||||
|
||||
{% if features.account_recovery %}
|
||||
{{ button.link_text(text=_("mas.login.forgot_password"), href="/recover", class="self-center") }}
|
||||
{% endif %}
|
||||
|
||||
{{ button.button(text=_("action.continue")) }}
|
||||
</form>
|
||||
|
||||
|
32
templates/pages/recovery/disabled.html
Normal file
32
templates/pages/recovery/disabled.html
Normal file
@@ -0,0 +1,32 @@
|
||||
{#
|
||||
Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
#}
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<header class="page-heading">
|
||||
<div class="icon invalid">
|
||||
{{ icon.lock_solid() }}
|
||||
</div>
|
||||
|
||||
<div class="header">
|
||||
<h1 class="title">{{ _("mas.recovery.disabled.heading") }}</h1>
|
||||
<p class="text">{{ _("mas.recovery.disabled.description") }}</p>
|
||||
</div>
|
||||
|
||||
{{ button.link_outline(text=_("action.back"), href="/login") }}
|
||||
</header>
|
||||
{% endblock content %}
|
@@ -1,16 +1,20 @@
|
||||
{
|
||||
"action": {
|
||||
"back": "Back",
|
||||
"@back": {
|
||||
"context": "pages/recovery/disabled.html:30:32-48"
|
||||
},
|
||||
"cancel": "Cancel",
|
||||
"@cancel": {
|
||||
"context": "pages/consent.html:75:11-29, pages/device_consent.html:132:13-31, pages/login.html:100:13-31, pages/policy_violation.html:52:13-31, pages/register.html:89:13-31"
|
||||
"context": "pages/consent.html:75:11-29, pages/device_consent.html:132:13-31, pages/login.html:104:13-31, pages/policy_violation.html:52:13-31, pages/register.html:89:13-31"
|
||||
},
|
||||
"continue": "Continue",
|
||||
"@continue": {
|
||||
"context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:63:28-48, pages/device_consent.html:129:13-33, pages/device_link.html:48:26-46, pages/login.html:62:30-50, pages/reauth.html:40:28-48, pages/recovery/start.html:45:26-46, pages/register.html:84:28-48, pages/sso.html:45:28-48"
|
||||
"context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:63:28-48, pages/device_consent.html:129:13-33, pages/device_link.html:48:26-46, pages/login.html:66:30-50, pages/reauth.html:40:28-48, pages/recovery/start.html:46:26-46, pages/register.html:84:28-48, pages/sso.html:45:28-48"
|
||||
},
|
||||
"create_account": "Create Account",
|
||||
"@create_account": {
|
||||
"context": "pages/login.html:72:35-61, pages/upstream_oauth2/do_register.html:157:26-52"
|
||||
"context": "pages/login.html:76:35-61, pages/upstream_oauth2/do_register.html:157:26-52"
|
||||
},
|
||||
"sign_in": "Sign in",
|
||||
"@sign_in": {
|
||||
@@ -294,17 +298,22 @@
|
||||
"login": {
|
||||
"call_to_register": "Don't have an account yet?",
|
||||
"@call_to_register": {
|
||||
"context": "pages/login.html:68:15-46"
|
||||
"context": "pages/login.html:72:15-46"
|
||||
},
|
||||
"continue_with_provider": "Continue with %(provider)s",
|
||||
"@continue_with_provider": {
|
||||
"context": "pages/login.html:87:13-65",
|
||||
"context": "pages/login.html:91:13-65",
|
||||
"description": "Button to log in with an upstream provider"
|
||||
},
|
||||
"description": "Please sign in to continue:",
|
||||
"@description": {
|
||||
"context": "pages/login.html:38:31-57"
|
||||
},
|
||||
"forgot_password": "Forgot password?",
|
||||
"@forgot_password": {
|
||||
"context": "pages/login.html:63:35-65",
|
||||
"description": "On the login page, link to the account recovery process"
|
||||
},
|
||||
"headline": "Sign in",
|
||||
"@headline": {
|
||||
"context": "pages/login.html:37:33-56"
|
||||
@@ -321,7 +330,7 @@
|
||||
},
|
||||
"no_login_methods": "No login methods available.",
|
||||
"@no_login_methods": {
|
||||
"context": "pages/login.html:94:11-42"
|
||||
"context": "pages/login.html:98:11-42"
|
||||
}
|
||||
},
|
||||
"navbar": {
|
||||
@@ -376,6 +385,16 @@
|
||||
}
|
||||
},
|
||||
"recovery": {
|
||||
"disabled": {
|
||||
"description": "If you have lost your credentials, please contact the administrator to recover your account.",
|
||||
"@description": {
|
||||
"context": "pages/recovery/disabled.html:27:25-63"
|
||||
},
|
||||
"heading": "Account recovery is disabled",
|
||||
"@heading": {
|
||||
"context": "pages/recovery/disabled.html:26:27-61"
|
||||
}
|
||||
},
|
||||
"finish": {
|
||||
"confirm": "Enter new password again",
|
||||
"@confirm": {
|
||||
|
Reference in New Issue
Block a user