syn2mas/synapse_reader/
checks.rs1use 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(#[from] figment::Error),
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
135#[must_use]
137pub fn synapse_config_check(synapse_config: &Config) -> (Vec<CheckWarning>, Vec<CheckError>) {
138 let mut errors = Vec::new();
139 let mut warnings = Vec::new();
140
141 if synapse_config.enable_registration {
142 warnings.push(CheckWarning::DisableRegistrationAfterMigration);
143 }
144 if synapse_config.user_consent.is_some() {
145 warnings.push(CheckWarning::DisableUserConsentAfterMigration);
146 }
147
148 if synapse_config.cas_config.enabled {
151 warnings.push(CheckWarning::ExternalAuthSystem("CAS"));
152 }
153 if synapse_config.saml2_config.enabled {
154 warnings.push(CheckWarning::ExternalAuthSystem("SAML2"));
155 }
156 if synapse_config.jwt_config.enabled {
157 warnings.push(CheckWarning::ExternalAuthSystem("JWT"));
158 }
159 if synapse_config.password_config.enabled && !synapse_config.password_config.localdb_enabled {
160 warnings.push(CheckWarning::ExternalAuthSystem(
161 "non-standard password provider plugin",
162 ));
163 }
164
165 if synapse_config.enable_3pid_changes == Some(true) {
166 errors.push(CheckError::ThreepidChangesEnabled);
167 }
168
169 if synapse_config.login_via_existing_session.enabled {
170 errors.push(CheckError::LoginViaExistingSessionEnabled);
171 }
172
173 (warnings, errors)
174}
175
176pub async fn synapse_config_check_against_mas_config(
185 synapse: &Config,
186 mas: &Figment,
187) -> Result<(Vec<CheckWarning>, Vec<CheckError>), Error> {
188 let mut errors = Vec::new();
189 let mut warnings = Vec::new();
190
191 let mas_passwords = PasswordsConfig::extract_or_default(mas)?;
192 let mas_password_schemes = mas_passwords
193 .load()
194 .await
195 .map_err(Error::MasPasswordConfig)?;
196
197 let mas_matrix = MatrixConfig::extract(mas)?;
198
199 if let Some((_, algorithm, _, secret)) = mas_password_schemes
203 .iter()
204 .find(|(version, _, _, _)| *version == MIGRATED_PASSWORD_VERSION)
205 {
206 if algorithm != &PasswordAlgorithm::Bcrypt {
207 errors.push(CheckError::PasswordSchemeNotBcrypt);
208 }
209
210 let synapse_pepper = synapse
211 .password_config
212 .pepper
213 .as_ref()
214 .map(String::as_bytes);
215 if secret.as_deref() != synapse_pepper {
216 errors.push(CheckError::PasswordSchemeWrongPepper);
217 }
218 } else {
219 errors.push(CheckError::MissingPasswordScheme);
220 }
221
222 if synapse.allow_guest_access {
223 errors.push(CheckError::GuestsEnabled);
224 }
225
226 if 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 }
232
233 let mas_captcha = CaptchaConfig::extract_or_default(mas)?;
234 if synapse.enable_registration_captcha && mas_captcha.service.is_none() {
235 warnings.push(CheckWarning::ShouldPortRegistrationCaptcha);
236 }
237
238 let mas_branding = BrandingConfig::extract_or_default(mas)?;
239 if synapse.user_consent.is_some() && mas_branding.tos_uri.is_none() {
240 warnings.push(CheckWarning::ShouldPortUserConsentAsTerms);
241 }
242
243 Ok((warnings, errors))
244}
245
246#[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)]
261 struct UpstreamOAuthProvider {
262 auth_provider: String,
263 num_users: i64,
264 }
265
266 let mut errors = Vec::new();
267 let mut warnings = Vec::new();
268
269 let num_guests: i64 = query_scalar("SELECT COUNT(1) FROM users WHERE is_guest <> 0")
270 .fetch_one(&mut *synapse_connection)
271 .await?;
272 if num_guests > 0 {
273 warnings.push(CheckWarning::GuestsInDatabase { num_guests });
274 }
275
276 let 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?;
280 if num_non_email_3pids > 0 {
281 warnings.push(CheckWarning::NonEmailThreepidsInDatabase {
282 num_non_email_3pids,
283 });
284 }
285
286 let 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?;
296 if !oauth_provider_user_counts.is_empty() {
297 let syn_oauth2 = synapse.all_oidc_providers();
298 let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(mas)?;
299 for row in oauth_provider_user_counts {
300 if row.auth_provider == "oauth-delegated" {
302 errors.push(CheckError::ExistingOAuthDelegated {
303 num_users: row.num_users,
304 });
305 continue;
306 }
307
308 let matching_syn = syn_oauth2.get(&row.auth_provider);
309
310 let Some(matching_syn) = matching_syn else {
311 errors.push(CheckError::SynapseMissingOAuthProvider {
312 provider: row.auth_provider,
313 num_users: row.num_users,
314 });
315 continue;
316 };
317
318 let matching_mas = mas_oauth2.providers.iter().find(|mas_provider| {
320 mas_provider.synapse_idp_id.as_ref() == Some(&row.auth_provider)
321 });
322
323 if 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 }
335
336 Ok((warnings, errors))
337}