tuwunel_api_admin/users/
create_or_modify.rs1use std::collections::BTreeSet;
2
3use axum::extract::State;
4use futures::StreamExt;
5use ruma::{MilliSecondsSinceUnixEpoch, UserId, profile::ProfileFieldName, thirdparty::Medium};
6use synapse_admin_api::users::create_or_modify::v2::{
7 self as create_or_modify, ThirdPartyIdentifier,
8};
9use tuwunel_core::{
10 Err, Result,
11 arrayvec::ArrayVec,
12 utils::{BoolExt, IterStream, ReadyExt, stream::automatic_width},
13};
14use tuwunel_service::{
15 profile::{ProfileValue, string_field},
16 threepid::canonicalize_email,
17 users::PASSWORD_SENTINEL,
18};
19
20use super::user_details;
21use crate::{Ruma, refuse_server_user, require_admin};
22
23type Emails = BTreeSet<String>;
24
25type ProfileWrites = ArrayVec<ProfileValue, 2>;
26
27pub(crate) async fn admin_create_or_modify_route(
32 State(services): State<crate::State>,
33 body: Ruma<create_or_modify::Request>,
34) -> Result<create_or_modify::Response> {
35 let sender_user = body.sender_user();
36
37 require_admin(&services, sender_user).await?;
38
39 let user_id = &body.user_id;
40
41 if !services.globals.user_is_local(user_id) {
42 return Err!(Request(InvalidParam("Can only create or modify local users")));
43 }
44
45 if body.deactivated == Some(true) && body.locked == Some(true) {
46 return Err!(Request(InvalidParam("An account cannot be deactivated and locked")));
47 }
48
49 if body.admin == Some(false) && sender_user == body.user_id {
50 return Err!(Request(InvalidParam("You may not demote yourself.")));
51 }
52
53 let emails = body
55 .threepids
56 .as_deref()
57 .map(canonical_emails)
58 .transpose()?;
59
60 let created = !services.users.exists(user_id).await;
61
62 if created || changes_access(&body) {
64 refuse_server_user(services, user_id)?;
65 }
66
67 let profile = requested_profile(&body);
68 let credentials = write_credentials(services, &body, created);
69
70 services
72 .profile
73 .set_profile_keys_after(user_id, &profile, None, credentials)
74 .await?;
75
76 match body.admin {
77 | None => {},
78 | Some(true) => services.admin.make_user_admin(user_id).await?,
79 | Some(false) => {
80 services.admin.revoke_admin(user_id).await?;
81 },
82 }
83
84 match body.deactivated {
85 | Some(true) =>
86 services
87 .deactivate
88 .full_deactivate(user_id, false)
89 .await?,
90 | Some(false)
91 if services
92 .users
93 .is_deactivated(user_id)
94 .await
95 .unwrap_or(false) =>
96 {
97 if body.password.is_none() {
100 services
101 .users
102 .set_password(user_id, Some(PASSWORD_SENTINEL))
103 .await?;
104 }
105 },
106 | _ => {},
107 }
108
109 match body.locked {
110 | Some(true) => services.users.set_locked(user_id, sender_user),
111 | Some(false) => services.users.clear_locked(user_id),
112 | None => {},
113 }
114
115 if let Some(emails) = emails {
116 replace_emails(services, user_id, &emails).await;
117 }
118
119 let details = user_details(services, user_id).await;
120
121 Ok(create_or_modify::Response::new(details))
122}
123
124fn canonical_emails(threepids: &[ThirdPartyIdentifier]) -> Result<Emails> {
128 threepids
129 .iter()
130 .filter(|tpid| tpid.medium == Medium::Email)
131 .map(|tpid| canonicalize_email(&tpid.address))
132 .collect()
133}
134
135fn changes_access(body: &create_or_modify::Request) -> bool {
136 body.password.is_some()
137 || body.deactivated.is_some()
138 || body.locked.is_some()
139 || body.admin.is_some()
140 || body.threepids.is_some()
141}
142
143fn requested_profile(body: &create_or_modify::Request) -> ProfileWrites {
145 [
146 (ProfileFieldName::DisplayName, &body.displayname),
147 (ProfileFieldName::AvatarUrl, &body.avatar_url),
148 ]
149 .into_iter()
150 .filter_map(|(name, value)| {
151 value
152 .as_deref()
153 .map(|value| string_field(name, value.is_empty().or_some(value)))
154 })
155 .collect()
156}
157
158#[tracing::instrument(level = "debug", skip_all)]
159async fn write_credentials(
160 services: crate::State,
161 body: &create_or_modify::Request,
162 created: bool,
163) -> Result {
164 let user_id = &body.user_id;
165
166 if created {
167 return services
168 .users
169 .create(user_id, body.password.as_deref(), None)
170 .await;
171 }
172
173 let Some(password) = body.password.as_deref() else {
174 return Ok(());
175 };
176
177 services
178 .users
179 .set_password(user_id, Some(password))
180 .await?;
181
182 if body.logout_devices {
183 services.users.remove_devices(user_id, None).await;
184 }
185
186 Ok(())
187}
188
189async fn replace_emails(services: crate::State, user_id: &UserId, desired: &Emails) {
193 let current: Emails = services
194 .threepid
195 .get_bindings(user_id)
196 .ready_filter(|tpid| tpid.medium == Medium::Email)
197 .map(|tpid| tpid.address)
198 .collect()
199 .await;
200
201 current
202 .difference(desired)
203 .stream()
204 .for_each_concurrent(automatic_width(), |address| {
205 services.threepid.del_binding(user_id, address)
206 })
207 .await;
208
209 let now = MilliSecondsSinceUnixEpoch::now();
210
211 desired
212 .difference(¤t)
213 .stream()
214 .for_each_concurrent(automatic_width(), |address| {
215 services
216 .threepid
217 .put_binding(user_id, address, Medium::Email, now, now)
218 })
219 .await;
220}