tuwunel_service/migrations/
mod.rs1use std::{cmp::Ordering, time::Duration};
8
9use futures::{FutureExt, StreamExt, TryStreamExt};
10use ruma::{OwnedUserId, ServerName, UserId};
11use tokio::time::sleep;
12use tuwunel_core::{
13 Err, Result, err, format_small_string, info,
14 itertools::Itertools,
15 result::NotFound,
16 smallstr::SmallString,
17 utils::{BoolExt, ReadyExt, TryReadyExt},
18 warn,
19};
20use tuwunel_database::Deserialized;
21
22use self::{
23 account_status::migrate_account_status,
24 clear_servername_status::clear_servername_status,
25 clear_state_local_error_memos::clear_state_local_error_memos,
26 email_bindings::migrate_email_bindings,
27 fix_bad_double_separator_in_state_cache::fix_bad_double_separator_in_state_cache,
28 fix_hashed_sentinel_passwords::fix_hashed_sentinel_passwords,
29 fix_readreceiptid_readreceipt_duplicates::fix_readreceiptid_readreceipt_duplicates,
30 fix_referencedevents_missing_sep::fix_referencedevents_missing_sep,
31 import_conduit_knocks::import_conduit_knocks,
32 injectivity::{fix as fix_injectivity, mark_clean as mark_clean_injectivity},
33 migrate_media::migrate_media,
34 migrate_profile_keys::migrate_profile_keys,
35 rebuild_roomid_tscount_pducount::rebuild_roomid_tscount_pducount,
36 remove_remote_media_userid::remove_remote_media_userid,
37 retroactively_fix_bad_data_from_roomuserid_joined::retroactively_fix_bad_data_from_roomuserid_joined,
38 split_conduit_highlight_counts::split_conduit_highlight_counts,
39 token_expiry::{migrate_token_expiry, restore_token_expiry},
40 upgrade_legacy_mediaid_user::upgrade_legacy_mediaid_user,
41};
42use crate::Services;
43
44mod account_status;
45mod clear_servername_status;
46mod clear_state_local_error_memos;
47mod conduit;
48mod email_bindings;
49mod fix_bad_double_separator_in_state_cache;
50mod fix_hashed_sentinel_passwords;
51mod fix_readreceiptid_readreceipt_duplicates;
52mod fix_referencedevents_missing_sep;
53mod import_conduit_knocks;
54mod injectivity;
55mod migrate_media;
56mod migrate_profile_keys;
57mod moderation;
58mod rebuild_roomid_tscount_pducount;
59mod remove_remote_media_userid;
60mod retroactively_fix_bad_data_from_roomuserid_joined;
61mod split_conduit_highlight_counts;
62mod token_expiry;
63mod upgrade_legacy_mediaid_user;
64
65#[cfg(test)]
66mod tests;
67
68pub(crate) const DATABASE_VERSION: u64 = 17;
75
76const SERVER_NAME_KEY: &[u8] = b"server_name";
77
78const FORCE_MIGRATION_DELAY: Duration = Duration::from_secs(15);
79
80const CLEAR_STATE_LOCAL_ERROR_MEMOS: &str = "clear_state_local_error_memos";
81
82const FOREIGN_LINEAGE_MARKER: &[u8] = b"populate_userroomid_leftstate_table";
87
88type UserIdBuf = SmallString<[u8; 48]>;
90
91pub(crate) async fn migrations(services: &Services) -> Result {
92 if services.config.force_migration {
93 warn!(
94 delay = ?FORCE_MIGRATION_DELAY,
95 "The force_migration option is set. THIS IS NOT INTENDED TO BE USED UNDER ANY \
96 NORMAL CIRCUMSTANCES AND YOU MAY BE CORRUPTING YOUR DATABASE BY PROCEEDING. \
97 Remove force_migration from the configuration to clear this warning; startup \
98 continues after the delay."
99 );
100
101 sleep(FORCE_MIGRATION_DELAY).await;
102 }
103
104 if !services.config.database_migrations {
105 if !marker_present(services, CLEAR_STATE_LOCAL_ERROR_MEMOS).await? {
106 return Err!(Config(
107 "database_migrations",
108 "Local state memo invalidation is pending. Enable database_migrations for one \
109 startup, then this setting may be disabled again."
110 ));
111 }
112
113 warn!("Skipping database migrations due to configuration...");
114 return Ok(());
115 }
116
117 services.server.check_running()?;
119
120 let users_count = services.users.count().await;
121 if users_count == 0 {
122 return fresh(services).await;
123 }
124
125 let foreign_lineage = is_foreign_lineage(services).await;
128
129 check_database_version(services, foreign_lineage).await?;
130 check_server_name(services).await?;
131
132 services.server.check_running()?;
133
134 fix_injectivity(services).await?;
137
138 let migrated = migrate(services, foreign_lineage).await;
139
140 services.server.progress.end();
141
142 migrated.inspect_err(|error| {
143 if error.is_interrupted() {
144 warn!(
145 "Stopped during database migrations. The steps that completed are recorded; the \
146 rest run on the next start."
147 );
148 }
149 })
150}
151
152async fn is_foreign_lineage(services: &Services) -> bool {
157 let global = &services.db["global"];
158
159 global.get(SERVER_NAME_KEY).await.is_not_found()
160 || global.get(FOREIGN_LINEAGE_MARKER).await.is_ok()
161}
162
163async fn check_database_version(services: &Services, foreign_lineage: bool) -> Result {
171 let discovered = services.globals.db.database_version().await;
172
173 if discovered < 13 {
174 return Err!(Database("Database schema version {discovered} is no longer supported"));
175 }
176
177 if discovered > DATABASE_VERSION && !foreign_lineage && !services.config.force_migration {
178 return Err!(Database(
179 "Database schema version {discovered} is newer than this build supports \
180 ({DATABASE_VERSION}). Upgrade tuwunel to a build supporting this database."
181 ));
182 }
183
184 Ok(())
185}
186
187async fn check_server_name(services: &Services) -> Result {
192 let server_name = &services.server.name;
193
194 let existing = services.db["global"]
195 .get(SERVER_NAME_KEY)
196 .await
197 .deserialized::<String>();
198
199 match existing {
200 | Err(_) => backfill_server_name(services).await,
201 | Ok(existing) if existing.eq(server_name) => Ok(()),
202 | Ok(existing) => Err!(Database(
203 "Database belongs to {existing}; configured server name is {server_name}. Cannot \
204 reuse."
205 )),
206 }
207}
208
209async fn backfill_server_name(services: &Services) -> Result {
213 let server_name = &services.server.name;
214
215 services
216 .users
217 .stream()
218 .ready_any(|user_id| services.globals.user_is_local(user_id))
219 .await
220 .into_option()
221 .ok_or_else(|| {
222 err!(Database(
223 "Database has no users from {server_name}; refusing to reuse with this \
224 server_name."
225 ))
226 })?;
227
228 services.db["global"].insert(SERVER_NAME_KEY, server_name.as_str());
229 info!(%server_name, "Stamped server_name marker on upgraded database");
230
231 Ok(())
232}
233
234async fn fresh(services: &Services) -> Result {
235 let db = &services.db;
236
237 services
238 .globals
239 .db
240 .bump_database_version(DATABASE_VERSION);
241
242 db["global"].insert(SERVER_NAME_KEY, services.server.name.as_str());
243 db["global"].insert("feat_sha256_media", []);
244 db["global"].insert("fix_pdu_missing_room_id", []);
245 db["global"].insert("fix_bad_double_separator_in_state_cache", []);
246 db["global"].insert("retroactively_fix_bad_data_from_roomuserid_joined", []);
247 db["global"].insert("fix_referencedevents_missing_sep", []);
248 db["global"].insert("fix_readreceiptid_readreceipt_duplicates", []);
249 db["global"].insert("fix_hashed_sentinel_passwords", []);
250 db["global"].insert("upgrade_legacy_mediaid_user", []);
251 db["global"].insert("remove_remote_media_userid", []);
252 db["global"].insert("rebuild_roomid_tscount_pducount", []);
253 db["global"].insert("rebuild_relatesto_typed", []);
254 db["global"].insert("migrate_profile_keys_to_useridprofilekey", []);
255 db["global"].insert("rebuild_thread_activity", []);
256 db["global"].insert("clear_servername_status", []);
257 db["global"].insert(CLEAR_STATE_LOCAL_ERROR_MEMOS, []);
258 db["global"].insert("adopt_foreign_account_status", []);
259 db["global"].insert("adopt_foreign_email_bindings", []);
260 db["global"].insert(token_expiry::RESTORE_MARKER, []);
261 db["global"].insert(token_expiry::ADOPT_MARKER, []);
262 mark_clean_injectivity(services);
263
264 if services.config.create_admin_room {
266 crate::admin::create_admin_room(services)
267 .boxed()
268 .await?;
269 }
270
271 warn!("Created new RocksDB database with version {DATABASE_VERSION}");
272
273 Ok(())
274}
275
276async fn migrate(services: &Services, foreign_lineage: bool) -> Result {
278 let db = &services.db;
279
280 services.server.check_running()?;
281
282 let target_version = DATABASE_VERSION;
283 let discovered = services.globals.db.database_version().await;
284
285 if foreign_lineage && discovered > target_version {
292 services
293 .globals
294 .db
295 .bump_database_version(target_version);
296 }
297
298 migrate_media(services).await?;
299
300 if pending(services, "fix_pdu_missing_room_id").await? {
301 conduit::migrate_conduit_pdus(services).await?;
302 db["global"].insert("fix_pdu_missing_room_id", []);
303 }
304
305 import_conduit_knocks(services).await?;
306 split_conduit_highlight_counts(services).await?;
307
308 if db
311 .open_cf("servernamemediaid_metadata")?
312 .is_some()
313 {
314 db["global"].insert("fix_bad_double_separator_in_state_cache", []);
315 db["global"].insert("retroactively_fix_bad_data_from_roomuserid_joined", []);
316 }
317
318 if pending(services, "fix_bad_double_separator_in_state_cache").await? {
319 fix_bad_double_separator_in_state_cache(services).await?;
320 }
321
322 if pending(services, "retroactively_fix_bad_data_from_roomuserid_joined").await? {
323 retroactively_fix_bad_data_from_roomuserid_joined(services).await?;
324 }
325
326 if pending(services, "fix_referencedevents_missing_sep").await? {
327 fix_referencedevents_missing_sep(services).await?;
328 }
329
330 if pending(services, "fix_readreceiptid_readreceipt_duplicates").await? {
331 fix_readreceiptid_readreceipt_duplicates(services).await?;
332 }
333
334 if pending(services, "fix_hashed_sentinel_passwords").await? {
335 fix_hashed_sentinel_passwords(services).await?;
336 }
337
338 if pending(services, "upgrade_legacy_mediaid_user").await? {
339 upgrade_legacy_mediaid_user(services).await?;
340 }
341
342 if pending(services, "remove_remote_media_userid").await? {
343 remove_remote_media_userid(services).await?;
344 }
345
346 if pending(services, "rebuild_roomid_tscount_pducount").await? {
347 rebuild_roomid_tscount_pducount(services).await?;
348 }
349
350 if pending(services, "rebuild_relatesto_typed").await? {
351 services
352 .pdu_metadata
353 .rebuild_typed_relations()
354 .await?;
355
356 db["global"].insert("rebuild_relatesto_typed", []);
357 }
358
359 if pending(services, "migrate_profile_keys_to_useridprofilekey").await? {
360 migrate_profile_keys(services).await?;
361 }
362
363 if pending(services, "rebuild_thread_activity").await? {
364 services.threads.rebuild_thread_activity().await?;
365
366 db["global"].insert("rebuild_thread_activity", []);
367 }
368
369 if pending(services, "clear_servername_status").await? {
370 clear_servername_status(services).await?;
371 }
372
373 if pending(services, CLEAR_STATE_LOCAL_ERROR_MEMOS).await? {
374 clear_state_local_error_memos(services).await?;
375 }
376
377 services.server.check_running()?;
378
379 services
383 .server
384 .progress
385 .begin("migrate_moderation");
386 moderation::migrate_moderation(services).await?;
387
388 if pending(services, "adopt_foreign_account_status").await? {
389 migrate_account_status(services).await?;
390
391 db["global"].insert("adopt_foreign_account_status", []);
392 }
393
394 if pending(services, "adopt_foreign_email_bindings").await? {
395 migrate_email_bindings(services).await?;
396
397 db["global"].insert("adopt_foreign_email_bindings", []);
398 }
399
400 until_finished(services, token_expiry::RESTORE_MARKER, restore_token_expiry).await?;
402 until_finished(services, token_expiry::ADOPT_MARKER, migrate_token_expiry).await?;
403
404 services.server.check_running()?;
405
406 services
410 .globals
411 .db
412 .bump_database_version(target_version);
413
414 match discovered.cmp(&target_version) {
415 | Ordering::Less =>
416 info!("Database: migrated schema version from {discovered} to {target_version}."),
417 | Ordering::Greater => warn!(
418 "Database: stamped schema version {target_version} over a higher discovered version \
419 {discovered} (forced downgrade or foreign import)."
420 ),
421 | Ordering::Equal => {},
422 }
423
424 warn_forbidden_names(services).await?;
425
426 info!("Loaded RocksDB database with schema version {DATABASE_VERSION}");
427
428 Ok(())
429}
430
431async fn until_finished<F>(services: &Services, marker: &'static str, pass: F) -> Result
436where
437 F: AsyncFnOnce(&Services) -> Result<bool>,
438{
439 if pending(services, marker).await? {
440 let finished = pass(services).await?;
441
442 if finished {
443 services.db["global"].insert(marker, []);
444 }
445 }
446
447 Ok(())
448}
449
450async fn warn_forbidden_names(services: &Services) -> Result {
456 services.server.check_running()?;
457
458 if !services.config.forbidden_usernames.is_empty() {
459 services
460 .server
461 .progress
462 .begin("scan_forbidden_usernames");
463
464 services
465 .users
466 .stream()
467 .map(|user_id| {
468 services
469 .server
470 .check_running()
471 .map(|()| user_id.to_owned())
472 })
473 .try_filter_map(async |user_id| {
474 Ok(services
475 .users
476 .is_active_local(&user_id)
477 .await
478 .then_some(user_id))
479 })
480 .ready_try_filter_map(|user_id| {
481 let patterns = &services.config.forbidden_usernames;
482 let matches = patterns.matches(user_id.localpart());
483 let matched_patterns = matches
484 .iter()
485 .map(|pattern_index| &patterns.patterns()[pattern_index])
486 .join(", ");
487
488 Ok(matches
489 .matched_any()
490 .then_some((user_id, matched_patterns)))
491 })
492 .ready_try_for_each(|(user_id, matched_patterns)| {
493 warn!("User {user_id} matches forbidden username patterns: {matched_patterns}");
494 Ok(())
495 })
496 .await?;
497 }
498
499 services.server.check_running()?;
500
501 if !services.config.forbidden_alias_names.is_empty() {
502 services
503 .server
504 .progress
505 .begin("scan_forbidden_alias_names");
506
507 services
508 .metadata
509 .iter_ids()
510 .map(|room_id| {
511 services
512 .server
513 .check_running()
514 .map(|()| room_id.to_owned())
515 })
516 .try_for_each(async |room_id| {
517 services
518 .alias
519 .local_aliases_for_room(&room_id)
520 .map(|room_alias| {
521 services
522 .server
523 .check_running()
524 .map(|()| room_alias)
525 })
526 .ready_try_for_each(|room_alias| {
527 let patterns = &services.config.forbidden_alias_names;
528 let matches = patterns.matches(room_alias.alias());
529 let matched_patterns = matches
530 .iter()
531 .map(|pattern_index| &patterns.patterns()[pattern_index])
532 .join(", ");
533
534 if matches.matched_any() {
535 warn!(
536 "Room {room_id} with alias {room_alias} matches the following \
537 forbidden alias name patterns: {matched_patterns}"
538 );
539 }
540
541 Ok(())
542 })
543 .await
544 })
545 .await?;
546 }
547
548 Ok(())
549}
550
551async fn pending(services: &Services, marker: &'static str) -> Result<bool> {
560 services.server.check_running()?;
561
562 let pending = !marker_present(services, marker).await?;
563
564 if pending {
565 services.server.progress.begin(marker);
566 }
567
568 Ok(pending)
569}
570
571pub(super) async fn marker_present(services: &Services, marker: &str) -> Result<bool> {
576 services.db["global"]
577 .get(marker)
578 .await
579 .optional()
580 .inspect_err(|error| warn!(%marker, %error, "Migration marker failed to read"))
581 .map(|stamp| stamp.is_some())
582}
583
584pub(crate) fn local_user_id(localpart: &str, server_name: &ServerName) -> Option<OwnedUserId> {
590 let user_id: UserIdBuf = format_small_string!("@{localpart}:{server_name}");
591
592 UserId::parse(user_id.as_str()).ok()
593}