mas_handlers/graphql/model/
oauth.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-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 anyhow::Context as _;
8use async_graphql::{Context, Description, Enum, ID, Object};
9use chrono::{DateTime, Utc};
10use mas_storage::{oauth2::OAuth2ClientRepository, user::BrowserSessionRepository};
11use oauth2_types::{oidc::ApplicationType, scope::Scope};
12use ulid::Ulid;
13use url::Url;
14
15use super::{BrowserSession, NodeType, SessionState, User, UserAgent};
16use crate::graphql::{UserId, state::ContextExt};
17
18/// An OAuth 2.0 session represents a client session which used the OAuth APIs
19/// to login.
20#[derive(Description)]
21pub struct OAuth2Session(pub mas_data_model::Session);
22
23#[Object(use_type_description)]
24impl OAuth2Session {
25    /// ID of the object.
26    pub async fn id(&self) -> ID {
27        NodeType::OAuth2Session.id(self.0.id)
28    }
29
30    /// OAuth 2.0 client used by this session.
31    pub async fn client(&self, ctx: &Context<'_>) -> Result<OAuth2Client, async_graphql::Error> {
32        let state = ctx.state();
33        let mut repo = state.repository().await?;
34        let client = repo
35            .oauth2_client()
36            .lookup(self.0.client_id)
37            .await?
38            .context("Could not load client")?;
39        repo.cancel().await?;
40
41        Ok(OAuth2Client(client))
42    }
43
44    /// Scope granted for this session.
45    pub async fn scope(&self) -> String {
46        self.0.scope.to_string()
47    }
48
49    /// When the object was created.
50    pub async fn created_at(&self) -> DateTime<Utc> {
51        self.0.created_at
52    }
53
54    /// When the session ended.
55    pub async fn finished_at(&self) -> Option<DateTime<Utc>> {
56        match &self.0.state {
57            mas_data_model::SessionState::Valid => None,
58            mas_data_model::SessionState::Finished { finished_at } => Some(*finished_at),
59        }
60    }
61
62    /// The user-agent with which the session was created.
63    pub async fn user_agent(&self) -> Option<UserAgent> {
64        self.0.user_agent.clone().map(UserAgent::from)
65    }
66
67    /// The state of the session.
68    pub async fn state(&self) -> SessionState {
69        match &self.0.state {
70            mas_data_model::SessionState::Valid => SessionState::Active,
71            mas_data_model::SessionState::Finished { .. } => SessionState::Finished,
72        }
73    }
74
75    /// The browser session which started this OAuth 2.0 session.
76    pub async fn browser_session(
77        &self,
78        ctx: &Context<'_>,
79    ) -> Result<Option<BrowserSession>, async_graphql::Error> {
80        let Some(user_session_id) = self.0.user_session_id else {
81            return Ok(None);
82        };
83
84        let state = ctx.state();
85        let mut repo = state.repository().await?;
86        let browser_session = repo
87            .browser_session()
88            .lookup(user_session_id)
89            .await?
90            .context("Could not load browser session")?;
91        repo.cancel().await?;
92
93        Ok(Some(BrowserSession(browser_session)))
94    }
95
96    /// User authorized for this session.
97    pub async fn user(&self, ctx: &Context<'_>) -> Result<Option<User>, async_graphql::Error> {
98        let state = ctx.state();
99        let Some(user_id) = self.0.user_id else {
100            return Ok(None);
101        };
102
103        if !ctx.requester().is_owner_or_admin(&UserId(user_id)) {
104            return Err(async_graphql::Error::new("Unauthorized"));
105        }
106
107        let mut repo = state.repository().await?;
108        let user = repo
109            .user()
110            .lookup(user_id)
111            .await?
112            .context("Could not load user")?;
113        repo.cancel().await?;
114
115        Ok(Some(User(user)))
116    }
117
118    /// The last IP address used by the session.
119    pub async fn last_active_ip(&self) -> Option<String> {
120        self.0.last_active_ip.map(|ip| ip.to_string())
121    }
122
123    /// The last time the session was active.
124    pub async fn last_active_at(&self) -> Option<DateTime<Utc>> {
125        self.0.last_active_at
126    }
127}
128
129/// The application type advertised by the client.
130#[derive(Enum, Copy, Clone, Eq, PartialEq)]
131pub enum OAuth2ApplicationType {
132    /// Client is a web application.
133    Web,
134
135    /// Client is a native application.
136    Native,
137}
138
139/// An OAuth 2.0 client
140#[derive(Description)]
141pub struct OAuth2Client(pub mas_data_model::Client);
142
143#[Object(use_type_description)]
144impl OAuth2Client {
145    /// ID of the object.
146    pub async fn id(&self) -> ID {
147        NodeType::OAuth2Client.id(self.0.id)
148    }
149
150    /// OAuth 2.0 client ID
151    pub async fn client_id(&self) -> &str {
152        &self.0.client_id
153    }
154
155    /// Client name advertised by the client.
156    pub async fn client_name(&self) -> Option<&str> {
157        self.0.client_name.as_deref()
158    }
159
160    /// Client URI advertised by the client.
161    pub async fn client_uri(&self) -> Option<&Url> {
162        self.0.client_uri.as_ref()
163    }
164
165    /// Logo URI advertised by the client.
166    pub async fn logo_uri(&self) -> Option<&Url> {
167        self.0.logo_uri.as_ref()
168    }
169
170    /// Terms of services URI advertised by the client.
171    pub async fn tos_uri(&self) -> Option<&Url> {
172        self.0.tos_uri.as_ref()
173    }
174
175    /// Privacy policy URI advertised by the client.
176    pub async fn policy_uri(&self) -> Option<&Url> {
177        self.0.policy_uri.as_ref()
178    }
179
180    /// List of redirect URIs used for authorization grants by the client.
181    pub async fn redirect_uris(&self) -> &[Url] {
182        &self.0.redirect_uris
183    }
184
185    /// The application type advertised by the client.
186    pub async fn application_type(&self) -> Option<OAuth2ApplicationType> {
187        match self.0.application_type.as_ref()? {
188            ApplicationType::Web => Some(OAuth2ApplicationType::Web),
189            ApplicationType::Native => Some(OAuth2ApplicationType::Native),
190            ApplicationType::Unknown(_) => None,
191        }
192    }
193}
194
195/// An OAuth 2.0 consent represents the scope a user consented to grant to a
196/// client.
197#[derive(Description)]
198pub struct OAuth2Consent {
199    scope: Scope,
200    client_id: Ulid,
201}
202
203#[Object(use_type_description)]
204impl OAuth2Consent {
205    /// Scope consented by the user for this client.
206    pub async fn scope(&self) -> String {
207        self.scope.to_string()
208    }
209
210    /// OAuth 2.0 client for which the user granted access.
211    pub async fn client(&self, ctx: &Context<'_>) -> Result<OAuth2Client, async_graphql::Error> {
212        let state = ctx.state();
213        let mut repo = state.repository().await?;
214        let client = repo
215            .oauth2_client()
216            .lookup(self.client_id)
217            .await?
218            .context("Could not load client")?;
219        repo.cancel().await?;
220
221        Ok(OAuth2Client(client))
222    }
223}