mas_handlers/admin/
call_context.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 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 std::convert::Infallible;
8
9use aide::OperationIo;
10use axum::{
11    Json,
12    extract::FromRequestParts,
13    response::{IntoResponse, Response},
14};
15use axum_extra::TypedHeader;
16use headers::{Authorization, authorization::Bearer};
17use hyper::StatusCode;
18use mas_data_model::{Session, User};
19use mas_storage::{BoxClock, BoxRepository, RepositoryError};
20use ulid::Ulid;
21
22use super::response::ErrorResponse;
23use crate::BoundActivityTracker;
24
25#[derive(Debug, thiserror::Error)]
26pub enum Rejection {
27    /// The authorization header is missing
28    #[error("Missing authorization header")]
29    MissingAuthorizationHeader,
30
31    /// The authorization header is invalid
32    #[error("Invalid authorization header")]
33    InvalidAuthorizationHeader,
34
35    /// Couldn't load the database repository
36    #[error("Couldn't load the database repository")]
37    RepositorySetup(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
38
39    /// A database operation failed
40    #[error("Invalid repository operation")]
41    Repository(#[from] RepositoryError),
42
43    /// The access token could not be found in the database
44    #[error("Unknown access token")]
45    UnknownAccessToken,
46
47    /// The access token provided expired
48    #[error("Access token expired")]
49    TokenExpired,
50
51    /// The session associated with the access token was revoked
52    #[error("Access token revoked")]
53    SessionRevoked,
54
55    /// The user associated with the session is locked
56    #[error("User locked")]
57    UserLocked,
58
59    /// Failed to load the session
60    #[error("Failed to load session {0}")]
61    LoadSession(Ulid),
62
63    /// Failed to load the user
64    #[error("Failed to load user {0}")]
65    LoadUser(Ulid),
66
67    /// The session does not have the `urn:mas:admin` scope
68    #[error("Missing urn:mas:admin scope")]
69    MissingScope,
70}
71
72impl Rejection {
73    fn status_code(&self) -> StatusCode {
74        match self {
75            Self::InvalidAuthorizationHeader | Self::MissingAuthorizationHeader => {
76                StatusCode::BAD_REQUEST
77            }
78            Self::UnknownAccessToken
79            | Self::TokenExpired
80            | Self::SessionRevoked
81            | Self::UserLocked
82            | Self::MissingScope => StatusCode::UNAUTHORIZED,
83            _ => StatusCode::INTERNAL_SERVER_ERROR,
84        }
85    }
86}
87
88impl IntoResponse for Rejection {
89    fn into_response(self) -> Response {
90        let response = ErrorResponse::from_error(&self);
91        let status = self.status_code();
92        (status, Json(response)).into_response()
93    }
94}
95
96/// An extractor which authorizes the request
97///
98/// Because we need to load the database repository and the clock, we keep them
99/// in the context to avoid creating two instances for each request.
100#[non_exhaustive]
101#[derive(OperationIo)]
102#[aide(input)]
103pub struct CallContext {
104    pub repo: BoxRepository,
105    pub clock: BoxClock,
106    pub user: Option<User>,
107    pub session: Session,
108}
109
110impl<S> FromRequestParts<S> for CallContext
111where
112    S: Send + Sync,
113    BoundActivityTracker: FromRequestParts<S, Rejection = Infallible>,
114    BoxRepository: FromRequestParts<S>,
115    BoxClock: FromRequestParts<S, Rejection = Infallible>,
116    <BoxRepository as FromRequestParts<S>>::Rejection:
117        Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
118{
119    type Rejection = Rejection;
120
121    async fn from_request_parts(
122        parts: &mut axum::http::request::Parts,
123        state: &S,
124    ) -> Result<Self, Self::Rejection> {
125        let Ok(activity_tracker) = BoundActivityTracker::from_request_parts(parts, state).await;
126        let Ok(clock) = BoxClock::from_request_parts(parts, state).await;
127
128        // Load the database repository
129        let mut repo = BoxRepository::from_request_parts(parts, state)
130            .await
131            .map_err(Into::into)
132            .map_err(Rejection::RepositorySetup)?;
133
134        // Extract the access token from the authorization header
135        let token = TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, state)
136            .await
137            .map_err(|e| {
138                // We map to two differentsson of errors depending on whether the header is
139                // missing or invalid
140                if e.is_missing() {
141                    Rejection::MissingAuthorizationHeader
142                } else {
143                    Rejection::InvalidAuthorizationHeader
144                }
145            })?;
146
147        let token = token.token();
148
149        // Look for the access token in the database
150        let token = repo
151            .oauth2_access_token()
152            .find_by_token(token)
153            .await?
154            .ok_or(Rejection::UnknownAccessToken)?;
155
156        // Look for the associated session in the database
157        let session = repo
158            .oauth2_session()
159            .lookup(token.session_id)
160            .await?
161            .ok_or_else(|| Rejection::LoadSession(token.session_id))?;
162
163        // Record the activity on the session
164        activity_tracker
165            .record_oauth2_session(&clock, &session)
166            .await;
167
168        // Load the user if there is one
169        let user = if let Some(user_id) = session.user_id {
170            let user = repo
171                .user()
172                .lookup(user_id)
173                .await?
174                .ok_or_else(|| Rejection::LoadUser(user_id))?;
175            Some(user)
176        } else {
177            None
178        };
179
180        // If there is a user for this session, check that it is not locked
181        if let Some(user) = &user {
182            if !user.is_valid() {
183                return Err(Rejection::UserLocked);
184            }
185        }
186
187        if !session.is_valid() {
188            return Err(Rejection::SessionRevoked);
189        }
190
191        if !token.is_valid(clock.now()) {
192            return Err(Rejection::TokenExpired);
193        }
194
195        // For now, we only check that the session has the admin scope
196        // Later we might want to check other route-specific scopes
197        if !session.scope.contains("urn:mas:admin") {
198            return Err(Rejection::MissingScope);
199        }
200
201        Ok(Self {
202            repo,
203            clock,
204            user,
205            session,
206        })
207    }
208}