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 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."
78 )]
79 MasMissingOAuthProvider {
80 provider: String,
81 issuer: String,
82 num_users: i64,
83 },
84}
85
86#[derive(Debug, Error)]
90pub enum CheckWarning {
91 #[error(
92 "Synapse config contains OIDC auth configuration (issuer: {issuer:?}) which will need to be manually mapped to an upstream OpenID Connect Provider during migration."
93 )]
94 UpstreamOidcProvider { issuer: String },
95
96 #[error(
97 "Synapse config contains {0} auth configuration which will need to be manually mapped as an upstream OAuth 2.0 provider during migration."
98 )]
99 ExternalAuthSystem(&'static str),
100
101 #[error(
102 "Synapse config has registration enabled. This must be disabled after migration before bringing Synapse back online."
103 )]
104 DisableRegistrationAfterMigration,
105
106 #[error("Synapse config has `user_consent` enabled. This should be disabled after migration.")]
107 DisableUserConsentAfterMigration,
108
109 #[error(
110 "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."
111 )]
112 ShouldPortUserConsentAsTerms,
113
114 #[error(
115 "Synapse config has a registration CAPTCHA enabled, but no CAPTCHA has been configured in MAS. You may wish to manually configure this."
116 )]
117 ShouldPortRegistrationCaptcha,
118
119 #[error(
120 "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"
121 )]
122 GuestsInDatabase { num_guests: i64 },
123
124 #[error(
125 "Synapse database contains {num_non_email_3pids} non-email 3PIDs (probably phone numbers), which will be migrated but are not supported by MAS."
126 )]
127 NonEmailThreepidsInDatabase { num_non_email_3pids: i64 },
128}
129
130#[must_use]
132pub fn synapse_config_check(synapse_config: &Config) -> (Vec<CheckWarning>, Vec<CheckError>) {
133 let mut errors = Vec::new();
134 let mut warnings = Vec::new();
135
136 if synapse_config.enable_registration {
137 warnings.push(CheckWarning::DisableRegistrationAfterMigration);
138 }
139 if synapse_config.user_consent.is_some() {
140 warnings.push(CheckWarning::DisableUserConsentAfterMigration);
141 }
142
143 if synapse_config.cas_config.enabled {
146 warnings.push(CheckWarning::ExternalAuthSystem("CAS"));
147 }
148 if synapse_config.saml2_config.enabled {
149 warnings.push(CheckWarning::ExternalAuthSystem("SAML2"));
150 }
151 if synapse_config.jwt_config.enabled {
152 warnings.push(CheckWarning::ExternalAuthSystem("JWT"));
153 }
154 if synapse_config.password_config.enabled && !synapse_config.password_config.localdb_enabled {
155 warnings.push(CheckWarning::ExternalAuthSystem(
156 "non-standard password provider plugin",
157 ));
158 }
159
160 if synapse_config.enable_3pid_changes {
161 errors.push(CheckError::ThreepidChangesEnabled);
162 }
163
164 if synapse_config.login_via_existing_session.enabled {
165 errors.push(CheckError::LoginViaExistingSessionEnabled);
166 }
167
168 (warnings, errors)
169}
170
171pub async fn synapse_config_check_against_mas_config(
180 synapse: &Config,
181 mas: &Figment,
182) -> Result<(Vec<CheckWarning>, Vec<CheckError>), Error> {
183 let mut errors = Vec::new();
184 let mut warnings = Vec::new();
185
186 let mas_passwords = PasswordsConfig::extract_or_default(mas)?;
187 let mas_password_schemes = mas_passwords
188 .load()
189 .await
190 .map_err(Error::MasPasswordConfig)?;
191
192 let mas_matrix = MatrixConfig::extract(mas)?;
193
194 if let Some((_, algorithm, _, secret)) = mas_password_schemes
198 .iter()
199 .find(|(version, _, _, _)| *version == MIGRATED_PASSWORD_VERSION)
200 {
201 if algorithm != &PasswordAlgorithm::Bcrypt {
202 errors.push(CheckError::PasswordSchemeNotBcrypt);
203 }
204
205 let synapse_pepper = synapse
206 .password_config
207 .pepper
208 .as_ref()
209 .map(String::as_bytes);
210 if secret.as_deref() != synapse_pepper {
211 errors.push(CheckError::PasswordSchemeWrongPepper);
212 }
213 } else {
214 errors.push(CheckError::MissingPasswordScheme);
215 }
216
217 if synapse.allow_guest_access {
218 errors.push(CheckError::GuestsEnabled);
219 }
220
221 if synapse.server_name != mas_matrix.homeserver {
222 errors.push(CheckError::ServerNameMismatch {
223 synapse: synapse.server_name.clone(),
224 mas: mas_matrix.homeserver.clone(),
225 });
226 }
227
228 let mas_captcha = CaptchaConfig::extract_or_default(mas)?;
229 if synapse.enable_registration_captcha && mas_captcha.service.is_none() {
230 warnings.push(CheckWarning::ShouldPortRegistrationCaptcha);
231 }
232
233 let mas_branding = BrandingConfig::extract_or_default(mas)?;
234 if synapse.user_consent.is_some() && mas_branding.tos_uri.is_none() {
235 warnings.push(CheckWarning::ShouldPortUserConsentAsTerms);
236 }
237
238 Ok((warnings, errors))
239}
240
241#[tracing::instrument(skip_all)]
250pub async fn synapse_database_check(
251 synapse_connection: &mut PgConnection,
252 synapse: &Config,
253 mas: &Figment,
254) -> Result<(Vec<CheckWarning>, Vec<CheckError>), Error> {
255 #[derive(FromRow)]
256 struct UpstreamOAuthProvider {
257 auth_provider: String,
258 num_users: i64,
259 }
260
261 let mut errors = Vec::new();
262 let mut warnings = Vec::new();
263
264 let num_guests: i64 = query_scalar("SELECT COUNT(1) FROM users WHERE is_guest <> 0")
265 .fetch_one(&mut *synapse_connection)
266 .await?;
267 if num_guests > 0 {
268 warnings.push(CheckWarning::GuestsInDatabase { num_guests });
269 }
270
271 let num_non_email_3pids: i64 =
272 query_scalar("SELECT COUNT(1) FROM user_threepids WHERE medium <> 'email'")
273 .fetch_one(&mut *synapse_connection)
274 .await?;
275 if num_non_email_3pids > 0 {
276 warnings.push(CheckWarning::NonEmailThreepidsInDatabase {
277 num_non_email_3pids,
278 });
279 }
280
281 let oauth_provider_user_counts = query_as::<_, UpstreamOAuthProvider>(
282 "
283 SELECT auth_provider, COUNT(*) AS num_users
284 FROM user_external_ids
285 GROUP BY auth_provider
286 ORDER BY auth_provider
287 ",
288 )
289 .fetch_all(&mut *synapse_connection)
290 .await?;
291 if !oauth_provider_user_counts.is_empty() {
292 let syn_oauth2 = synapse.all_oidc_providers();
293 let mas_oauth2 = UpstreamOAuth2Config::extract_or_default(mas)?;
294 for row in oauth_provider_user_counts {
295 let matching_syn = syn_oauth2.get(&row.auth_provider);
296
297 let Some(matching_syn) = matching_syn else {
298 errors.push(CheckError::SynapseMissingOAuthProvider {
299 provider: row.auth_provider,
300 num_users: row.num_users,
301 });
302 continue;
303 };
304
305 let matching_mas = mas_oauth2.providers.iter().find(|mas_provider| {
307 mas_provider.synapse_idp_id.as_ref() == Some(&row.auth_provider)
308 });
309
310 if matching_mas.is_none() {
311 errors.push(CheckError::MasMissingOAuthProvider {
312 provider: row.auth_provider,
313 issuer: matching_syn
314 .issuer
315 .clone()
316 .unwrap_or("<unspecified>".to_owned()),
317 num_users: row.num_users,
318 });
319 }
320 }
321 }
322
323 Ok((warnings, errors))
324}