Skip to main content

tuwunel_api/client/admin/mas/
provision_user.rs

1use 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
20/// # `POST /_synapse/mas/provision_user`
21pub(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	// Canonicalize up front so a malformed address fails before any mutation.
29	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
92/// Reconcile the user's bound email addresses to exactly `desired`. A desired
93/// address bound to another user is reassigned, its prior binding removed first
94/// so no dangling forward row survives.
95async 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(&current)
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}