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