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_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 #[error("Missing authorization header")]
29 MissingAuthorizationHeader,
30
31 #[error("Invalid authorization header")]
33 InvalidAuthorizationHeader,
34
35 #[error("Couldn't load the database repository")]
37 RepositorySetup(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
38
39 #[error("Invalid repository operation")]
41 Repository(#[from] RepositoryError),
42
43 #[error("Unknown access token")]
45 UnknownAccessToken,
46
47 #[error("Access token expired")]
49 TokenExpired,
50
51 #[error("Access token revoked")]
53 SessionRevoked,
54
55 #[error("User locked")]
57 UserLocked,
58
59 #[error("Failed to load session {0}")]
61 LoadSession(Ulid),
62
63 #[error("Failed to load user {0}")]
65 LoadUser(Ulid),
66
67 #[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#[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 let mut repo = BoxRepository::from_request_parts(parts, state)
130 .await
131 .map_err(Into::into)
132 .map_err(Rejection::RepositorySetup)?;
133
134 let token = TypedHeader::<Authorization<Bearer>>::from_request_parts(parts, state)
136 .await
137 .map_err(|e| {
138 if e.is_missing() {
141 Rejection::MissingAuthorizationHeader
142 } else {
143 Rejection::InvalidAuthorizationHeader
144 }
145 })?;
146
147 let token = token.token();
148
149 let token = repo
151 .oauth2_access_token()
152 .find_by_token(token)
153 .await?
154 .ok_or(Rejection::UnknownAccessToken)?;
155
156 let session = repo
158 .oauth2_session()
159 .lookup(token.session_id)
160 .await?
161 .ok_or_else(|| Rejection::LoadSession(token.session_id))?;
162
163 activity_tracker
165 .record_oauth2_session(&clock, &session)
166 .await;
167
168 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 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 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}