mas_handlers/graphql/mutations/
oauth2_session.rs1use anyhow::Context as _;
8use async_graphql::{Context, Description, Enum, ID, InputObject, Object};
9use chrono::Duration;
10use mas_data_model::{Device, TokenType};
11use mas_storage::{
12 RepositoryAccess,
13 oauth2::{
14 OAuth2AccessTokenRepository, OAuth2ClientRepository, OAuth2RefreshTokenRepository,
15 OAuth2SessionRepository,
16 },
17 queue::{QueueJobRepositoryExt as _, SyncDevicesJob},
18 user::UserRepository,
19};
20use oauth2_types::scope::Scope;
21
22use crate::graphql::{
23 model::{NodeType, OAuth2Session},
24 state::ContextExt,
25};
26
27#[derive(Default)]
28pub struct OAuth2SessionMutations {
29 _private: (),
30}
31
32#[derive(InputObject)]
34pub struct CreateOAuth2SessionInput {
35 scope: String,
37
38 user_id: ID,
40
41 permanent: Option<bool>,
43}
44
45#[derive(Description)]
47pub struct CreateOAuth2SessionPayload {
48 access_token: String,
49 refresh_token: Option<String>,
50 session: mas_data_model::Session,
51}
52
53#[Object(use_type_description)]
54impl CreateOAuth2SessionPayload {
55 pub async fn access_token(&self) -> &str {
57 &self.access_token
58 }
59
60 pub async fn refresh_token(&self) -> Option<&str> {
62 self.refresh_token.as_deref()
63 }
64
65 pub async fn oauth2_session(&self) -> OAuth2Session {
67 OAuth2Session(self.session.clone())
68 }
69}
70
71#[derive(InputObject)]
73pub struct EndOAuth2SessionInput {
74 oauth2_session_id: ID,
76}
77
78pub enum EndOAuth2SessionPayload {
80 NotFound,
81 Ended(Box<mas_data_model::Session>),
82}
83
84#[derive(Enum, Copy, Clone, PartialEq, Eq, Debug)]
86enum EndOAuth2SessionStatus {
87 Ended,
89
90 NotFound,
92}
93
94#[Object]
95impl EndOAuth2SessionPayload {
96 async fn status(&self) -> EndOAuth2SessionStatus {
98 match self {
99 Self::Ended(_) => EndOAuth2SessionStatus::Ended,
100 Self::NotFound => EndOAuth2SessionStatus::NotFound,
101 }
102 }
103
104 async fn oauth2_session(&self) -> Option<OAuth2Session> {
106 match self {
107 Self::Ended(session) => Some(OAuth2Session(*session.clone())),
108 Self::NotFound => None,
109 }
110 }
111}
112
113#[derive(InputObject)]
115pub struct SetOAuth2SessionNameInput {
116 oauth2_session_id: ID,
118
119 human_name: String,
121}
122
123pub enum SetOAuth2SessionNamePayload {
125 NotFound,
127
128 Updated(Box<mas_data_model::Session>),
130}
131
132#[derive(Enum, Copy, Clone, PartialEq, Eq, Debug)]
134enum SetOAuth2SessionNameStatus {
135 Updated,
137
138 NotFound,
140}
141
142#[Object]
143impl SetOAuth2SessionNamePayload {
144 async fn status(&self) -> SetOAuth2SessionNameStatus {
146 match self {
147 Self::Updated(_) => SetOAuth2SessionNameStatus::Updated,
148 Self::NotFound => SetOAuth2SessionNameStatus::NotFound,
149 }
150 }
151
152 async fn oauth2_session(&self) -> Option<OAuth2Session> {
154 match self {
155 Self::Updated(session) => Some(OAuth2Session(*session.clone())),
156 Self::NotFound => None,
157 }
158 }
159}
160
161#[Object]
162impl OAuth2SessionMutations {
163 async fn create_oauth2_session(
167 &self,
168 ctx: &Context<'_>,
169 input: CreateOAuth2SessionInput,
170 ) -> Result<CreateOAuth2SessionPayload, async_graphql::Error> {
171 let state = ctx.state();
172 let homeserver = state.homeserver_connection();
173 let user_id = NodeType::User.extract_ulid(&input.user_id)?;
174 let scope: Scope = input.scope.parse().context("Invalid scope")?;
175 let permanent = input.permanent.unwrap_or(false);
176 let requester = ctx.requester();
177
178 if !requester.is_admin() {
179 return Err(async_graphql::Error::new("Unauthorized"));
180 }
181
182 let session = requester
183 .oauth2_session()
184 .context("Requester should be a OAuth 2.0 client")?;
185
186 let mut repo = state.repository().await?;
187 let clock = state.clock();
188 let mut rng = state.rng();
189
190 let client = repo
191 .oauth2_client()
192 .lookup(session.client_id)
193 .await?
194 .context("Client not found")?;
195
196 let user = repo
197 .user()
198 .lookup(user_id)
199 .await?
200 .context("User not found")?;
201
202 let access_token = TokenType::AccessToken.generate(&mut rng);
204
205 let session = repo
207 .oauth2_session()
208 .add(&mut rng, &clock, &client, Some(&user), None, scope)
209 .await?;
210
211 repo.user().acquire_lock_for_sync(&user).await?;
213
214 for scope in &*session.scope {
216 if let Some(device) = Device::from_scope_token(scope) {
217 homeserver
218 .upsert_device(&user.username, device.as_str(), None)
219 .await
220 .context("Failed to provision device")?;
221 }
222 }
223
224 let ttl = if permanent {
225 None
226 } else {
227 Some(Duration::microseconds(5 * 60 * 1000 * 1000))
228 };
229 let access_token = repo
230 .oauth2_access_token()
231 .add(&mut rng, &clock, &session, access_token, ttl)
232 .await?;
233
234 let refresh_token = if permanent {
235 None
236 } else {
237 let refresh_token = TokenType::RefreshToken.generate(&mut rng);
238
239 let refresh_token = repo
240 .oauth2_refresh_token()
241 .add(&mut rng, &clock, &session, &access_token, refresh_token)
242 .await?;
243
244 Some(refresh_token)
245 };
246
247 repo.save().await?;
248
249 Ok(CreateOAuth2SessionPayload {
250 session,
251 access_token: access_token.access_token,
252 refresh_token: refresh_token.map(|t| t.refresh_token),
253 })
254 }
255
256 async fn end_oauth2_session(
257 &self,
258 ctx: &Context<'_>,
259 input: EndOAuth2SessionInput,
260 ) -> Result<EndOAuth2SessionPayload, async_graphql::Error> {
261 let state = ctx.state();
262 let oauth2_session_id = NodeType::OAuth2Session.extract_ulid(&input.oauth2_session_id)?;
263 let requester = ctx.requester();
264
265 let mut repo = state.repository().await?;
266 let clock = state.clock();
267 let mut rng = state.rng();
268
269 let session = repo.oauth2_session().lookup(oauth2_session_id).await?;
270 let Some(session) = session else {
271 return Ok(EndOAuth2SessionPayload::NotFound);
272 };
273
274 if !requester.is_owner_or_admin(&session) {
275 return Ok(EndOAuth2SessionPayload::NotFound);
276 }
277
278 if let Some(user_id) = session.user_id {
279 let user = repo
280 .user()
281 .lookup(user_id)
282 .await?
283 .context("Could not load user")?;
284
285 repo.queue_job()
287 .schedule_job(&mut rng, &clock, SyncDevicesJob::new(&user))
288 .await?;
289 }
290
291 let session = repo.oauth2_session().finish(&clock, session).await?;
292
293 repo.save().await?;
294
295 Ok(EndOAuth2SessionPayload::Ended(Box::new(session)))
296 }
297
298 async fn set_oauth2_session_name(
299 &self,
300 ctx: &Context<'_>,
301 input: SetOAuth2SessionNameInput,
302 ) -> Result<SetOAuth2SessionNamePayload, async_graphql::Error> {
303 let state = ctx.state();
304 let oauth2_session_id = NodeType::OAuth2Session.extract_ulid(&input.oauth2_session_id)?;
305 let requester = ctx.requester();
306
307 let mut repo = state.repository().await?;
308 let homeserver = state.homeserver_connection();
309
310 let session = repo.oauth2_session().lookup(oauth2_session_id).await?;
311 let Some(session) = session else {
312 return Ok(SetOAuth2SessionNamePayload::NotFound);
313 };
314
315 if !requester.is_owner_or_admin(&session) {
316 return Ok(SetOAuth2SessionNamePayload::NotFound);
317 }
318
319 let user_id = session.user_id.context("Session has no user")?;
320
321 let user = repo
322 .user()
323 .lookup(user_id)
324 .await?
325 .context("User not found")?;
326
327 let session = repo
328 .oauth2_session()
329 .set_human_name(session, Some(input.human_name.clone()))
330 .await?;
331
332 for scope in &*session.scope {
334 if let Some(device) = Device::from_scope_token(scope) {
335 homeserver
336 .update_device_display_name(&user.username, device.as_str(), &input.human_name)
337 .await
338 .context("Failed to provision device")?;
339 }
340 }
341
342 repo.save().await?;
343
344 Ok(SetOAuth2SessionNamePayload::Updated(Box::new(session)))
345 }
346}