1// Copyright 2025 New Vector Ltd.
2//
3// SPDX-License-Identifier: AGPL-3.0-only
4// Please see LICENSE in the repository root for full details.
56//! # Synapse Checks
7//!
8//! This module provides safety checks to run against a Synapse database before
9//! running the Synapse-to-MAS migration.
1011use figment::Figment;
12use mas_config::{
13 BrandingConfig, CaptchaConfig, ConfigurationSection, ConfigurationSectionExt, MatrixConfig,
14 PasswordAlgorithm, PasswordsConfig, UpstreamOAuth2Config,
15};
16use sqlx::{PgConnection, prelude::FromRow, query_as, query_scalar};
17use thiserror::Error;
1819use super::config::Config;
20use crate::mas_writer::MIGRATED_PASSWORD_VERSION;
2122#[derive(Debug, Error)]
23pub enum Error {
24#[error("query failed: {0}")]
25Sqlx(#[from] sqlx::Error),
2627#[error("failed to load MAS config: {0}")]
28MasConfig(#[from] figment::Error),
2930#[error("failed to load MAS password config: {0}")]
31MasPasswordConfig(#[source] anyhow::Error),
32}
3334/// An error found whilst checking the Synapse database, that should block a
35/// migration.
36#[derive(Debug, Error)]
37pub enum CheckError {
38#[error("MAS config is missing a password hashing scheme with version '1'")]
39MissingPasswordScheme,
4041#[error(
42"Password scheme version '1' in the MAS config must use the Bcrypt algorithm, so that Synapse passwords can be imported and will be compatible."
43)]
44PasswordSchemeNotBcrypt,
4546#[error(
47"Password scheme version '1' in the MAS config must have the same secret as the `pepper` value from Synapse, so that Synapse passwords can be imported and will be compatible."
48)]
49PasswordSchemeWrongPepper,
5051#[error(
52"Guest support is enabled in the Synapse configuration. Guests aren't supported by MAS, but if you don't have any then you could disable the option. See https://github.com/element-hq/matrix-authentication-service/issues/1445"
53)]
54GuestsEnabled,
5556#[error(
57"Synapse config has `enable_3pid_changes` explicitly enabled, which must be disabled or removed."
58)]
59ThreepidChangesEnabled,
6061#[error(
62"Synapse config has `login_via_existing_session.enabled` set to true, which must be disabled."
63)]
64LoginViaExistingSessionEnabled,
6566#[error(
67"MAS configuration has the wrong `matrix.homeserver` set ({mas:?}), it should match Synapse's `server_name` ({synapse:?})"
68)]
69ServerNameMismatch { synapse: String, mas: String },
7071#[error(
72"Synapse database contains {num_users} users associated to the OpenID Connect or OAuth2 provider '{provider}' but the Synapse configuration does not contain this provider."
73)]
74SynapseMissingOAuthProvider { provider: String, num_users: i64 },
7576#[error(
77"Synapse database has {num_users} mapping entries from a previously-configured MAS instance. If this is from a previous migration attempt, run the following SQL query against the Synapse database: `DELETE FROM user_external_ids WHERE auth_provider = 'oauth-delegated';` and then run the migration again."
78)]
79ExistingOAuthDelegated { num_users: i64 },
8081#[error(
82"Synapse config contains an OpenID Connect or OAuth2 provider '{provider}' (issuer: {issuer:?}) used by {num_users} users which must also be configured in the MAS configuration as an upstream provider."
83)]
84MasMissingOAuthProvider {
85 provider: String,
86 issuer: String,
87 num_users: i64,
88 },
89}
9091/// A potential hazard found whilst checking the Synapse database, that should
92/// be presented to the operator to check they are aware of a caveat before
93/// proceeding with the migration.
94#[derive(Debug, Error)]
95pub enum CheckWarning {
96#[error(
97"Synapse config contains OIDC auth configuration (issuer: {issuer:?}) which will need to be manually mapped to an upstream OpenID Connect Provider during migration."
98)]
99UpstreamOidcProvider { issuer: String },
100101#[error(
102"Synapse config contains {0} auth configuration which will need to be manually mapped as an upstream OAuth 2.0 provider during migration."
103)]
104ExternalAuthSystem(&'static str),
105106#[error(
107"Synapse config has registration enabled. This must be disabled after migration before bringing Synapse back online."
108)]
109DisableRegistrationAfterMigration,
110111#[error("Synapse config has `user_consent` enabled. This should be disabled after migration.")]
112DisableUserConsentAfterMigration,
113114#[error(
115"Synapse config has `user_consent` enabled but MAS has not been configured with terms of service. You may wish to set up a `tos_uri` in your MAS branding configuration to replace the user consent."
116)]
117ShouldPortUserConsentAsTerms,
118119#[error(
120"Synapse config has a registration CAPTCHA enabled, but no CAPTCHA has been configured in MAS. You may wish to manually configure this."
121)]
122ShouldPortRegistrationCaptcha,
123124#[error(
125"Synapse database contains {num_guests} guests which will be migrated are not supported by MAS. See https://github.com/element-hq/matrix-authentication-service/issues/1445"
126)]
127GuestsInDatabase { num_guests: i64 },
128129#[error(
130"Synapse database contains {num_non_email_3pids} non-email 3PIDs (probably phone numbers), which will be migrated but are not supported by MAS."
131)]
132NonEmailThreepidsInDatabase { num_non_email_3pids: i64 },
133}
134135/// Check that the Synapse configuration is sane for migration.
136#[must_use]
137pub fn synapse_config_check(synapse_config: &Config) -> (Vec<CheckWarning>, Vec<CheckError>) {
138let mut errors = Vec::new();
139let mut warnings = Vec::new();
140141if synapse_config.enable_registration {
142 warnings.push(CheckWarning::DisableRegistrationAfterMigration);
143 }
144if synapse_config.user_consent.is_some() {
145 warnings.push(CheckWarning::DisableUserConsentAfterMigration);
146 }
147148// TODO provide guidance on migrating these auth systems
149 // that are not directly supported as upstreams in MAS
150if synapse_config.cas_config.enabled {
151 warnings.push(CheckWarning::ExternalAuthSystem("CAS"));
152 }
153if synapse_config.saml2_config.enabled {
154 warnings.push(CheckWarning::ExternalAuthSystem("SAML2"));
155 }
156if synapse_config.jwt_config.enabled {
157 warnings.push(CheckWarning::ExternalAuthSystem("JWT"));
158 }
159if synapse_config.password_config.enabled && !synapse_config.password_config.localdb_enabled {
160 warnings.push(CheckWarning::ExternalAuthSystem(
161"non-standard password provider plugin",
162 ));
163 }
164165if synapse_config.enable_3pid_changes == Some(true) {
166 errors.push(CheckError::ThreepidChangesEnabled);
167 }
168169if synapse_config.login_via_existing_session.enabled {
170 errors.push(CheckError::LoginViaExistingSessionEnabled);
171 }
172173 (warnings, errors)
174}
175176/// Check that the given Synapse configuration is sane for migration to a MAS
177/// with the given MAS configuration.
178///
179/// # Errors
180///
181/// - If any necessary section of MAS config cannot be parsed.
182/// - If the MAS password configuration (including any necessary secrets) can't
183/// be loaded.
184pub async fn synapse_config_check_against_mas_config(
185 synapse: &Config,
186 mas: &Figment,
187) -> Result<(Vec<CheckWarning>, Vec<CheckError>), Error> {
188let mut errors = Vec::new();
189let mut warnings = Vec::new();
190191let mas_passwords = PasswordsConfig::extract_or_default(mas)?;
192let mas_password_schemes = mas_passwords
193 .load()
194 .await
195.map_err(Error::MasPasswordConfig)?;
196197let mas_matrix = MatrixConfig::extract(mas)?;
198199// Look for the MAS password hashing scheme that will be used for imported
200 // Synapse passwords, then check the configuration matches so that Synapse
201 // passwords will be compatible with MAS.
202if let Some((_, algorithm, _, secret)) = mas_password_schemes
203 .iter()
204 .find(|(version, _, _, _)| *version == MIGRATED_PASSWORD_VERSION)
205 {
206if algorithm != &PasswordAlgorithm::Bcrypt {
207 errors.push(CheckError::PasswordSchemeNotBcrypt);
208 }
209210let synapse_pepper = synapse
211 .password_config
212 .pepper
213 .as_ref()
214 .map(String::as_bytes);
215if secret.as_deref() != synapse_pepper {
216 errors.push(CheckError::PasswordSchemeWrongPepper);
217 }
218 } else {
219 errors.push(CheckError::MissingPasswordScheme);
220 }
221222if synapse.allow_guest_access {
223 errors.push(CheckError::GuestsEnabled);
224 }
225226if synapse.server_name != mas_matrix.homeserver {
227 errors.push(CheckError::ServerNameMismatch {
228 synapse: synapse.server_name.clone(),
229 mas: mas_matrix.homeserver.clone(),
230 });
231 }
232233let mas_captcha = CaptchaConfig::extract_or_default(mas)?;
234if synapse.enable_registration_captcha && mas_captcha.service.is_none() {
235 warnings.push(CheckWarning::ShouldPortRegistrationCaptcha);
236 }
237238let mas_branding = BrandingConfig::extract_or_default(mas)?;
239if synapse.user_consent.is_some() && mas_branding.tos_uri.is_none() {
240 warnings.push(CheckWarning::ShouldPortUserConsentAsTerms);
241 }
242243Ok((warnings, errors))
244}
245246/// Check that the Synapse database is sane for migration. Returns a list of
247/// warnings and errors.
248///
249/// # Errors
250///
251/// - If there is some database connection error, or the given database is not a
252/// Synapse database.
253/// - If the OAuth2 section of the MAS configuration could not be parsed.
254#[tracing::instrument(skip_all)]
255pub async fn synapse_database_check(
256 synapse_connection: &mut PgConnection,
257 synapse: &Config,
258 mas: &Figment,
259) -> Result<(Vec<CheckWarning>, Vec<CheckError>), Error> {
260#[derive(FromRow)]
261struct UpstreamOAuthProvider {
262 auth_provider: String,
263 num_users: i64,
264 }
265266let mut errors = Vec::new();
267let mut warnings = Vec::new();
268269let num_guests: i64 = query_scalar("SELECT COUNT(1) FROM users WHERE is_guest <> 0")
270 .fetch_one(&mut *synapse_connection)
271 .await?;
272if num_guests > 0 {
273 warnings.push(CheckWarning::GuestsInDatabase { num_guests });
274 }
275276let num_non_email_3pids: i64 =
277 query_scalar("SELECT COUNT(1) FROM user_threepids WHERE medium <> 'email'")
278 .fetch_one(&mut *synapse_connection)
279 .await?;
280if num_non_email_3pids > 0 {
281 warnings.push(CheckWarning::NonEmailThreepidsInDatabase {
282 num_non_email_3pids,
283 });
284 }
285286let oauth_provider_user_counts = query_as::<_, UpstreamOAuthProvider>(
287"
288 SELECT auth_provider, COUNT(*) AS num_users
289 FROM user_external_ids
290 GROUP BY auth_provider
291 ORDER BY auth_provider
292 ",
293 )
294 .fetch_all(&mut *synapse_connection)
295 .await?;
296if !oauth_provider_user_counts.is_empty() {
297let syn_oauth2 = synapse.all_oidc_providers();
298let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(mas)?;
299for row in oauth_provider_user_counts {
300// This is a special case of a previous migration attempt to MAS
301if row.auth_provider == "oauth-delegated" {
302 errors.push(CheckError::ExistingOAuthDelegated {
303 num_users: row.num_users,
304 });
305continue;
306 }
307308let matching_syn = syn_oauth2.get(&row.auth_provider);
309310let Some(matching_syn) = matching_syn else {
311 errors.push(CheckError::SynapseMissingOAuthProvider {
312 provider: row.auth_provider,
313 num_users: row.num_users,
314 });
315continue;
316 };
317318// Matching by `synapse_idp_id` is the same as what we'll do for the migration
319let matching_mas = mas_oauth2.providers.iter().find(|mas_provider| {
320 mas_provider.synapse_idp_id.as_ref() == Some(&row.auth_provider)
321 });
322323if matching_mas.is_none() {
324 errors.push(CheckError::MasMissingOAuthProvider {
325 provider: row.auth_provider,
326 issuer: matching_syn
327 .issuer
328 .clone()
329 .unwrap_or("<unspecified>".to_owned()),
330 num_users: row.num_users,
331 });
332 }
333 }
334 }
335336Ok((warnings, errors))
337}