mas_handlers/admin/
call_context.rs1use 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_axum_utils::record_error;
19use mas_data_model::{Session, User};
20use mas_storage::{BoxClock, BoxRepository, RepositoryError};
21use ulid::Ulid;
22
23use super::response::ErrorResponse;
24use crate::BoundActivityTracker;
25
26#[derive(Debug, thiserror::Error)]
27pub enum Rejection {
28 #[error("Missing authorization header")]
30 MissingAuthorizationHeader,
31
32 #[error("Invalid authorization header")]
34 InvalidAuthorizationHeader,
35
36 #[error("Couldn't load the database repository")]
38 RepositorySetup(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
39
40 #[error("Invalid repository operation")]
42 Repository(#[from] RepositoryError),
43
44 #[error("Unknown access token")]
46 UnknownAccessToken,
47
48 #[error("Access token expired")]
50 TokenExpired,
51
52 #[error("Access token revoked")]
54 SessionRevoked,
55
56 #[error("User locked")]
58 UserLocked,
59
60 #[error("Failed to load session {0}")]
62 LoadSession(Ulid),
63
64 #[error("Failed to load user {0}")]
66 LoadUser(Ulid),
67
68 #[error("Missing urn:mas:admin scope")]
70 MissingScope,
71}
72
73impl IntoResponse for Rejection {
74 fn into_response(self) -> Response {
75 let response = ErrorResponse::from_error(&self);
76 let sentry_event_id = record_error!(
77 self,
78 Self::RepositorySetup(_)
79 | Self::Repository(_)
80 | Self::LoadSession(_)
81 | Self::LoadUser(_)
82 );
83
84 let status = match &self {
85 Rejection::InvalidAuthorizationHeader | Rejection::MissingAuthorizationHeader => {
86 StatusCode::BAD_REQUEST
87 }
88
89 Rejection::UnknownAccessToken
90 | Rejection::TokenExpired
91 | Rejection::SessionRevoked
92 | Rejection::UserLocked
93 | Rejection::MissingScope => StatusCode::UNAUTHORIZED,
94
95 Rejection::RepositorySetup(_)
96 | Rejection::Repository(_)
97 | Rejection::LoadSession(_)
98 | Rejection::LoadUser(_) => StatusCode::INTERNAL_SERVER_ERROR,
99 };
100
101 (status, sentry_event_id, Json(response)).into_response()
102 }
103}
104
105#[non_exhaustive]
110#[derive(OperationIo)]
111#[aide(input)]
112pub struct CallContext {
113 pub repo: BoxRepository,
114 pub clock: BoxClock,
115 pub user: Option<User>,
116 pub session: Session,
117}
118
119impl<S> FromRequestParts<S> for CallContext
120where
121 S: Send + Sync,
122 BoundActivityTracker: FromRequestParts<S, Rejection = Infallible>,
123 BoxRepository: FromRequestParts<S>,
124 BoxClock: FromRequestParts<S, Rejection = Infallible>,
125 <BoxRepository as FromRequestParts<S>>::Rejection:
126 Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
127{
128 type Rejection = Rejection;
129
130 async fn from_request_parts(
131 parts: &mut axum::http::request::Parts,
132 state: &S,
133 ) -> Result<Self, Self::Rejection> {
134 let Ok(activity_tracker) = BoundActivityTracker::from_request_parts(parts, state).await;
135 let Ok(clock) = BoxClock::from_request_parts(parts, state).await;
136
137 let mut repo = BoxRepository::from_request_parts(parts, state)
139 .await
140 .map_err(Into::into)
141 .map_err(Rejection::RepositorySetup)?;
142
143 let token = TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, state)
145 .await
146 .map_err(|e| {
147 if e.is_missing() {
150 Rejection::MissingAuthorizationHeader
151 } else {
152 Rejection::InvalidAuthorizationHeader
153 }
154 })?;
155
156 let token = token.token();
157
158 let token = repo
160 .oauth2_access_token()
161 .find_by_token(token)
162 .await?
163 .ok_or(Rejection::UnknownAccessToken)?;
164
165 let session = repo
167 .oauth2_session()
168 .lookup(token.session_id)
169 .await?
170 .ok_or_else(|| Rejection::LoadSession(token.session_id))?;
171
172 activity_tracker
174 .record_oauth2_session(&clock, &session)
175 .await;
176
177 let user = if let Some(user_id) = session.user_id {
179 let user = repo
180 .user()
181 .lookup(user_id)
182 .await?
183 .ok_or_else(|| Rejection::LoadUser(user_id))?;
184 Some(user)
185 } else {
186 None
187 };
188
189 if let Some(user) = &user {
191 if !user.is_valid() {
192 return Err(Rejection::UserLocked);
193 }
194 }
195
196 if !session.is_valid() {
197 return Err(Rejection::SessionRevoked);
198 }
199
200 if !token.is_valid(clock.now()) {
201 return Err(Rejection::TokenExpired);
202 }
203
204 if !session.scope.contains("urn:mas:admin") {
207 return Err(Rejection::MissingScope);
208 }
209
210 Ok(Self {
211 repo,
212 clock,
213 user,
214 session,
215 })
216 }
217}