diff --git a/crates/cli/src/commands/server.rs b/crates/cli/src/commands/server.rs index 224adc08..0eba7a72 100644 --- a/crates/cli/src/commands/server.rs +++ b/crates/cli/src/commands/server.rs @@ -145,6 +145,7 @@ impl Options { &config.matrix, &config.experimental, &config.passwords, + &config.account, &config.captcha, )?; diff --git a/crates/cli/src/commands/templates.rs b/crates/cli/src/commands/templates.rs index fcfb4255..5597356a 100644 --- a/crates/cli/src/commands/templates.rs +++ b/crates/cli/src/commands/templates.rs @@ -17,8 +17,8 @@ use std::process::ExitCode; use clap::Parser; use figment::Figment; use mas_config::{ - BrandingConfig, CaptchaConfig, ConfigurationSection, ExperimentalConfig, MatrixConfig, - PasswordsConfig, TemplatesConfig, + AccountConfig, BrandingConfig, CaptchaConfig, ConfigurationSection, ExperimentalConfig, + MatrixConfig, PasswordsConfig, TemplatesConfig, }; use mas_storage::{Clock, SystemClock}; use rand::SeedableRng; @@ -50,6 +50,7 @@ impl Options { let matrix_config = MatrixConfig::extract(figment)?; let experimental_config = ExperimentalConfig::extract(figment)?; let password_config = PasswordsConfig::extract(figment)?; + let account_config = AccountConfig::extract(figment)?; let captcha_config = CaptchaConfig::extract(figment)?; let clock = SystemClock::default(); @@ -62,6 +63,7 @@ impl Options { &matrix_config, &experimental_config, &password_config, + &account_config, &captcha_config, )?; let templates = diff --git a/crates/cli/src/commands/worker.rs b/crates/cli/src/commands/worker.rs index 26234bdf..ca0e8e6e 100644 --- a/crates/cli/src/commands/worker.rs +++ b/crates/cli/src/commands/worker.rs @@ -54,6 +54,7 @@ impl Options { &config.matrix, &config.experimental, &config.passwords, + &config.account, &config.captcha, )?; diff --git a/crates/cli/src/util.rs b/crates/cli/src/util.rs index a91d2bf3..39b0e8fe 100644 --- a/crates/cli/src/util.rs +++ b/crates/cli/src/util.rs @@ -16,8 +16,9 @@ use std::time::Duration; use anyhow::Context; use mas_config::{ - BrandingConfig, CaptchaConfig, DatabaseConfig, EmailConfig, EmailSmtpMode, EmailTransportKind, - ExperimentalConfig, MatrixConfig, PasswordsConfig, PolicyConfig, TemplatesConfig, + AccountConfig, BrandingConfig, CaptchaConfig, DatabaseConfig, EmailConfig, EmailSmtpMode, + EmailTransportKind, ExperimentalConfig, MatrixConfig, PasswordsConfig, PolicyConfig, + TemplatesConfig, }; use mas_data_model::SiteConfig; use mas_email::{MailTransport, Mailer}; @@ -152,6 +153,7 @@ pub fn site_config_from_config( matrix_config: &MatrixConfig, experimental_config: &ExperimentalConfig, password_config: &PasswordsConfig, + account_config: &AccountConfig, captcha_config: &CaptchaConfig, ) -> Result { let captcha = captcha_config_from_config(captcha_config)?; @@ -164,13 +166,13 @@ pub fn site_config_from_config( imprint: branding_config.imprint.clone(), password_login_enabled: password_config.enabled(), password_registration_enabled: password_config.enabled() - && experimental_config.password_registration_enabled, - email_change_allowed: experimental_config.email_change_allowed, - displayname_change_allowed: experimental_config.displayname_change_allowed, + && account_config.password_registration_enabled, + email_change_allowed: account_config.email_change_allowed, + displayname_change_allowed: account_config.displayname_change_allowed, password_change_allowed: password_config.enabled() - && experimental_config.password_change_allowed, + && account_config.password_change_allowed, account_recovery_allowed: password_config.enabled() - && experimental_config.account_recovery_enabled, + && account_config.password_recovery_enabled, captcha, minimum_password_complexity: password_config.minimum_complexity(), }) diff --git a/crates/config/src/sections/account.rs b/crates/config/src/sections/account.rs new file mode 100644 index 00000000..fc87ce38 --- /dev/null +++ b/crates/config/src/sections/account.rs @@ -0,0 +1,99 @@ +// 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. + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::ConfigurationSection; + +const fn default_true() -> bool { + true +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +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 section to configure features related to account management +#[allow(clippy::struct_excessive_bools)] +#[derive(Clone, Debug, Deserialize, JsonSchema, Serialize)] +pub struct AccountConfig { + /// Whether users are allowed to change their email addresses. Defaults to + /// `true`. + #[serde(default = "default_true", skip_serializing_if = "is_default_true")] + pub email_change_allowed: bool, + + /// Whether users are allowed to change their display names. Defaults to + /// `true`. + /// + /// This should be in sync with the policy in the homeserver configuration. + #[serde(default = "default_true", skip_serializing_if = "is_default_true")] + pub displayname_change_allowed: bool, + + /// Whether to enable self-service password registration. Defaults to + /// `false` if password authentication is enabled. + /// + /// This has no effect if password login is disabled. + #[serde(default = "default_false", skip_serializing_if = "is_default_false")] + pub password_registration_enabled: bool, + + /// Whether users are allowed to change their passwords. Defaults to `true`. + /// + /// This has no effect if password login is disabled. + #[serde(default = "default_true", skip_serializing_if = "is_default_true")] + pub password_change_allowed: bool, + + /// Whether email-based password recovery is enabled. Defaults to `false`. + /// + /// This has no effect if password login is disabled. + #[serde(default = "default_false", skip_serializing_if = "is_default_false")] + pub password_recovery_enabled: bool, +} + +impl Default for AccountConfig { + fn default() -> Self { + Self { + email_change_allowed: default_true(), + displayname_change_allowed: default_true(), + password_registration_enabled: default_false(), + password_change_allowed: default_true(), + password_recovery_enabled: default_false(), + } + } +} + +impl AccountConfig { + /// Returns true if the configuration is the default one + pub(crate) fn is_default(&self) -> bool { + is_default_false(&self.password_registration_enabled) + && 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.password_recovery_enabled) + } +} + +impl ConfigurationSection for AccountConfig { + const PATH: Option<&'static str> = Some("account"); +} diff --git a/crates/config/src/sections/experimental.rs b/crates/config/src/sections/experimental.rs index 84ccf8c5..80dce762 100644 --- a/crates/config/src/sections/experimental.rs +++ b/crates/config/src/sections/experimental.rs @@ -27,24 +27,6 @@ fn is_default_token_ttl(value: &Duration) -> bool { *value == default_token_ttl() } -const fn default_true() -> bool { - true -} - -#[allow(clippy::trivially_copy_pass_by_ref)] -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. @@ -70,29 +52,6 @@ pub struct ExperimentalConfig { )] #[serde_as(as = "serde_with::DurationSeconds")] pub compat_token_ttl: Duration, - - /// Whether to enable self-service password registration. Defaults to `true` - /// if password authentication is enabled. - #[serde(default = "default_true", skip_serializing_if = "is_default_true")] - pub password_registration_enabled: bool, - - /// Whether users are allowed to change their email addresses. Defaults to - /// `true`. - #[serde(default = "default_true", skip_serializing_if = "is_default_true")] - pub email_change_allowed: bool, - - /// Whether users are allowed to change their display names. Defaults to - /// `true`. - #[serde(default = "default_true", skip_serializing_if = "is_default_true")] - pub displayname_change_allowed: bool, - - /// 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 { @@ -100,24 +59,13 @@ impl Default for ExperimentalConfig { Self { access_token_ttl: default_token_ttl(), compat_token_ttl: default_token_ttl(), - password_registration_enabled: default_true(), - email_change_allowed: default_true(), - displayname_change_allowed: default_true(), - password_change_allowed: default_true(), - account_recovery_enabled: default_false(), } } } impl ExperimentalConfig { pub(crate) fn is_default(&self) -> bool { - is_default_token_ttl(&self.access_token_ttl) - && is_default_token_ttl(&self.compat_token_ttl) - && is_default_true(&self.password_registration_enabled) - && 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) + is_default_token_ttl(&self.access_token_ttl) && is_default_token_ttl(&self.compat_token_ttl) } } diff --git a/crates/config/src/sections/mod.rs b/crates/config/src/sections/mod.rs index 2104f7a6..78e0e821 100644 --- a/crates/config/src/sections/mod.rs +++ b/crates/config/src/sections/mod.rs @@ -16,6 +16,7 @@ use rand::Rng; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +mod account; mod branding; mod captcha; mod clients; @@ -32,6 +33,7 @@ mod templates; mod upstream_oauth2; pub use self::{ + account::AccountConfig, branding::BrandingConfig, captcha::{CaptchaConfig, CaptchaServiceKind}, clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig}, @@ -113,6 +115,11 @@ pub struct RootConfig { #[serde(default, skip_serializing_if = "CaptchaConfig::is_default")] pub captcha: CaptchaConfig, + /// Configuration section to configure features related to account + /// management + #[serde(default, skip_serializing_if = "AccountConfig::is_default")] + pub account: AccountConfig, + /// Experimental configuration options #[serde(default, skip_serializing_if = "ExperimentalConfig::is_default")] pub experimental: ExperimentalConfig, @@ -133,6 +140,7 @@ impl ConfigurationSection for RootConfig { self.upstream_oauth2.validate(figment)?; self.branding.validate(figment)?; self.captcha.validate(figment)?; + self.account.validate(figment)?; self.experimental.validate(figment)?; Ok(()) @@ -163,6 +171,7 @@ impl RootConfig { upstream_oauth2: UpstreamOAuth2Config::default(), branding: BrandingConfig::default(), captcha: CaptchaConfig::default(), + account: AccountConfig::default(), experimental: ExperimentalConfig::default(), }) } @@ -184,6 +193,7 @@ impl RootConfig { upstream_oauth2: UpstreamOAuth2Config::default(), branding: BrandingConfig::default(), captcha: CaptchaConfig::default(), + account: AccountConfig::default(), experimental: ExperimentalConfig::default(), } } @@ -221,6 +231,9 @@ pub struct AppConfig { #[serde(default)] pub captcha: CaptchaConfig, + #[serde(default)] + pub account: AccountConfig, + #[serde(default)] pub experimental: ExperimentalConfig, } @@ -237,6 +250,7 @@ impl ConfigurationSection for AppConfig { self.policy.validate(figment)?; self.branding.validate(figment)?; self.captcha.validate(figment)?; + self.account.validate(figment)?; self.experimental.validate(figment)?; Ok(()) diff --git a/docs/config.schema.json b/docs/config.schema.json index 900203e6..887cea4d 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -193,6 +193,14 @@ } ] }, + "account": { + "description": "Configuration section to configure features related to account management", + "allOf": [ + { + "$ref": "#/definitions/AccountConfig" + } + ] + }, "experimental": { "description": "Experimental configuration options", "allOf": [ @@ -2107,6 +2115,32 @@ } ] }, + "AccountConfig": { + "description": "Configuration section to configure features related to account management", + "type": "object", + "properties": { + "email_change_allowed": { + "description": "Whether users are allowed to change their email addresses. Defaults to `true`.", + "type": "boolean" + }, + "displayname_change_allowed": { + "description": "Whether users are allowed to change their display names. Defaults to `true`.\n\nThis should be in sync with the policy in the homeserver configuration.", + "type": "boolean" + }, + "password_registration_enabled": { + "description": "Whether to enable self-service password registration. Defaults to `false` if password authentication is enabled.\n\nThis has no effect if password login is disabled.", + "type": "boolean" + }, + "password_change_allowed": { + "description": "Whether users are allowed to change their passwords. Defaults to `true`.\n\nThis has no effect if password login is disabled.", + "type": "boolean" + }, + "password_recovery_enabled": { + "description": "Whether email-based password recovery is enabled. Defaults to `false`.\n\nThis has no effect if password login is disabled.", + "type": "boolean" + } + } + }, "ExperimentalConfig": { "description": "Configuration sections for experimental options\n\nDo not change these options unless you know what you are doing.", "type": "object", @@ -2124,26 +2158,6 @@ "format": "uint64", "maximum": 86400.0, "minimum": 60.0 - }, - "password_registration_enabled": { - "description": "Whether to enable self-service password registration. Defaults to `true` if password authentication is enabled.", - "type": "boolean" - }, - "email_change_allowed": { - "description": "Whether users are allowed to change their email addresses. Defaults to `true`.", - "type": "boolean" - }, - "displayname_change_allowed": { - "description": "Whether users are allowed to change their display names. Defaults to `true`.", - "type": "boolean" - }, - "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" } } }