1use 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;
18
19use super::config::Config;
20use crate::mas_writer::MIGRATED_PASSWORD_VERSION;
21
22#[derive(Debug, Error)]
23pub enum Error {
24 #[error("query failed: {0}")]
25 Sqlx(#[from] sqlx::Error),
26
27 #[error("failed to load MAS config: {0}")]
28 MasConfig(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
29
30 #[error("failed to load MAS password config: {0}")]
31 MasPasswordConfig(#[source] anyhow::Error),
32}
33
34#[derive(Debug, Error)]
37pub enum CheckError {
38 #[error("MAS config is missing a password hashing scheme with version '1'")]
39 MissingPasswordScheme,
40
41 #[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 )]
44 PasswordSchemeNotBcrypt,
45
46 #[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 )]
49 PasswordSchemeWrongPepper,
50
51 #[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 )]
54 GuestsEnabled,
55
56 #[error(
57 "Synapse config has `enable_3pid_changes` explicitly enabled, which must be disabled or removed."
58 )]
59 ThreepidChangesEnabled,
60
61 #[error(
62 "Synapse config has `login_via_existing_session.enabled` set to true, which must be disabled."
63 )]
64 LoginViaExistingSessionEnabled,
65
66 #[error(
67 "MAS configuration has the wrong `matrix.homeserver` set ({mas:?}), it should match Synapse's `server_name` ({synapse:?})"
68 )]
69 ServerNameMismatch { synapse: String, mas: String },
70
71 #[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 )]
74 SynapseMissingOAuthProvider { provider: String, num_users: i64 },
75
76 #[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 )]
79 ExistingOAuthDelegated { num_users: i64 },
80
81 #[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 )]
84 MasMissingOAuthProvider {
85 provider: String,
86 issuer: String,
87 num_users: i64,
88 },
89}
90
91#[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 )]
99 UpstreamOidcProvider { issuer: String },
100
101 #[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 )]
104 ExternalAuthSystem(&'static str),
105
106 #[error(
107 "Synapse config has registration enabled. This must be disabled after migration before bringing Synapse back online."
108 )]
109 DisableRegistrationAfterMigration,
110
111 #[error("Synapse config has `user_consent` enabled. This should be disabled after migration.")]
112 DisableUserConsentAfterMigration,
113
114 #[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 )]
117 ShouldPortUserConsentAsTerms,
118
119 #[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 )]
122 ShouldPortRegistrationCaptcha,
123
124 #[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 )]
127 GuestsInDatabase { num_guests: i64 },
128
129 #[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 )]
132 NonEmailThreepidsInDatabase { num_non_email_3pids: i64 },
133
134 #[error(
135 "Synapse database contains {num_users} users associated to the OpenID Connect or OAuth2 provider '{provider}' but the Synapse configuration does not contain this provider."
136 )]
137 SynapseMissingOAuthProvider { provider: String, num_users: i64 },
138}
139
140#[must_use]
142pub fn synapse_config_check(synapse_config: &Config) -> (Vec<CheckWarning>, Vec<CheckError>) {
143 let mut errors = Vec::new();
144 let mut warnings = Vec::new();
145
146 if synapse_config.enable_registration {
147 warnings.push(CheckWarning::DisableRegistrationAfterMigration);
148 }
149 if synapse_config.user_consent.is_some() {
150 warnings.push(CheckWarning::DisableUserConsentAfterMigration);
151 }
152
153 if synapse_config.cas_config.enabled {
156 warnings.push(CheckWarning::ExternalAuthSystem("CAS"));
157 }
158 if synapse_config.saml2_config.enabled {
159 warnings.push(CheckWarning::ExternalAuthSystem("SAML2"));
160 }
161 if synapse_config.jwt_config.enabled {
162 warnings.push(CheckWarning::ExternalAuthSystem("JWT"));
163 }
164 if synapse_config.password_config.enabled && !synapse_config.password_config.localdb_enabled {
165 warnings.push(CheckWarning::ExternalAuthSystem(
166 "non-standard password provider plugin",
167 ));
168 }
169
170 if synapse_config.enable_3pid_changes == Some(true) {
171 errors.push(CheckError::ThreepidChangesEnabled);
172 }
173
174 if synapse_config.login_via_existing_session.enabled {
175 errors.push(CheckError::LoginViaExistingSessionEnabled);
176 }
177
178 (warnings, errors)
179}
180
181pub async fn synapse_config_check_against_mas_config(
190 synapse: &Config,
191 mas: &Figment,
192) -> Result<(Vec<CheckWarning>, Vec<CheckError>), Error> {
193 let mut errors = Vec::new();
194 let mut warnings = Vec::new();
195
196 let mas_passwords = PasswordsConfig::extract_or_default(mas).map_err(Error::MasConfig)?;
197 let mas_password_schemes = mas_passwords
198 .load()
199 .await
200 .map_err(Error::MasPasswordConfig)?;
201
202 let mas_matrix = MatrixConfig::extract(mas).map_err(Error::MasConfig)?;
203
204 if let Some((_, algorithm, _, secret, _)) = mas_password_schemes
208 .iter()
209 .find(|(version, _, _, _, _)| *version == MIGRATED_PASSWORD_VERSION)
210 {
211 if algorithm != &PasswordAlgorithm::Bcrypt {
212 errors.push(CheckError::PasswordSchemeNotBcrypt);
213 }
214
215 let synapse_pepper = synapse
216 .password_config
217 .pepper
218 .as_ref()
219 .map(String::as_bytes);
220 if secret.as_deref() != synapse_pepper {
221 errors.push(CheckError::PasswordSchemeWrongPepper);
222 }
223 } else {
224 errors.push(CheckError::MissingPasswordScheme);
225 }
226
227 if synapse.allow_guest_access {
228 errors.push(CheckError::GuestsEnabled);
229 }
230
231 if synapse.server_name != mas_matrix.homeserver {
232 errors.push(CheckError::ServerNameMismatch {
233 synapse: synapse.server_name.clone(),
234 mas: mas_matrix.homeserver.clone(),
235 });
236 }
237
238 let mas_captcha = CaptchaConfig::extract_or_default(mas).map_err(Error::MasConfig)?;
239 if synapse.enable_registration_captcha && mas_captcha.service.is_none() {
240 warnings.push(CheckWarning::ShouldPortRegistrationCaptcha);
241 }
242
243 let mas_branding = BrandingConfig::extract_or_default(mas).map_err(Error::MasConfig)?;
244 if synapse.user_consent.is_some() && mas_branding.tos_uri.is_none() {
245 warnings.push(CheckWarning::ShouldPortUserConsentAsTerms);
246 }
247
248 Ok((warnings, errors))
249}
250
251#[tracing::instrument(skip_all)]
261pub async fn synapse_database_check(
262 synapse_connection: &mut PgConnection,
263 synapse: &Config,
264 mas: &Figment,
265 ignore_missing_auth_providers: bool,
266) -> Result<(Vec<CheckWarning>, Vec<CheckError>), Error> {
267 #[derive(FromRow)]
268 struct UpstreamOAuthProvider {
269 auth_provider: String,
270 num_users: i64,
271 }
272
273 let mut errors = Vec::new();
274 let mut warnings = Vec::new();
275
276 let num_guests: i64 = query_scalar("SELECT COUNT(1) FROM users WHERE is_guest <> 0")
277 .fetch_one(&mut *synapse_connection)
278 .await?;
279 if num_guests > 0 {
280 warnings.push(CheckWarning::GuestsInDatabase { num_guests });
281 }
282
283 let num_non_email_3pids: i64 =
284 query_scalar("SELECT COUNT(1) FROM user_threepids WHERE medium <> 'email'")
285 .fetch_one(&mut *synapse_connection)
286 .await?;
287 if num_non_email_3pids > 0 {
288 warnings.push(CheckWarning::NonEmailThreepidsInDatabase {
289 num_non_email_3pids,
290 });
291 }
292
293 let oauth_provider_user_counts = query_as::<_, UpstreamOAuthProvider>(
294 "
295 SELECT auth_provider, COUNT(*) AS num_users
296 FROM user_external_ids
297 GROUP BY auth_provider
298 ORDER BY auth_provider
299 ",
300 )
301 .fetch_all(&mut *synapse_connection)
302 .await?;
303 if !oauth_provider_user_counts.is_empty() {
304 let syn_oauth2 = synapse.all_oidc_providers();
305 let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(mas).map_err(Error::MasConfig)?;
306 for row in oauth_provider_user_counts {
307 if row.auth_provider == "oauth-delegated" {
309 errors.push(CheckError::ExistingOAuthDelegated {
310 num_users: row.num_users,
311 });
312 continue;
313 }
314
315 let matching_syn = syn_oauth2.get(&row.auth_provider);
316
317 let Some(matching_syn) = matching_syn else {
318 if ignore_missing_auth_providers {
319 warnings.push(CheckWarning::SynapseMissingOAuthProvider {
320 provider: row.auth_provider,
321 num_users: row.num_users,
322 });
323 } else {
324 errors.push(CheckError::SynapseMissingOAuthProvider {
325 provider: row.auth_provider,
326 num_users: row.num_users,
327 });
328 }
329 continue;
330 };
331
332 let matching_mas = mas_oauth2.providers.iter().find(|mas_provider| {
334 mas_provider.synapse_idp_id.as_ref() == Some(&row.auth_provider)
335 });
336
337 if matching_mas.is_none() {
338 errors.push(CheckError::MasMissingOAuthProvider {
339 provider: row.auth_provider,
340 issuer: matching_syn
341 .issuer
342 .clone()
343 .unwrap_or("<unspecified>".to_owned()),
344 num_users: row.num_users,
345 });
346 }
347 }
348 }
349
350 Ok((warnings, errors))
351}