tuwunel_service/migrations/
account_status.rs1use std::sync::Arc;
2
3use futures::TryStreamExt;
4use ruma::{OwnedUserId, UserId};
5use tuwunel_core::{
6 Result, err, info,
7 utils::{ReadyExt, option::OptionExt, stream::BroadbandExt},
8 warn,
9};
10use tuwunel_database::Map;
11
12use super::local_user_id;
13use crate::{
14 Services,
15 users::{PASSWORD_DISABLED, PASSWORD_SENTINEL},
16};
17
18pub(super) async fn migrate_account_status(services: &Services) -> Result {
29 let deactivated = services.db.open_cf("userid_deactivated")?;
30 let subjects = services.db.open_cf("openidsubject_localpart")?;
31
32 if let Some(deactivated) = deactivated.as_ref() {
33 adopt_deactivations(services, deactivated).await?;
34 }
35
36 if let Some(subjects) = subjects.as_ref() {
37 adopt_passwordless(services, subjects, deactivated.as_ref()).await?;
38 }
39
40 Ok(())
41}
42
43async fn adopt_deactivations(services: &Services, deactivated: &Arc<Map>) -> Result {
50 let userid_password = &services.db["userid_password"];
51 let cork = services.db.cork_and_sync();
52
53 let (adopted, unreadable) = deactivated
54 .keys::<&UserId>()
55 .map_ok(ToOwned::to_owned)
56 .broad_filter_map(async |account: Result<OwnedUserId>| {
57 let user_id = match account {
58 | Ok(user_id) => user_id,
59 | Err(e) => return Some(Err(e)),
60 };
61
62 match hash_empty(userid_password, &user_id).await {
63 | Ok(Some(false)) => Some(Ok(user_id)),
64 | Ok(_) => None,
65 | Err(e) => Some(Err(e)),
66 }
67 })
68 .ready_fold((0_usize, 0_usize), |counts, account| {
69 write_password(userid_password, PASSWORD_DISABLED, counts, account)
70 })
71 .await;
72
73 drop(cork);
74
75 if adopted > 0 {
76 info!(%adopted, "Adopted deactivated accounts from a foreign database");
77 }
78
79 unreadable
80 .eq(&0)
81 .then_some(())
82 .ok_or_else(|| err!(Database("{unreadable} accounts could not be read")))
83}
84
85async fn adopt_passwordless(
97 services: &Services,
98 subjects: &Arc<Map>,
99 deactivated: Option<&Arc<Map>>,
100) -> Result {
101 let userid_password = &services.db["userid_password"];
102 let server_name = services.globals.server_name();
103 let cork = services.db.cork_and_sync();
104
105 let (adopted, unreadable) = subjects
106 .stream()
107 .ready_filter_map(|subject: Result<(&str, &str)>| match subject {
108 | Ok((_, localpart)) => local_user_id(localpart, server_name).map(Ok),
109 | Err(e) => Some(Err(e)),
110 })
111 .broad_filter_map(async |account: Result<OwnedUserId>| {
112 let user_id = match account {
113 | Ok(user_id) => user_id,
114 | Err(e) => return Some(Err(e)),
115 };
116
117 match restorable(services, deactivated, &user_id).await {
118 | Ok(false) => None,
119 | Ok(true) => Some(Ok(user_id)),
120 | Err(e) => Some(Err(e)),
121 }
122 })
123 .ready_fold((0_usize, 0_usize), |counts, account| {
124 write_password(userid_password, PASSWORD_SENTINEL, counts, account)
125 })
126 .await;
127
128 drop(cork);
129
130 if adopted > 0 {
131 info!(%adopted, "Restored accounts authenticated elsewhere from a foreign database");
132 }
133
134 unreadable
135 .eq(&0)
136 .then_some(())
137 .ok_or_else(|| err!(Database("{unreadable} accounts could not be read")))
138}
139
140async fn restorable(
148 services: &Services,
149 deactivated: Option<&Arc<Map>>,
150 user_id: &UserId,
151) -> Result<bool> {
152 let userid_password = &services.db["userid_password"];
153 let passwordless = hash_empty(userid_password, user_id)
154 .await?
155 .is_some_and(|empty| empty);
156
157 let marked = match deactivated
158 .map_async(|deactivated| deactivated.exists(user_id))
159 .await
160 {
161 | None => false,
162 | Some(Ok(())) => true,
163 | Some(Err(e)) if e.is_not_found() => false,
164 | Some(Err(e)) => return Err(e),
165 };
166
167 Ok(passwordless && !marked)
168}
169
170async fn hash_empty(userid_password: &Arc<Map>, user_id: &UserId) -> Result<Option<bool>> {
177 match userid_password.get(user_id).await {
178 | Ok(hash) => Ok(Some(hash.is_empty())),
179 | Err(e) if e.is_not_found() => Ok(None),
180 | Err(e) => Err(e),
181 }
182}
183
184fn write_password(
187 userid_password: &Arc<Map>,
188 password: &str,
189 (adopted, unreadable): (usize, usize),
190 account: Result<OwnedUserId>,
191) -> (usize, usize) {
192 match account {
193 | Ok(user_id) => {
194 userid_password.insert(&user_id, password);
195
196 (adopted.saturating_add(1), unreadable)
197 },
198 | Err(e) => {
199 warn!(error = %e, "an account could not be read");
200
201 (adopted, unreadable.saturating_add(1))
202 },
203 }
204}