Skip to main content

tuwunel_api/client/admin/users/
create_or_modify.rs

1use std::collections::BTreeSet;
2
3use axum::extract::State;
4use futures::StreamExt;
5use ruma::{MilliSecondsSinceUnixEpoch, MxcUri, UserId, thirdparty::Medium};
6use synapse_admin_api::users::create_or_modify::v2 as create_or_modify;
7use tuwunel_core::{
8	Err, Result,
9	utils::{IterStream, ReadyExt, stream::automatic_width},
10};
11use tuwunel_service::{threepid::canonicalize_email, users::PASSWORD_SENTINEL};
12
13use super::user_details;
14use crate::{
15	Ruma,
16	client::admin::{refuse_server_user, require_admin},
17};
18
19/// # `PUT /_synapse/admin/v2/users/{user_id}`
20///
21/// Creates a local account or modifies an existing one. `user_type`,
22/// `external_ids` and `approved` are accepted and ignored (not persisted).
23pub(crate) async fn admin_create_or_modify_route(
24	State(services): State<crate::State>,
25	body: Ruma<create_or_modify::Request>,
26) -> Result<create_or_modify::Response> {
27	let sender_user = body.sender_user();
28
29	require_admin(&services, sender_user).await?;
30
31	let user_id = &body.user_id;
32
33	if !services.globals.user_is_local(user_id) {
34		return Err!(Request(InvalidParam("Can only create or modify local users")));
35	}
36
37	if body.deactivated == Some(true) && body.locked == Some(true) {
38		return Err!(Request(InvalidParam("An account cannot be deactivated and locked")));
39	}
40
41	if body.admin == Some(false) && sender_user == body.user_id {
42		return Err!(Request(InvalidParam("You may not demote yourself.")));
43	}
44
45	let created = !services.users.exists(user_id).await;
46
47	// The server user's profile stays editable here; its access does not.
48	if created || changes_access(&body) {
49		refuse_server_user(services, user_id)?;
50	}
51
52	if created {
53		services
54			.users
55			.create(user_id, body.password.as_deref(), None)
56			.await?;
57	} else if let Some(password) = body.password.as_deref() {
58		services
59			.users
60			.set_password(user_id, Some(password))
61			.await?;
62
63		if body.logout_devices {
64			services
65				.users
66				.all_device_ids(user_id)
67				.map(ToOwned::to_owned)
68				.for_each_concurrent(automatic_width(), async |device_id| {
69					services
70						.users
71						.remove_device(user_id, &device_id)
72						.await;
73				})
74				.await;
75		}
76	}
77
78	if let Some(displayname) = body.displayname.as_deref() {
79		let displayname = (!displayname.is_empty()).then_some(displayname);
80
81		services
82			.profile
83			.set_displayname(user_id, displayname, None)
84			.await?;
85	}
86
87	if let Some(avatar_url) = body.avatar_url.as_deref() {
88		let avatar_url = (!avatar_url.is_empty()).then(|| <&MxcUri>::from(avatar_url));
89
90		services
91			.profile
92			.set_avatar_url(user_id, avatar_url, None)
93			.await?;
94	}
95
96	match body.admin {
97		| Some(true) => services.admin.make_user_admin(user_id).await?,
98		| Some(false) => services.admin.revoke_admin(user_id).await?,
99		| None => {},
100	}
101
102	match body.deactivated {
103		| Some(true) =>
104			services
105				.deactivate
106				.full_deactivate(user_id, false)
107				.await?,
108		| Some(false)
109			if services
110				.users
111				.is_deactivated(user_id)
112				.await
113				.unwrap_or(false) =>
114		{
115			// Reactivation writes a sentinel so a delegated-auth user can sign in again;
116			// a caller supplying a password has already reactivated the account above.
117			if body.password.is_none() {
118				services
119					.users
120					.set_password(user_id, Some(PASSWORD_SENTINEL))
121					.await?;
122			}
123		},
124		| _ => {},
125	}
126
127	match body.locked {
128		| Some(true) => services.users.set_locked(user_id, sender_user),
129		| Some(false) => services.users.clear_locked(user_id),
130		| None => {},
131	}
132
133	if let Some(threepids) = body.threepids.as_deref() {
134		replace_emails(services, user_id, threepids).await?;
135	}
136
137	let details = user_details(services, user_id).await;
138
139	Ok(create_or_modify::Response::new(details))
140}
141
142fn changes_access(body: &create_or_modify::Request) -> bool {
143	body.password.is_some()
144		|| body.deactivated.is_some()
145		|| body.locked.is_some()
146		|| body.admin.is_some()
147		|| body.threepids.is_some()
148}
149
150/// Replaces the user's email bindings with exactly the email threepids in
151/// `threepids`, canonicalizing each. Non-email media are ignored (no store).
152async fn replace_emails(
153	services: crate::State,
154	user_id: &UserId,
155	threepids: &[create_or_modify::ThirdPartyIdentifier],
156) -> Result {
157	let desired: BTreeSet<String> = threepids
158		.iter()
159		.filter(|tpid| tpid.medium == Medium::Email)
160		.map(|tpid| canonicalize_email(&tpid.address))
161		.collect::<Result<_>>()?;
162
163	let current: BTreeSet<String> = services
164		.threepid
165		.get_bindings(user_id)
166		.ready_filter_map(|tpid| (tpid.medium == Medium::Email).then_some(tpid.address))
167		.collect()
168		.await;
169
170	current
171		.difference(&desired)
172		.stream()
173		.for_each_concurrent(automatic_width(), |address| {
174			services.threepid.del_binding(user_id, address)
175		})
176		.await;
177
178	let now = MilliSecondsSinceUnixEpoch::now();
179
180	desired
181		.difference(&current)
182		.stream()
183		.for_each_concurrent(automatic_width(), |address| {
184			services
185				.threepid
186				.put_binding(user_id, address, Medium::Email, now, now)
187		})
188		.await;
189
190	Ok(())
191}