Skip to content

Commit e5b6e1e

Browse files
reivilibresandhose
andcommitted
Add pre-migration checks to syn2mas (#3805)
This matches or exceeds `advisor.mts` from the old tool. Co-authored-by: Quentin Gliech <[email protected]>
1 parent 29fb1db commit e5b6e1e

File tree

14 files changed

+757
-57
lines changed

14 files changed

+757
-57
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cli/src/commands/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ enum Subcommand {
5252

5353
/// Migrate from Synapse's built-in auth system to MAS.
5454
#[clap(name = "syn2mas")]
55-
Syn2Mas(self::syn2mas::Options),
55+
// Box<> is to work around a 'large size difference between variants' lint
56+
Syn2Mas(Box<self::syn2mas::Options>),
5657
}
5758

5859
#[derive(Parser, Debug)]

crates/cli/src/commands/syn2mas.rs

Lines changed: 123 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
use std::process::ExitCode;
22

33
use anyhow::Context;
4+
use camino::Utf8PathBuf;
45
use clap::Parser;
56
use figment::Figment;
6-
use mas_config::{ConfigurationSectionExt, DatabaseConfig};
7+
use mas_config::{ConfigurationSection, ConfigurationSectionExt, DatabaseConfig, MatrixConfig};
78
use rand::thread_rng;
8-
use sqlx::{Connection, Either, PgConnection};
9-
use syn2mas::{LockedMasDatabase, MasWriter, SynapseReader};
9+
use sqlx::{postgres::PgConnectOptions, Connection, Either, PgConnection};
10+
use syn2mas::{synapse_config, LockedMasDatabase, MasWriter, SynapseReader};
1011
use tracing::{error, warn};
1112

1213
use crate::util::database_connection_from_config;
1314

15+
/// The exit code used by `syn2mas check` and `syn2mas migrate` when there are errors preventing migration.
16+
const EXIT_CODE_CHECK_ERRORS: u8 = 10;
17+
18+
/// The exit code used by `syn2mas check` when there are warnings which should be considered prior to migration.
19+
const EXIT_CODE_CHECK_WARNINGS: u8 = 11;
20+
1421
#[derive(Parser, Debug)]
1522
pub(super) struct Options {
1623
#[command(subcommand)]
@@ -22,11 +29,37 @@ pub(super) struct Options {
2229
/// If you want to migrate from Synapse to MAS today, please use the Node.js-based tool in the MAS repository.
2330
#[clap(long = "i-swear-i-am-just-testing-in-a-staging-environment")]
2431
experimental_accepted: bool,
32+
33+
/// Path to the Synapse configuration (in YAML format).
34+
/// May be specified multiple times if multiple Synapse configuration files are in use.
35+
#[clap(long = "synapse-config")]
36+
synapse_configuration_files: Vec<Utf8PathBuf>,
37+
38+
/// Override the Synapse database URI.
39+
/// syn2mas normally loads the Synapse database connection details from the Synapse configuration.
40+
/// However, it may sometimes be necessary to override the database URI and in that case this flag can be used.
41+
///
42+
/// Should be a connection URI of the following general form:
43+
/// ```text
44+
/// postgresql://[user[:password]@][host][:port][/dbname][?param1=value1&...]
45+
/// ```
46+
/// To use a UNIX socket at a custom path, the host should be a path to a socket, but in the URI string
47+
/// it must be URI-encoded by replacing `/` with `%2F`.
48+
///
49+
/// Finally, any missing values will be loaded from the libpq-compatible environment variables
50+
/// `PGHOST`, `PGPORT`, `PGUSER`, `PGDATABASE`, `PGPASSWORD`, etc.
51+
/// It is valid to specify the URL `postgresql:` and configure all values through those environment variables.
52+
#[clap(long = "synapse-database-uri")]
53+
synapse_database_uri: Option<PgConnectOptions>,
2554
}
2655

2756
#[derive(Parser, Debug)]
2857
enum Subcommand {
58+
/// Check the setup for potential problems before running a migration.
59+
///
60+
/// It is OK for Synapse to be online during these checks.
2961
Check,
62+
/// Perform a migration. Synapse must be offline during this process.
3063
Migrate,
3164
}
3265

@@ -41,8 +74,26 @@ impl Options {
4174
return Ok(ExitCode::FAILURE);
4275
}
4376

44-
// TODO allow configuring the synapse database location
45-
let mut syn_conn = PgConnection::connect("postgres:///fakesyn").await.unwrap();
77+
if self.synapse_configuration_files.is_empty() {
78+
error!("Please specify the path to the Synapse configuration file(s).");
79+
return Ok(ExitCode::FAILURE);
80+
}
81+
82+
let synapse_config = synapse_config::Config::load(&self.synapse_configuration_files)
83+
.context("Failed to load Synapse configuration")?;
84+
85+
// Establish a connection to Synapse's Postgres database
86+
let syn_connection_options = if let Some(db_override) = self.synapse_database_uri {
87+
db_override
88+
} else {
89+
synapse_config
90+
.database
91+
.to_sqlx_postgres()
92+
.context("Synapse configuration does not use Postgres, cannot migrate.")?
93+
};
94+
let mut syn_conn = PgConnection::connect_with(&syn_connection_options)
95+
.await
96+
.context("could not connect to Synapse Postgres database")?;
4697

4798
let config = DatabaseConfig::extract_or_default(figment)?;
4899

@@ -57,27 +108,79 @@ impl Options {
57108
return Ok(ExitCode::FAILURE);
58109
};
59110

111+
// Check configuration
112+
let (mut check_warnings, mut check_errors) = syn2mas::synapse_config_check(&synapse_config);
113+
{
114+
let (extra_warnings, extra_errors) =
115+
syn2mas::synapse_config_check_against_mas_config(&synapse_config, figment).await?;
116+
check_warnings.extend(extra_warnings);
117+
check_errors.extend(extra_errors);
118+
}
119+
120+
// Check databases
60121
syn2mas::mas_pre_migration_checks(&mut mas_connection).await?;
61-
syn2mas::synapse_pre_migration_checks(&mut syn_conn).await?;
122+
{
123+
let (extra_warnings, extra_errors) =
124+
syn2mas::synapse_database_check(&mut syn_conn, &synapse_config, figment).await?;
125+
check_warnings.extend(extra_warnings);
126+
check_errors.extend(extra_errors);
127+
}
128+
129+
// Display errors and warnings
130+
if !check_errors.is_empty() {
131+
eprintln!("===== Errors =====");
132+
eprintln!("These issues prevent migrating from Synapse to MAS right now:\n");
133+
for error in &check_errors {
134+
eprintln!("• {error}\n");
135+
}
136+
}
137+
if !check_warnings.is_empty() {
138+
eprintln!("===== Warnings =====");
139+
eprintln!("These potential issues should be considered before migrating from Synapse to MAS right now:\n");
140+
for warning in &check_warnings {
141+
eprintln!("• {warning}\n");
142+
}
143+
}
62144

63-
let mut reader = SynapseReader::new(&mut syn_conn, true).await?;
64-
let mut writer_mas_connections = Vec::with_capacity(NUM_WRITER_CONNECTIONS);
65-
for _ in 0..NUM_WRITER_CONNECTIONS {
66-
writer_mas_connections.push(database_connection_from_config(&config).await?);
145+
// Do not proceed if there are any errors
146+
if !check_errors.is_empty() {
147+
return Ok(ExitCode::from(EXIT_CODE_CHECK_ERRORS));
67148
}
68-
let mut writer = MasWriter::new(mas_connection, writer_mas_connections).await?;
69149

70-
// TODO is this rng ok?
71-
#[allow(clippy::disallowed_methods)]
72-
let mut rng = thread_rng();
150+
match self.subcommand {
151+
Subcommand::Check => {
152+
if !check_warnings.is_empty() {
153+
return Ok(ExitCode::from(EXIT_CODE_CHECK_WARNINGS));
154+
}
73155

74-
// TODO progress reporting
75-
// TODO allow configuring the server name
76-
syn2mas::migrate(&mut reader, &mut writer, "matrix.org", &mut rng).await?;
156+
println!("Check completed successfully with no errors or warnings.");
77157

78-
reader.finish().await?;
79-
writer.finish().await?;
158+
Ok(ExitCode::SUCCESS)
159+
}
160+
Subcommand::Migrate => {
161+
// TODO how should we handle warnings at this stage?
80162

81-
Ok(ExitCode::SUCCESS)
163+
let mut reader = SynapseReader::new(&mut syn_conn, true).await?;
164+
let mut writer_mas_connections = Vec::with_capacity(NUM_WRITER_CONNECTIONS);
165+
for _ in 0..NUM_WRITER_CONNECTIONS {
166+
writer_mas_connections.push(database_connection_from_config(&config).await?);
167+
}
168+
let mut writer = MasWriter::new(mas_connection, writer_mas_connections).await?;
169+
170+
// TODO is this rng ok?
171+
#[allow(clippy::disallowed_methods)]
172+
let mut rng = thread_rng();
173+
174+
// TODO progress reporting
175+
let mas_matrix = MatrixConfig::extract(figment)?;
176+
syn2mas::migrate(&mut reader, &mut writer, &mas_matrix.homeserver, &mut rng)
177+
.await?;
178+
179+
reader.finish().await?;
180+
writer.finish().await?;
181+
182+
Ok(ExitCode::SUCCESS)
183+
}
184+
}
82185
}
83186
}

crates/config/src/sections/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ pub use self::{
5151
ClaimsImports as UpstreamOAuth2ClaimsImports, DiscoveryMode as UpstreamOAuth2DiscoveryMode,
5252
EmailImportPreference as UpstreamOAuth2EmailImportPreference,
5353
ImportAction as UpstreamOAuth2ImportAction, PkceMethod as UpstreamOAuth2PkceMethod,
54-
ResponseMode as UpstreamOAuth2ResponseMode,
54+
Provider as UpstreamOAuth2Provider, ResponseMode as UpstreamOAuth2ResponseMode,
5555
TokenAuthMethod as UpstreamOAuth2TokenAuthMethod, UpstreamOAuth2Config,
5656
},
5757
};

crates/config/src/sections/passwords.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ fn default_bcrypt_cost() -> Option<u32> {
179179
}
180180

181181
/// A hashing algorithm
182-
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
182+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
183183
#[serde(rename_all = "lowercase")]
184184
pub enum Algorithm {
185185
/// bcrypt

crates/config/src/sections/upstream_oauth2.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ pub struct SignInWithApple {
391391
pub key_id: String,
392392
}
393393

394+
/// Configuration for one upstream OAuth 2 provider.
394395
#[skip_serializing_none]
395396
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
396397
pub struct Provider {
@@ -537,4 +538,21 @@ pub struct Provider {
537538
/// Orders of the keys are not preserved.
538539
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
539540
pub additional_authorization_parameters: BTreeMap<String, String>,
541+
542+
/// The ID of the provider that was used by Synapse.
543+
/// In order to perform a Synapse-to-MAS migration, this must be specified.
544+
///
545+
/// ## For providers that used OAuth 2.0 or OpenID Connect in Synapse
546+
///
547+
/// ### For `oidc_providers`:
548+
/// This should be specified as `oidc-` followed by the ID that was
549+
/// configured as `idp_id` in one of the `oidc_providers` in the Synapse
550+
/// configuration.
551+
/// For example, if Synapse's configuration contained `idp_id: wombat` for
552+
/// this provider, then specify `oidc-wombat` here.
553+
///
554+
/// ### For `oidc_config` (legacy):
555+
/// Specify `oidc` here.
556+
#[serde(skip_serializing_if = "Option::is_none")]
557+
pub synapse_idp_id: Option<String>,
540558
}

crates/syn2mas/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ repository.workspace = true
1010
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1111

1212
[dependencies]
13+
anyhow.workspace = true
14+
camino.workspace = true
15+
figment.workspace = true
16+
serde.workspace = true
1317
thiserror.workspace = true
1418
thiserror-ext.workspace = true
1519
tokio.workspace = true
@@ -30,5 +34,7 @@ anyhow.workspace = true
3034
insta.workspace = true
3135
serde.workspace = true
3236

37+
mas-config.workspace = true
38+
3339
[lints]
3440
workspace = true

crates/syn2mas/src/checks.rs

Lines changed: 0 additions & 30 deletions
This file was deleted.

crates/syn2mas/src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
mod mas_writer;
77
mod synapse_reader;
88

9-
mod checks;
109
mod migration;
1110

12-
pub use self::checks::synapse_pre_migration_checks;
1311
pub use self::mas_writer::locking::LockedMasDatabase;
1412
pub use self::mas_writer::{checks::mas_pre_migration_checks, MasWriter};
1513
pub use self::migration::migrate;
14+
pub use self::synapse_reader::checks::{
15+
synapse_config_check, synapse_config_check_against_mas_config, synapse_database_check,
16+
};
17+
pub use self::synapse_reader::config as synapse_config;
1618
pub use self::synapse_reader::SynapseReader;

crates/syn2mas/src/mas_writer/mod.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ pub struct MasNewUserPassword {
207207
pub created_at: DateTime<Utc>,
208208
}
209209

210+
/// The 'version' of the password hashing scheme used for passwords when they are
211+
/// migrated from Synapse to MAS.
212+
/// This is version 1, as in the previous syn2mas script.
213+
// TODO hardcoding version to `1` may not be correct long-term?
214+
pub const MIGRATED_PASSWORD_VERSION: u16 = 1;
215+
210216
/// List of all MAS tables that are written to by syn2mas.
211217
pub const MAS_TABLES_AFFECTED_BY_MIGRATION: &[&str] = &["users", "user_passwords"];
212218

@@ -578,8 +584,7 @@ impl<'conn> MasWriter<'conn> {
578584
user_ids.push(user_id);
579585
hashed_passwords.push(hashed_password);
580586
created_ats.push(created_at);
581-
// TODO hardcoding version to `1` may not be correct long-term?
582-
versions.push(1);
587+
versions.push(MIGRATED_PASSWORD_VERSION.into());
583588
}
584589

585590
sqlx::query!(

0 commit comments

Comments
 (0)