mas_handlers/graphql/query/
mod.rs

1// Copyright 2024, 2025 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, MergedObject, Object};
8
9use crate::graphql::{
10    model::{
11        Anonymous, BrowserSession, CompatSession, Node, NodeType, OAuth2Client, OAuth2Session,
12        SiteConfig, User, UserEmail, UserRecoveryTicket,
13    },
14    state::ContextExt,
15};
16
17mod session;
18mod upstream_oauth;
19mod user;
20mod viewer;
21
22use self::{
23    session::SessionQuery, upstream_oauth::UpstreamOAuthQuery, user::UserQuery, viewer::ViewerQuery,
24};
25use super::model::UserEmailAuthentication;
26
27/// The query root of the GraphQL interface.
28#[derive(Default, MergedObject)]
29pub struct Query(
30    BaseQuery,
31    UserQuery,
32    UpstreamOAuthQuery,
33    SessionQuery,
34    ViewerQuery,
35);
36
37impl Query {
38    #[must_use]
39    pub fn new() -> Self {
40        Self::default()
41    }
42}
43
44#[derive(Default)]
45struct BaseQuery;
46
47// TODO: move the rest of the queries in separate modules
48#[Object]
49impl BaseQuery {
50    /// Get the current logged in browser session
51    #[graphql(deprecation = "Use `viewerSession` instead.")]
52    async fn current_browser_session(
53        &self,
54        ctx: &Context<'_>,
55    ) -> Result<Option<BrowserSession>, async_graphql::Error> {
56        let requester = ctx.requester();
57        Ok(requester
58            .browser_session()
59            .cloned()
60            .map(BrowserSession::from))
61    }
62
63    /// Get the current logged in user
64    #[graphql(deprecation = "Use `viewer` instead.")]
65    async fn current_user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
66        let requester = ctx.requester();
67        Ok(requester.user().cloned().map(User::from))
68    }
69
70    /// Fetch an OAuth 2.0 client by its ID.
71    async fn oauth2_client(
72        &self,
73        ctx: &Context<'_>,
74        id: ID,
75    ) -> Result<Option<OAuth2Client>, async_graphql::Error> {
76        let state = ctx.state();
77        let id = NodeType::OAuth2Client.extract_ulid(&id)?;
78
79        let mut repo = state.repository().await?;
80        let client = repo.oauth2_client().lookup(id).await?;
81        repo.cancel().await?;
82
83        Ok(client.map(OAuth2Client))
84    }
85
86    /// Fetch a browser session by its ID.
87    async fn browser_session(
88        &self,
89        ctx: &Context<'_>,
90        id: ID,
91    ) -> Result<Option<BrowserSession>, async_graphql::Error> {
92        let state = ctx.state();
93        let id = NodeType::BrowserSession.extract_ulid(&id)?;
94        let requester = ctx.requester();
95
96        let mut repo = state.repository().await?;
97        let browser_session = repo.browser_session().lookup(id).await?;
98        repo.cancel().await?;
99
100        let Some(browser_session) = browser_session else {
101            return Ok(None);
102        };
103
104        if !requester.is_owner_or_admin(&browser_session) {
105            return Ok(None);
106        }
107
108        Ok(Some(BrowserSession(browser_session)))
109    }
110
111    /// Fetch a compatible session by its ID.
112    async fn compat_session(
113        &self,
114        ctx: &Context<'_>,
115        id: ID,
116    ) -> Result<Option<CompatSession>, async_graphql::Error> {
117        let state = ctx.state();
118        let id = NodeType::CompatSession.extract_ulid(&id)?;
119        let requester = ctx.requester();
120
121        let mut repo = state.repository().await?;
122        let compat_session = repo.compat_session().lookup(id).await?;
123        repo.cancel().await?;
124
125        let Some(compat_session) = compat_session else {
126            return Ok(None);
127        };
128
129        if !requester.is_owner_or_admin(&compat_session) {
130            return Ok(None);
131        }
132
133        Ok(Some(CompatSession::new(compat_session)))
134    }
135
136    /// Fetch an OAuth 2.0 session by its ID.
137    async fn oauth2_session(
138        &self,
139        ctx: &Context<'_>,
140        id: ID,
141    ) -> Result<Option<OAuth2Session>, async_graphql::Error> {
142        let state = ctx.state();
143        let id = NodeType::OAuth2Session.extract_ulid(&id)?;
144        let requester = ctx.requester();
145
146        let mut repo = state.repository().await?;
147        let oauth2_session = repo.oauth2_session().lookup(id).await?;
148        repo.cancel().await?;
149
150        let Some(oauth2_session) = oauth2_session else {
151            return Ok(None);
152        };
153
154        if !requester.is_owner_or_admin(&oauth2_session) {
155            return Ok(None);
156        }
157
158        Ok(Some(OAuth2Session(oauth2_session)))
159    }
160
161    /// Fetch a user email by its ID.
162    async fn user_email(
163        &self,
164        ctx: &Context<'_>,
165        id: ID,
166    ) -> Result<Option<UserEmail>, async_graphql::Error> {
167        let state = ctx.state();
168        let id = NodeType::UserEmail.extract_ulid(&id)?;
169        let requester = ctx.requester();
170
171        let mut repo = state.repository().await?;
172        let user_email = repo.user_email().lookup(id).await?;
173        repo.cancel().await?;
174
175        let Some(user_email) = user_email else {
176            return Ok(None);
177        };
178
179        if !requester.is_owner_or_admin(&user_email) {
180            return Ok(None);
181        }
182
183        Ok(Some(UserEmail(user_email)))
184    }
185
186    /// Fetch a user recovery ticket.
187    async fn user_recovery_ticket(
188        &self,
189        ctx: &Context<'_>,
190        ticket: String,
191    ) -> Result<Option<UserRecoveryTicket>, async_graphql::Error> {
192        let state = ctx.state();
193        let mut repo = state.repository().await?;
194        let ticket = repo.user_recovery().find_ticket(&ticket).await?;
195        repo.cancel().await?;
196
197        Ok(ticket.map(UserRecoveryTicket))
198    }
199
200    /// Fetch a user email authentication session
201    async fn user_email_authentication(
202        &self,
203        ctx: &Context<'_>,
204        id: ID,
205    ) -> Result<Option<UserEmailAuthentication>, async_graphql::Error> {
206        let state = ctx.state();
207        let id = NodeType::UserEmailAuthentication.extract_ulid(&id)?;
208        let requester = ctx.requester();
209        let mut repo = state.repository().await?;
210        let authentication = repo.user_email().lookup_authentication(id).await?;
211        let Some(authentication) = authentication else {
212            return Ok(None);
213        };
214
215        let Some(browser_session) = requester.browser_session() else {
216            return Ok(None);
217        };
218
219        if authentication.user_session_id != Some(browser_session.id) {
220            return Ok(None);
221        }
222
223        Ok(Some(UserEmailAuthentication(authentication)))
224    }
225
226    /// Fetches an object given its ID.
227    async fn node(&self, ctx: &Context<'_>, id: ID) -> Result<Option<Node>, async_graphql::Error> {
228        // Special case for the anonymous user
229        if id.as_str() == "anonymous" {
230            return Ok(Some(Node::Anonymous(Box::new(Anonymous))));
231        }
232
233        if id.as_str() == crate::graphql::model::SITE_CONFIG_ID {
234            return Ok(Some(Node::SiteConfig(Box::new(SiteConfig::new(
235                ctx.state().site_config(),
236            )))));
237        }
238
239        let (node_type, _id) = NodeType::from_id(&id)?;
240
241        let ret = match node_type {
242            // TODO
243            NodeType::Authentication | NodeType::CompatSsoLogin | NodeType::UserRecoveryTicket => {
244                None
245            }
246
247            NodeType::UpstreamOAuth2Provider => UpstreamOAuthQuery
248                .upstream_oauth2_provider(ctx, id)
249                .await?
250                .map(|c| Node::UpstreamOAuth2Provider(Box::new(c))),
251
252            NodeType::UpstreamOAuth2Link => UpstreamOAuthQuery
253                .upstream_oauth2_link(ctx, id)
254                .await?
255                .map(|c| Node::UpstreamOAuth2Link(Box::new(c))),
256
257            NodeType::OAuth2Client => self
258                .oauth2_client(ctx, id)
259                .await?
260                .map(|c| Node::OAuth2Client(Box::new(c))),
261
262            NodeType::UserEmail => self
263                .user_email(ctx, id)
264                .await?
265                .map(|e| Node::UserEmail(Box::new(e))),
266
267            NodeType::UserEmailAuthentication => self
268                .user_email_authentication(ctx, id)
269                .await?
270                .map(|e| Node::UserEmailAuthentication(Box::new(e))),
271
272            NodeType::CompatSession => self
273                .compat_session(ctx, id)
274                .await?
275                .map(|s| Node::CompatSession(Box::new(s))),
276
277            NodeType::OAuth2Session => self
278                .oauth2_session(ctx, id)
279                .await?
280                .map(|s| Node::OAuth2Session(Box::new(s))),
281
282            NodeType::BrowserSession => self
283                .browser_session(ctx, id)
284                .await?
285                .map(|s| Node::BrowserSession(Box::new(s))),
286
287            NodeType::User => UserQuery
288                .user(ctx, id)
289                .await?
290                .map(|u| Node::User(Box::new(u))),
291        };
292
293        Ok(ret)
294    }
295
296    /// Get the current site configuration
297    async fn site_config(&self, ctx: &Context<'_>) -> SiteConfig {
298        SiteConfig::new(ctx.state().site_config())
299    }
300}