Skip to main content

tuwunel_service/users/
server_user.rs

1use tuwunel_core::{
2	Err, Result,
3	config::ServerUserLocalpart,
4	err, implement,
5	utils::{BoolExt, result::NotFound},
6	warn,
7};
8use tuwunel_database::Deserialized;
9
10/// Key under which the server user's localpart is stamped at first boot.
11///
12/// Every later boot compares its configured localpart against the stamp.
13pub const SERVER_USER_KEY: &[u8] = b"server_user_localpart";
14
15/// Rejects changes to the server identity recorded by an existing database.
16///
17/// The localpart is stamped on the first boot and compared exactly afterwards.
18/// A database from before the stamp is backfilled from its admin room.
19#[implement(super::Service)]
20#[tracing::instrument(level = "debug", skip_all)]
21pub async fn validate_server_user(&self) -> Result {
22	let config = &self.services.server.config;
23	let configured = config.server_user_localpart.as_str();
24
25	let established = self.services.db["global"]
26		.get(SERVER_USER_KEY)
27		.await
28		.deserialized::<ServerUserLocalpart>()
29		.optional()?;
30
31	match established {
32		| None => self.backfill_server_user(configured).await,
33		| Some(established) if established == configured => Ok(()),
34		| Some(established) => Err!(Database(
35			"server_user_localpart is {configured} but this database established {established}; \
36			 changing it after first boot is unsupported"
37		)),
38	}
39}
40
41/// Stamps the identity on a database that pre-dates SERVER_USER_KEY.
42///
43/// Membership of the admin room establishes the identity, or an empty user
44/// table when there is no admin room; room creation does not, since a rebuilt
45/// admin room has a human creator. The legacy identity otherwise boots
46/// unstamped and any other is refused. A read-only database is validated but
47/// never stamped.
48#[implement(super::Service)]
49#[tracing::instrument(level = "debug", skip_all)]
50async fn backfill_server_user(&self, configured: &str) -> Result {
51	let established = match self.admin_room_joined().await? {
52		| Some(joined) => joined,
53		| None => self.is_empty().await?,
54	};
55
56	let legacy = configured == "conduit";
57
58	if !established {
59		return BoolExt::ok_or_else(legacy, || {
60			err!(Database(
61				"server_user_localpart is {configured} but neither the admin room nor an empty \
62				 user table establishes that identity; changing it after first boot is \
63				 unsupported"
64			))
65		});
66	}
67
68	if !self.services.globals.is_read_only() {
69		self.services.db["global"].insert(SERVER_USER_KEY, configured);
70	}
71
72	Ok(())
73}
74
75/// Reports whether the server user is joined to the admin room, or nothing
76/// without an admin alias.
77///
78/// An unjoined server user also means admin commands cannot work.
79#[implement(super::Service)]
80#[tracing::instrument(level = "debug", skip_all)]
81async fn admin_room_joined(&self) -> Result<Option<bool>> {
82	let services = &self.services;
83
84	let Some(room_id) = services
85		.alias
86		.resolve_local_alias(&services.admin.admin_alias)
87		.await
88		.optional()?
89	else {
90		return Ok(None);
91	};
92
93	let server_user = services.globals.server_user.as_ref();
94	let joined = services
95		.state_cache
96		.is_joined(server_user, &room_id)
97		.await;
98
99	if !joined {
100		warn!(
101			%room_id,
102			%server_user,
103			"The admin room does not include the server user; admin commands are unavailable \
104			 until it does"
105		);
106	}
107
108	Ok(Some(joined))
109}