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