Skip to main content

mas_config/sections/
mod.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7use anyhow::bail;
8use camino::Utf8PathBuf;
9use rand::Rng;
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12
13mod account;
14mod branding;
15mod captcha;
16mod clients;
17mod database;
18mod email;
19mod experimental;
20mod http;
21mod matrix;
22mod oauth;
23mod passwords;
24mod policy;
25mod rate_limiting;
26mod secrets;
27mod telemetry;
28mod templates;
29mod upstream_oauth2;
30
31pub use self::{
32    account::AccountConfig,
33    branding::BrandingConfig,
34    captcha::{CaptchaConfig, CaptchaServiceKind},
35    clients::{ClientAuthMethodConfig, ClientConfig, ClientsConfig},
36    database::{DatabaseConfig, PgSslMode},
37    email::{EmailConfig, EmailSmtpMode, EmailTransportKind},
38    experimental::{ExperimentalConfig, SessionLimitConfig as ExperimentalSessionLimitConfig},
39    http::{
40        BindConfig as HttpBindConfig, HttpConfig, ListenerConfig as HttpListenerConfig,
41        Resource as HttpResource, TlsConfig as HttpTlsConfig, UnixOrTcp,
42    },
43    matrix::{HomeserverKind, MatrixConfig},
44    oauth::OAuthConfig,
45    passwords::{
46        Algorithm as PasswordAlgorithm, HashingScheme as PasswordHashingScheme, PasswordsConfig,
47    },
48    policy::PolicyConfig,
49    rate_limiting::RateLimitingConfig,
50    secrets::SecretsConfig,
51    telemetry::{
52        MetricsConfig, MetricsExporterKind, Propagator, TelemetryConfig, TracingConfig,
53        TracingExporterKind,
54    },
55    templates::TemplatesConfig,
56    upstream_oauth2::{
57        ClaimsImports as UpstreamOAuth2ClaimsImports, DiscoveryMode as UpstreamOAuth2DiscoveryMode,
58        EmailImportPreference as UpstreamOAuth2EmailImportPreference,
59        ImportAction as UpstreamOAuth2ImportAction,
60        OnBackchannelLogout as UpstreamOAuth2OnBackchannelLogout,
61        OnConflict as UpstreamOAuth2OnConflict, PkceMethod as UpstreamOAuth2PkceMethod,
62        Provider as UpstreamOAuth2Provider, ResponseMode as UpstreamOAuth2ResponseMode,
63        TokenAuthMethod as UpstreamOAuth2TokenAuthMethod, UpstreamOAuth2Config,
64    },
65};
66use crate::util::ConfigurationSection;
67
68/// Application configuration root
69#[derive(Debug, Serialize, Deserialize, JsonSchema)]
70pub struct RootConfig {
71    /// List of OAuth 2.0/OIDC clients config
72    #[serde(default, skip_serializing_if = "ClientsConfig::is_default")]
73    pub clients: ClientsConfig,
74
75    /// Configuration of the HTTP server
76    #[serde(default)]
77    pub http: HttpConfig,
78
79    /// Database connection configuration
80    #[serde(default)]
81    pub database: DatabaseConfig,
82
83    /// Configuration related to sending monitoring data
84    #[serde(default, skip_serializing_if = "TelemetryConfig::is_default")]
85    pub telemetry: TelemetryConfig,
86
87    /// Configuration related to templates
88    #[serde(default, skip_serializing_if = "TemplatesConfig::is_default")]
89    pub templates: TemplatesConfig,
90
91    /// Configuration related to sending emails
92    #[serde(default)]
93    pub email: EmailConfig,
94
95    /// Application secrets
96    pub secrets: SecretsConfig,
97
98    /// Configuration related to user passwords
99    #[serde(default)]
100    pub passwords: PasswordsConfig,
101
102    /// Configuration related to the homeserver
103    pub matrix: MatrixConfig,
104
105    /// Configuration related to the OPA policies
106    #[serde(default, skip_serializing_if = "PolicyConfig::is_default")]
107    pub policy: PolicyConfig,
108
109    /// Configuration related to limiting the rate of user actions to prevent
110    /// abuse
111    #[serde(default, skip_serializing_if = "RateLimitingConfig::is_default")]
112    pub rate_limiting: RateLimitingConfig,
113
114    /// Configuration related to upstream OAuth providers
115    #[serde(default, skip_serializing_if = "UpstreamOAuth2Config::is_default")]
116    pub upstream_oauth2: UpstreamOAuth2Config,
117
118    /// Configuration section for tweaking the branding of the service
119    #[serde(default, skip_serializing_if = "BrandingConfig::is_default")]
120    pub branding: BrandingConfig,
121
122    /// Configuration section to setup CAPTCHA protection on a few operations
123    #[serde(default, skip_serializing_if = "CaptchaConfig::is_default")]
124    pub captcha: CaptchaConfig,
125
126    /// Configuration section to configure features related to account
127    /// management
128    #[serde(default, skip_serializing_if = "AccountConfig::is_default")]
129    pub account: AccountConfig,
130
131    /// Configuration section for OAuth 2.0 protocol options
132    #[serde(default, skip_serializing_if = "OAuthConfig::is_default")]
133    pub oauth: OAuthConfig,
134
135    /// Experimental configuration options
136    #[serde(default, skip_serializing_if = "ExperimentalConfig::is_default")]
137    pub experimental: ExperimentalConfig,
138}
139
140impl ConfigurationSection for RootConfig {
141    fn validate(
142        &self,
143        figment: &figment::Figment,
144    ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
145        self.clients.validate(figment)?;
146        self.http.validate(figment)?;
147        self.database.validate(figment)?;
148        self.telemetry.validate(figment)?;
149        self.templates.validate(figment)?;
150        self.email.validate(figment)?;
151        self.passwords.validate(figment)?;
152        self.secrets.validate(figment)?;
153        self.matrix.validate(figment)?;
154        self.policy.validate(figment)?;
155        self.rate_limiting.validate(figment)?;
156        self.upstream_oauth2.validate(figment)?;
157        self.branding.validate(figment)?;
158        self.captcha.validate(figment)?;
159        self.account.validate(figment)?;
160        self.oauth.validate(figment)?;
161        self.experimental.validate(figment)?;
162
163        Ok(())
164    }
165}
166
167impl RootConfig {
168    /// Generate a new configuration with random secrets
169    ///
170    /// # Errors
171    ///
172    /// Returns an error if the secrets could not be generated
173    pub async fn generate<R>(mut rng: R) -> anyhow::Result<Self>
174    where
175        R: Rng + Send,
176    {
177        Ok(Self {
178            clients: ClientsConfig::default(),
179            http: HttpConfig::default(),
180            database: DatabaseConfig::default(),
181            telemetry: TelemetryConfig::default(),
182            templates: TemplatesConfig::default(),
183            email: EmailConfig::default(),
184            passwords: PasswordsConfig::default(),
185            secrets: SecretsConfig::generate(&mut rng).await?,
186            matrix: MatrixConfig::generate(&mut rng),
187            policy: PolicyConfig::default(),
188            rate_limiting: RateLimitingConfig::default(),
189            upstream_oauth2: UpstreamOAuth2Config::default(),
190            branding: BrandingConfig::default(),
191            captcha: CaptchaConfig::default(),
192            account: AccountConfig::default(),
193            oauth: OAuthConfig::default(),
194            experimental: ExperimentalConfig::default(),
195        })
196    }
197
198    /// Configuration used in tests
199    #[must_use]
200    pub fn test() -> Self {
201        Self {
202            clients: ClientsConfig::default(),
203            http: HttpConfig::default(),
204            database: DatabaseConfig::default(),
205            telemetry: TelemetryConfig::default(),
206            templates: TemplatesConfig::default(),
207            passwords: PasswordsConfig::default(),
208            email: EmailConfig::default(),
209            secrets: SecretsConfig::test(),
210            matrix: MatrixConfig::test(),
211            policy: PolicyConfig::default(),
212            rate_limiting: RateLimitingConfig::default(),
213            upstream_oauth2: UpstreamOAuth2Config::default(),
214            branding: BrandingConfig::default(),
215            captcha: CaptchaConfig::default(),
216            account: AccountConfig::default(),
217            oauth: OAuthConfig::default(),
218            experimental: ExperimentalConfig::default(),
219        }
220    }
221}
222
223/// Partial configuration actually used by the server
224#[allow(missing_docs)]
225#[derive(Debug, Deserialize)]
226pub struct AppConfig {
227    #[serde(default)]
228    pub http: HttpConfig,
229
230    #[serde(default)]
231    pub database: DatabaseConfig,
232
233    #[serde(default)]
234    pub templates: TemplatesConfig,
235
236    #[serde(default)]
237    pub email: EmailConfig,
238
239    pub secrets: SecretsConfig,
240
241    #[serde(default)]
242    pub passwords: PasswordsConfig,
243
244    pub matrix: MatrixConfig,
245
246    #[serde(default)]
247    pub policy: PolicyConfig,
248
249    #[serde(default)]
250    pub rate_limiting: RateLimitingConfig,
251
252    #[serde(default)]
253    pub branding: BrandingConfig,
254
255    #[serde(default)]
256    pub captcha: CaptchaConfig,
257
258    #[serde(default)]
259    pub account: AccountConfig,
260
261    #[serde(default)]
262    pub oauth: OAuthConfig,
263
264    #[serde(default)]
265    pub experimental: ExperimentalConfig,
266}
267
268impl ConfigurationSection for AppConfig {
269    fn validate(
270        &self,
271        figment: &figment::Figment,
272    ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
273        self.http.validate(figment)?;
274        self.database.validate(figment)?;
275        self.templates.validate(figment)?;
276        self.email.validate(figment)?;
277        self.passwords.validate(figment)?;
278        self.secrets.validate(figment)?;
279        self.matrix.validate(figment)?;
280        self.policy.validate(figment)?;
281        self.rate_limiting.validate(figment)?;
282        self.branding.validate(figment)?;
283        self.captcha.validate(figment)?;
284        self.account.validate(figment)?;
285        self.oauth.validate(figment)?;
286        self.experimental.validate(figment)?;
287
288        Ok(())
289    }
290}
291
292/// Partial config used by the `mas-cli config sync` command
293#[allow(missing_docs)]
294#[derive(Debug, Deserialize)]
295pub struct SyncConfig {
296    #[serde(default)]
297    pub database: DatabaseConfig,
298
299    pub secrets: SecretsConfig,
300
301    #[serde(default)]
302    pub clients: ClientsConfig,
303
304    #[serde(default)]
305    pub upstream_oauth2: UpstreamOAuth2Config,
306}
307
308impl ConfigurationSection for SyncConfig {
309    fn validate(
310        &self,
311        figment: &figment::Figment,
312    ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
313        self.database.validate(figment)?;
314        self.secrets.validate(figment)?;
315        self.clients.validate(figment)?;
316        self.upstream_oauth2.validate(figment)?;
317
318        Ok(())
319    }
320}
321
322/// Client secret config option.
323///
324/// It either holds the client secret value directly or references a file where
325/// the client secret is stored.
326#[derive(Clone, Debug)]
327pub enum ClientSecret {
328    /// Path to the file containing the client secret.
329    File(Utf8PathBuf),
330
331    /// Client secret value.
332    Value(String),
333}
334
335/// Client secret fields as serialized in JSON.
336#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
337pub struct ClientSecretRaw {
338    /// Path to the file containing the client secret. The client secret is used
339    /// by the `client_secret_basic`, `client_secret_post` and
340    /// `client_secret_jwt` authentication methods.
341    #[schemars(with = "Option<String>")]
342    #[serde(skip_serializing_if = "Option::is_none")]
343    client_secret_file: Option<Utf8PathBuf>,
344
345    /// Alternative to `client_secret_file`: Reads the client secret directly
346    /// from the config.
347    #[serde(skip_serializing_if = "Option::is_none")]
348    client_secret: Option<String>,
349}
350
351impl ClientSecret {
352    /// Returns the client secret.
353    ///
354    /// If `client_secret_file` was given, the secret is read from that file.
355    ///
356    /// # Errors
357    ///
358    /// Returns an error when the client secret could not be read from file.
359    pub async fn value(&self) -> anyhow::Result<String> {
360        Ok(match self {
361            ClientSecret::File(path) => tokio::fs::read_to_string(path).await?,
362            ClientSecret::Value(client_secret) => client_secret.clone(),
363        })
364    }
365}
366
367impl TryFrom<ClientSecretRaw> for Option<ClientSecret> {
368    type Error = anyhow::Error;
369
370    fn try_from(value: ClientSecretRaw) -> Result<Self, Self::Error> {
371        match (value.client_secret, value.client_secret_file) {
372            (None, None) => Ok(None),
373            (None, Some(path)) => Ok(Some(ClientSecret::File(path))),
374            (Some(client_secret), None) => Ok(Some(ClientSecret::Value(client_secret))),
375            (Some(_), Some(_)) => {
376                bail!("Cannot specify both `client_secret` and `client_secret_file`")
377            }
378        }
379    }
380}
381
382impl From<Option<ClientSecret>> for ClientSecretRaw {
383    fn from(value: Option<ClientSecret>) -> Self {
384        match value {
385            Some(ClientSecret::File(path)) => ClientSecretRaw {
386                client_secret_file: Some(path),
387                client_secret: None,
388            },
389            Some(ClientSecret::Value(client_secret)) => ClientSecretRaw {
390                client_secret_file: None,
391                client_secret: Some(client_secret),
392            },
393            None => ClientSecretRaw {
394                client_secret_file: None,
395                client_secret: None,
396            },
397        }
398    }
399}