Skip to main content

tuwunel_api_admin/users/
create_or_modify.rs

1use 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
27/// # `PUT /_synapse/admin/v2/users/{user_id}`
28///
29/// Creates a local account or modifies an existing one. `user_type`,
30/// `external_ids` and `approved` are accepted and ignored (not persisted).
31pub(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	// A malformed address is refused before any field is written.
54	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	// The server user's profile stays editable here; its access does not.
63	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	// A refused profile value returns before the account or its credentials change.
71	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			// Reactivation writes a sentinel so a delegated-auth user can sign in again;
98			// a caller supplying a password has already reactivated the account above.
99			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
124/// Canonicalizes the email addresses among `threepids`.
125///
126/// Other media are dropped, since nothing stores them.
127fn 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
143// An empty value clears its field.
144fn 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
189/// Replaces the user's email bindings with exactly the addresses in `desired`.
190///
191/// The addresses must already be canonical emails.
192async 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(&current)
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}