tuwunel_api/client/admin/mas/
provision_user.rs1use std::collections::BTreeSet;
2
3use axum::extract::State;
4use futures::{StreamExt, TryStreamExt};
5use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, UserId, thirdparty::Medium};
6use synapse_admin_api::mas::provision_user::{Request, Response};
7use tuwunel_core::{
8 Result,
9 utils::{
10 IterStream, ReadyExt,
11 stream::{TryBroadbandExt, automatic_width},
12 },
13 warn,
14};
15use tuwunel_service::{threepid::canonicalize_email, users::PASSWORD_SENTINEL};
16
17use super::{Mas, local_user};
18use crate::Ruma;
19
20pub(crate) async fn provision_user_route(
22 _mas: Mas,
23 State(services): State<crate::State>,
24 body: Ruma<Request>,
25) -> Result<Response> {
26 let user_id = local_user(services, &body.localpart)?;
27
28 let desired_emails: Option<BTreeSet<String>> = if body.unset_emails {
30 Some(BTreeSet::new())
31 } else {
32 body.set_emails
33 .as_deref()
34 .map(canonicalize_emails)
35 .transpose()?
36 };
37
38 let created = !services.users.exists(&user_id).await;
39
40 if created {
41 services
42 .users
43 .create(&user_id, Some(PASSWORD_SENTINEL), Some("oidc"))
44 .await?;
45 }
46
47 let touch_displayname = body.set_displayname.is_some() || body.unset_displayname;
48 let touch_avatar = body.set_avatar_url.is_some() || body.unset_avatar_url;
49 if touch_displayname || touch_avatar {
50 if touch_displayname {
51 services
52 .profile
53 .set_displayname(&user_id, body.set_displayname.as_deref(), None)
54 .await?;
55 }
56
57 if touch_avatar {
58 let avatar = body
59 .set_avatar_url
60 .as_deref()
61 .map(<&MxcUri>::from);
62
63 services
64 .profile
65 .set_avatar_url(&user_id, avatar, None)
66 .await?;
67 }
68 }
69
70 if let Some(desired) = desired_emails {
71 sync_emails(services, &user_id, desired).await?;
72 }
73
74 match body.locked {
75 | Some(true) => services
76 .users
77 .set_locked(&user_id, &services.globals.server_user),
78 | Some(false) => services.users.clear_locked(&user_id),
79 | None => {},
80 }
81
82 Ok(Response::new(created))
83}
84
85fn canonicalize_emails(addrs: &[String]) -> Result<BTreeSet<String>> {
86 addrs
87 .iter()
88 .map(|a| canonicalize_email(a))
89 .collect()
90}
91
92async fn sync_emails(
96 services: crate::State,
97 user_id: &UserId,
98 desired: BTreeSet<String>,
99) -> Result {
100 let current: BTreeSet<String> = services
101 .threepid
102 .get_bindings(user_id)
103 .ready_filter_map(|tpid| (tpid.medium == Medium::Email).then_some(tpid.address))
104 .collect()
105 .await;
106
107 current
108 .difference(&desired)
109 .stream()
110 .for_each_concurrent(automatic_width(), |address| {
111 services.threepid.del_binding(user_id, address)
112 })
113 .await;
114
115 let now = MilliSecondsSinceUnixEpoch::now();
116
117 desired
118 .difference(¤t)
119 .try_stream()
120 .broad_and_then(async |address| {
121 if let Some(prior) = services
122 .threepid
123 .user_id_for_email(address)
124 .await? && prior != user_id
125 {
126 warn!(%user_id, %prior, "MAS provisioned an email bound to another user; reassigning");
127
128 services
129 .threepid
130 .del_binding(&prior, address)
131 .await;
132 }
133
134 services
135 .threepid
136 .put_binding(user_id, address, Medium::Email, now, now)
137 .await;
138
139 Ok(())
140 })
141 .try_collect::<()>()
142 .await
143}