mas_handlers/graphql/query/
session.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use async_graphql::{Context, ID, Object, Union};
8use mas_data_model::Device;
9use mas_storage::{
10    Pagination, RepositoryAccess,
11    compat::{CompatSessionFilter, CompatSessionRepository},
12    oauth2::OAuth2SessionFilter,
13};
14use oauth2_types::scope::Scope;
15
16use crate::graphql::{
17    UserId,
18    model::{CompatSession, NodeType, OAuth2Session},
19    state::ContextExt,
20};
21
22#[derive(Default)]
23pub struct SessionQuery;
24
25/// A client session, either compat or OAuth 2.0
26#[derive(Union)]
27enum Session {
28    CompatSession(Box<CompatSession>),
29    OAuth2Session(Box<OAuth2Session>),
30}
31
32#[Object]
33impl SessionQuery {
34    /// Lookup a compat or OAuth 2.0 session
35    async fn session(
36        &self,
37        ctx: &Context<'_>,
38        user_id: ID,
39        device_id: String,
40    ) -> Result<Option<Session>, async_graphql::Error> {
41        let user_id = NodeType::User.extract_ulid(&user_id)?;
42        let requester = ctx.requester();
43        if !requester.is_owner_or_admin(&UserId(user_id)) {
44            return Ok(None);
45        }
46
47        let device = Device::from(device_id);
48        let state = ctx.state();
49        let mut repo = state.repository().await?;
50
51        // Lookup the user
52        let Some(user) = repo.user().lookup(user_id).await? else {
53            return Ok(None);
54        };
55
56        // First, try to find a compat session
57        let filter = CompatSessionFilter::new()
58            .for_user(&user)
59            .active_only()
60            .for_device(&device);
61        // We only want most recent session
62        let pagination = Pagination::last(1);
63        let compat_sessions = repo.compat_session().list(filter, pagination).await?;
64
65        if compat_sessions.has_previous_page {
66            // XXX: should we bail out?
67            tracing::warn!(
68                "Found more than one active session with device {device} for user {user_id}"
69            );
70        }
71
72        if let Some((compat_session, sso_login)) = compat_sessions.edges.into_iter().next() {
73            repo.cancel().await?;
74
75            return Ok(Some(Session::CompatSession(Box::new(
76                CompatSession::new(compat_session).with_loaded_sso_login(sso_login),
77            ))));
78        }
79
80        // Then, try to find an OAuth 2.0 session. Because we don't have any dedicated
81        // device column, we're looking up using the device scope.
82        // All device IDs can't necessarily be encoded as a scope. If it's not the case,
83        // we'll skip looking for OAuth 2.0 sessions.
84        let Ok(scope_token) = device.to_scope_token() else {
85            repo.cancel().await?;
86
87            return Ok(None);
88        };
89        let scope = Scope::from_iter([scope_token]);
90        let filter = OAuth2SessionFilter::new()
91            .for_user(&user)
92            .active_only()
93            .with_scope(&scope);
94        let sessions = repo.oauth2_session().list(filter, pagination).await?;
95
96        // It's possible to have multiple active OAuth 2.0 sessions. For now, we just
97        // log it if it is the case
98        if sessions.has_previous_page {
99            // XXX: should we bail out?
100            tracing::warn!(
101                "Found more than one active session with device {device} for user {user_id}"
102            );
103        }
104
105        if let Some(session) = sessions.edges.into_iter().next() {
106            repo.cancel().await?;
107            return Ok(Some(Session::OAuth2Session(Box::new(OAuth2Session(
108                session,
109            )))));
110        }
111        repo.cancel().await?;
112
113        Ok(None)
114    }
115}