tuwunel_api_mas/
provision_user.rs1use std::collections::BTreeSet;
2
3use axum::extract::State;
4use futures::{StreamExt, TryStreamExt};
5use ruma::{MilliSecondsSinceUnixEpoch, UserId, profile::ProfileFieldName, thirdparty::Medium};
6use synapse_admin_api::mas::provision_user::{Request, Response};
7use tuwunel_core::{
8 Result,
9 arrayvec::ArrayVec,
10 utils::{
11 IterStream, ReadyExt,
12 option::OptionExt,
13 stream::{TryBroadbandExt, automatic_width},
14 string::truncate_chars,
15 },
16 warn,
17};
18use tuwunel_service::{
19 profile::{MAX_DISPLAYNAME_LENGTH, ProfileValue, string_field},
20 threepid::canonicalize_email,
21 users::PASSWORD_SENTINEL,
22};
23
24use super::{Mas, local_user};
25use crate::Ruma;
26
27type ProfileWrites = ArrayVec<ProfileValue, 2>;
28
29pub(crate) async fn provision_user_route(
31 _mas: Mas,
32 State(services): State<crate::State>,
33 body: Ruma<Request>,
34) -> Result<Response> {
35 let user_id = local_user(services, &body.localpart)?;
36
37 let desired_emails: Option<BTreeSet<String>> = if body.unset_emails {
39 Some(BTreeSet::new())
40 } else {
41 body.set_emails
42 .as_deref()
43 .map(canonicalize_emails)
44 .transpose()?
45 };
46
47 let created = !services.users.exists(&user_id).await;
48 let profile = requested_profile(services, &user_id, &body).await;
49
50 let account = create_account(services, &user_id, created);
51
52 services
54 .profile
55 .set_profile_keys_after(&user_id, &profile, None, account)
56 .await?;
57
58 if let Some(desired) = desired_emails {
59 sync_emails(services, &user_id, desired).await?;
60 }
61
62 match body.locked {
63 | Some(true) => services
64 .users
65 .set_locked(&user_id, &services.globals.server_user),
66 | Some(false) => services.users.clear_locked(&user_id),
67 | None => {},
68 }
69
70 Ok(Response::new(created))
71}
72
73fn canonicalize_emails(addrs: &[String]) -> Result<BTreeSet<String>> {
74 addrs
75 .iter()
76 .map(|a| canonicalize_email(a))
77 .collect()
78}
79
80async fn requested_profile(
86 services: crate::State,
87 user_id: &UserId,
88 body: &Request,
89) -> ProfileWrites {
90 let displayname = body
91 .set_displayname
92 .as_deref()
93 .map_async(|name| fitted_displayname(services, user_id, name))
94 .await;
95
96 let touch_displayname = displayname.is_some() || body.unset_displayname;
97 let touch_avatar = body.set_avatar_url.is_some() || body.unset_avatar_url;
98 let avatar_url = body.set_avatar_url.as_deref();
99
100 [
101 touch_displayname.then(|| string_field(ProfileFieldName::DisplayName, displayname)),
102 touch_avatar.then(|| string_field(ProfileFieldName::AvatarUrl, avatar_url)),
103 ]
104 .into_iter()
105 .flatten()
106 .collect()
107}
108
109async fn fitted_displayname<'a>(
111 services: crate::State,
112 user_id: &UserId,
113 name: &'a str,
114) -> &'a str {
115 let fitted = truncate_chars(name, MAX_DISPLAYNAME_LENGTH);
116
117 if fitted.len() == name.len() {
118 return name;
119 }
120
121 let restated = services
122 .profile
123 .displayname(user_id)
124 .await
125 .is_ok_and(|stored| stored == name);
126
127 if restated { name } else { fitted }
128}
129
130#[tracing::instrument(level = "debug", skip_all)]
131async fn create_account(services: crate::State, user_id: &UserId, created: bool) -> Result {
132 if !created {
133 return Ok(());
134 }
135
136 services
137 .users
138 .create(user_id, Some(PASSWORD_SENTINEL), Some("oidc"))
139 .await
140}
141
142async fn sync_emails(
146 services: crate::State,
147 user_id: &UserId,
148 desired: BTreeSet<String>,
149) -> Result {
150 let current: BTreeSet<String> = services
151 .threepid
152 .get_bindings(user_id)
153 .ready_filter_map(|tpid| (tpid.medium == Medium::Email).then_some(tpid.address))
154 .collect()
155 .await;
156
157 current
158 .difference(&desired)
159 .stream()
160 .for_each_concurrent(automatic_width(), |address| {
161 services.threepid.del_binding(user_id, address)
162 })
163 .await;
164
165 let now = MilliSecondsSinceUnixEpoch::now();
166
167 desired
168 .difference(¤t)
169 .try_stream()
170 .broad_and_then(async |address| {
171 if let Some(prior) = services
172 .threepid
173 .user_id_for_email(address)
174 .await?
175 && prior != user_id
176 {
177 warn!(%user_id, %prior, "MAS provisioned an email bound to another user; reassigning");
178
179 services
180 .threepid
181 .del_binding(&prior, address)
182 .await;
183 }
184
185 services
186 .threepid
187 .put_binding(user_id, address, Medium::Email, now, now)
188 .await;
189
190 Ok(())
191 })
192 .try_collect::<()>()
193 .await
194}