Skip to main content

tuwunel_admin/query/
oauth.rs

1use clap::Subcommand;
2use futures::{StreamExt, TryStreamExt};
3use ruma::OwnedUserId;
4use tuwunel_core::{
5	Err, Result, apply,
6	either::{Either, Left, Right},
7	err,
8	itertools::Itertools,
9	utils::stream::{IterStream, ReadyExt},
10};
11use tuwunel_service::oauth::{Provider, ProviderId, SessionId};
12
13use crate::{admin_command, admin_command_dispatch};
14
15#[admin_command_dispatch(handler_prefix = "oauth")]
16#[derive(Debug, Subcommand)]
17/// Query OAuth service state
18pub(crate) enum OauthCommand {
19	/// Associate existing user with future authorization claims.
20	Associate {
21		/// ID of configured provider to listen on.
22		provider: String,
23
24		/// MXID of local user to associate.
25		user_id: OwnedUserId,
26
27		/// List of claims to match in key=value format.
28		#[arg(long, required = true)]
29		claim: Vec<String>,
30	},
31
32	/// List configured OAuth providers.
33	ListProviders,
34
35	/// List users associated with any OAuth session
36	ListUsers,
37
38	/// List session ID's
39	ListSessions {
40		#[arg(long)]
41		user: Option<OwnedUserId>,
42	},
43
44	/// Show active configuration of a provider.
45	ShowProvider {
46		id: ProviderId,
47
48		#[arg(long)]
49		config: bool,
50	},
51
52	/// Show session state
53	ShowSession {
54		id: SessionId,
55	},
56
57	/// Show user sessions
58	ShowUser {
59		user_id: OwnedUserId,
60	},
61
62	/// Token introspection request to provider.
63	TokenInfo {
64		id: SessionId,
65	},
66
67	/// Revoke token for user_id or sess_id.
68	Revoke {
69		#[arg(value_parser = session_or_user_id)]
70		id: Either<SessionId, OwnedUserId>,
71	},
72
73	/// Remove oauth state (DANGER!)
74	Delete {
75		#[arg(value_parser = session_or_user_id)]
76		id: Either<SessionId, OwnedUserId>,
77
78		#[arg(long)]
79		force: bool,
80	},
81}
82
83type SessionOrUserId = Either<SessionId, OwnedUserId>;
84
85fn session_or_user_id(input: &str) -> Result<SessionOrUserId> {
86	OwnedUserId::parse(input)
87		.map(Right)
88		.or_else(|_| Ok(Left(input.to_owned())))
89}
90
91#[admin_command]
92pub(super) async fn oauth_associate(
93	&self,
94	provider: String,
95	user_id: OwnedUserId,
96	claim: Vec<String>,
97) -> Result {
98	if !self.services.globals.user_is_local(&user_id) {
99		return Err!(Request(NotFound("User {user_id:?} does not belong to this server.")));
100	}
101
102	if !self.services.users.exists(&user_id).await {
103		return Err!(Request(NotFound("User {user_id:?} is not registered")));
104	}
105
106	let provider = self
107		.services
108		.oauth
109		.providers
110		.get(&provider)
111		.await?;
112
113	let claim = claim
114		.iter()
115		.map(|kv| {
116			let (key, val) = kv
117				.split_once('=')
118				.ok_or_else(|| err!("Missing '=' in --claim {kv}=???"))?;
119
120			if !key.is_empty() && !val.is_empty() {
121				Ok((key, val))
122			} else {
123				Err!("Missing key or value in --claim=key=value argument")
124			}
125		})
126		.map_ok(apply!(2, ToOwned::to_owned))
127		.collect::<Result<_>>()?;
128
129	let _replaced = self
130		.services
131		.oauth
132		.sessions
133		.set_user_association_pending(provider.id(), &user_id, claim);
134
135	Ok(())
136}
137
138#[admin_command]
139pub(super) async fn oauth_list_providers(&self) -> Result {
140	self.services
141		.config
142		.identity_provider
143		.values()
144		.try_stream()
145		.map_ok(Provider::id)
146		.map_ok(|id| format!("{id}\n"))
147		.try_for_each(async |id| self.write_str(&id).await)
148		.await
149}
150
151#[admin_command]
152pub(super) async fn oauth_list_users(&self) -> Result {
153	self.services
154		.oauth
155		.sessions
156		.users()
157		.map(|id| format!("{id}\n"))
158		.map(Ok)
159		.try_for_each(async |id: String| self.write_str(&id).await)
160		.await
161}
162
163#[admin_command]
164pub(super) async fn oauth_list_sessions(&self, user_id: Option<OwnedUserId>) -> Result {
165	if let Some(user_id) = user_id.as_deref() {
166		return self
167			.services
168			.oauth
169			.sessions
170			.get_sess_id_by_user(user_id)
171			.map_ok(|id| format!("{id}\n"))
172			.try_for_each(async |id: String| self.write_str(&id).await)
173			.await;
174	}
175
176	self.services
177		.oauth
178		.sessions
179		.stream()
180		.ready_filter_map(|sess| sess.sess_id)
181		.map(|sess_id| format!("{sess_id:?}\n"))
182		.for_each(async |id: String| {
183			self.write_str(&id).await.ok();
184		})
185		.await;
186
187	Ok(())
188}
189
190#[admin_command]
191pub(super) async fn oauth_show_provider(&self, id: ProviderId, config: bool) -> Result {
192	if config {
193		let config = self.services.oauth.providers.get_config(&id)?;
194
195		self.write_str(&format!("{config:#?}\n")).await?;
196		return Ok(());
197	}
198
199	let provider = self.services.oauth.providers.get(&id).await?;
200
201	self.write_str(&format!("{provider:#?}\n")).await
202}
203
204#[admin_command]
205pub(super) async fn oauth_show_session(&self, id: SessionId) -> Result {
206	let session = self.services.oauth.sessions.get(&id).await?;
207
208	self.write_str(&format!("{session:#?}\n")).await
209}
210
211#[admin_command]
212pub(super) async fn oauth_show_user(&self, user_id: OwnedUserId) -> Result {
213	self.services
214		.oauth
215		.sessions
216		.get_sess_id_by_user(&user_id)
217		.try_for_each(async |id| {
218			let session = self.services.oauth.sessions.get(&id).await?;
219
220			self.write_str(&format!("{session:#?}\n")).await
221		})
222		.await
223}
224
225#[admin_command]
226pub(super) async fn oauth_token_info(&self, id: SessionId) -> Result {
227	let session = self.services.oauth.sessions.get(&id).await?;
228
229	let provider = self
230		.services
231		.oauth
232		.sessions
233		.provider(&session)
234		.await?;
235
236	let tokeninfo = self
237		.services
238		.oauth
239		.request_tokeninfo((&provider, &session))
240		.await?;
241
242	self.write_str(&format!("{tokeninfo:#?}\n")).await
243}
244
245#[admin_command]
246pub(super) async fn oauth_revoke(&self, id: SessionOrUserId) -> Result {
247	match id {
248		| Left(sess_id) => {
249			let session = self.services.oauth.sessions.get(&sess_id).await?;
250
251			let provider = self
252				.services
253				.oauth
254				.sessions
255				.provider(&session)
256				.await?;
257
258			self.services
259				.oauth
260				.revoke_token((&provider, &session))
261				.await
262				.ok();
263		},
264		| Right(user_id) =>
265			self.services
266				.oauth
267				.revoke_user_tokens(&user_id)
268				.await,
269	}
270
271	self.write_str("revoked").await
272}
273
274#[admin_command]
275pub(super) async fn oauth_delete(&self, id: SessionOrUserId, force: bool) -> Result {
276	if !force {
277		return Err!(
278			"Deleting these records can cause registration conflicts. Use --force to be sure."
279		);
280	}
281
282	match &id {
283		| Left(sess_id) => {
284			self.services.oauth.sessions.delete(sess_id).await;
285		},
286		| Right(user_id) => {
287			self.services
288				.oauth
289				.delete_user_sessions(user_id)
290				.await;
291		},
292	}
293
294	self.write_string(format!("deleted any oauth state for {id}"))
295		.await
296}