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