1#![allow(
9 clippy::module_name_repetitions,
10 clippy::unused_async_trait_impl,
13)]
14
15use std::{net::IpAddr, ops::Deref, sync::Arc};
16
17use async_graphql::{
18 EmptySubscription, InputObject,
19 extensions::Tracing,
20 http::MultipartOptions,
21 parser::types::{DocumentOperations, OperationType},
22};
23use axum::{
24 Extension, Json,
25 body::Body,
26 extract::{RawQuery, State as AxumState},
27 http::StatusCode,
28 response::{IntoResponse, Response},
29};
30use axum_extra::typed_header::TypedHeader;
31use chrono::{DateTime, Utc};
32use futures_util::TryStreamExt;
33use headers::{Authorization, ContentType, HeaderValue, authorization::Bearer};
34use hyper::header::CACHE_CONTROL;
35use mas_axum_utils::{
36 InternalError, RecordAsRequester, SessionInfo, SessionInfoExt, cookies::CookieJar, record_error,
37};
38use mas_data_model::{
39 BoxClock, BoxRng, BrowserSession, Clock, Session, SiteConfig, SystemClock, User,
40};
41use mas_matrix::HomeserverConnection;
42use mas_policy::{InstantiateError, Policy, PolicyFactory};
43use mas_router::UrlBuilder;
44use mas_storage::{BoxRepository, BoxRepositoryFactory, RepositoryError};
45use opentelemetry_semantic_conventions::trace::{
46 GRAPHQL_DOCUMENT, GRAPHQL_OPERATION_NAME, GRAPHQL_OPERATION_TYPE,
47};
48use rand::{SeedableRng, rng};
49use rand_chacha::ChaChaRng;
50use state::has_session_ended;
51use tracing::{Instrument, info_span};
52use ulid::Ulid;
53
54mod model;
55mod mutations;
56mod query;
57mod state;
58
59pub use self::state::{BoxState, State};
60use self::{
61 model::{CreationEvent, Node},
62 mutations::Mutation,
63 query::Query,
64};
65use crate::{
66 BoundActivityTracker, Limiter, RequesterFingerprint, impl_from_error_for_route,
67 passwords::PasswordManager,
68};
69
70#[cfg(test)]
71mod tests;
72
73#[derive(Debug, Clone)]
76pub struct ExtraRouterParameters {
77 pub undocumented_oauth2_access: bool,
78}
79
80struct GraphQLState {
81 repository_factory: BoxRepositoryFactory,
82 homeserver_connection: Arc<dyn HomeserverConnection>,
83 policy_factory: Arc<PolicyFactory>,
84 site_config: SiteConfig,
85 password_manager: PasswordManager,
86 url_builder: UrlBuilder,
87 limiter: Limiter,
88}
89
90#[async_trait::async_trait]
91impl state::State for GraphQLState {
92 async fn repository(&self) -> Result<BoxRepository, RepositoryError> {
93 self.repository_factory.create().await
94 }
95
96 async fn policy(&self) -> Result<Policy, InstantiateError> {
97 self.policy_factory.instantiate().await
98 }
99
100 fn password_manager(&self) -> PasswordManager {
101 self.password_manager.clone()
102 }
103
104 fn site_config(&self) -> &SiteConfig {
105 &self.site_config
106 }
107
108 fn homeserver_connection(&self) -> &dyn HomeserverConnection {
109 self.homeserver_connection.as_ref()
110 }
111
112 fn url_builder(&self) -> &UrlBuilder {
113 &self.url_builder
114 }
115
116 fn limiter(&self) -> &Limiter {
117 &self.limiter
118 }
119
120 fn clock(&self) -> BoxClock {
121 let clock = SystemClock::default();
122 Box::new(clock)
123 }
124
125 fn rng(&self) -> BoxRng {
126 #[expect(clippy::disallowed_methods)]
127 let mut rng = rng();
128
129 let rng = ChaChaRng::from_rng(&mut rng);
130 Box::new(rng)
131 }
132}
133
134#[must_use]
135pub fn schema(
136 repository_factory: BoxRepositoryFactory,
137 policy_factory: &Arc<PolicyFactory>,
138 homeserver_connection: impl HomeserverConnection + 'static,
139 site_config: SiteConfig,
140 password_manager: PasswordManager,
141 url_builder: UrlBuilder,
142 limiter: Limiter,
143) -> Schema {
144 let state = GraphQLState {
145 repository_factory,
146 policy_factory: Arc::clone(policy_factory),
147 homeserver_connection: Arc::new(homeserver_connection),
148 site_config,
149 password_manager,
150 url_builder,
151 limiter,
152 };
153 let state: BoxState = Box::new(state);
154
155 schema_builder().extension(Tracing).data(state).finish()
156}
157
158fn span_and_operation_for_graphql_request(
159 request: &mut async_graphql::Request,
160) -> (tracing::Span, GraphQLOperation) {
161 let span = info_span!(
162 "GraphQL operation",
163 "otel.name" = tracing::field::Empty,
164 "otel.kind" = "server",
165 { GRAPHQL_DOCUMENT } = request.query,
166 { GRAPHQL_OPERATION_NAME } = tracing::field::Empty,
167 { GRAPHQL_OPERATION_TYPE } = tracing::field::Empty,
168 );
169
170 let mut graphql_operation = GraphQLOperation {
171 operation_type: None,
172 operation_name: None,
173 };
174
175 let operation_name = request.operation_name.clone();
179 if let Ok(document) = request.parsed_query() {
180 match (&document.operations, operation_name) {
181 (DocumentOperations::Single(operation), None) => {
184 span.record("otel.name", format!("GraphQL {}", operation.node.ty));
185 span.record(
186 GRAPHQL_OPERATION_TYPE,
187 tracing::field::display(operation.node.ty),
188 );
189 graphql_operation.operation_type = Some(operation.node.ty);
190 }
191
192 (DocumentOperations::Multiple(operations), Some(name)) => {
193 if let Some((name, operation)) = operations.get_key_value(name.as_str()) {
194 span.record(
195 "otel.name",
196 format!("GraphQL {} {}", operation.node.ty, name),
197 );
198 span.record(
199 GRAPHQL_OPERATION_TYPE,
200 tracing::field::display(operation.node.ty),
201 );
202 span.record(GRAPHQL_OPERATION_NAME, tracing::field::display(name));
203 graphql_operation.operation_type = Some(operation.node.ty);
204 graphql_operation.operation_name = Some(name.to_string());
205 }
206 }
207
208 (DocumentOperations::Multiple(operations), None) if operations.len() == 1 => {
209 let mut iter = operations.iter();
210 let (name, operation) = iter.next().unwrap();
211 span.record(
212 "otel.name",
213 format!("GraphQL {} {}", operation.node.ty, name),
214 );
215 span.record(
216 GRAPHQL_OPERATION_TYPE,
217 tracing::field::display(operation.node.ty),
218 );
219 span.record(GRAPHQL_OPERATION_NAME, name.as_ref());
220 graphql_operation.operation_type = Some(operation.node.ty);
221 graphql_operation.operation_name = Some(name.to_string());
222 }
223
224 (DocumentOperations::Single(_), Some(_)) | (DocumentOperations::Multiple(_), None) => {}
228 }
229 }
230
231 (span, graphql_operation)
232}
233
234#[derive(Clone, Debug)]
237pub struct GraphQLOperation {
238 pub operation_type: Option<OperationType>,
240 pub operation_name: Option<String>,
242}
243
244#[derive(thiserror::Error, Debug)]
245pub enum RouteError {
246 #[error(transparent)]
247 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
248
249 #[error("Loading of some database objects failed")]
250 LoadFailed,
251
252 #[error("Invalid access token")]
253 InvalidToken,
254
255 #[error("Missing scope")]
256 MissingScope,
257
258 #[error(transparent)]
259 ParseRequest(#[from] async_graphql::ParseRequestError),
260}
261
262impl_from_error_for_route!(mas_storage::RepositoryError);
263
264impl IntoResponse for RouteError {
265 fn into_response(self) -> Response {
266 let sentry_event_id = record_error!(self, Self::Internal(_) | Self::LoadFailed);
267
268 let response = match self {
269 e @ (Self::Internal(_) | Self::LoadFailed) => {
270 let error = async_graphql::Error::new_with_source(e);
271 (
272 StatusCode::INTERNAL_SERVER_ERROR,
273 Json(serde_json::json!({"errors": [error]})),
274 )
275 .into_response()
276 }
277
278 Self::InvalidToken => {
279 let error = async_graphql::Error::new("Invalid token");
280 (
281 StatusCode::UNAUTHORIZED,
282 Json(serde_json::json!({"errors": [error]})),
283 )
284 .into_response()
285 }
286
287 Self::MissingScope => {
288 let error = async_graphql::Error::new("Missing urn:mas:graphql:* scope");
289 (
290 StatusCode::UNAUTHORIZED,
291 Json(serde_json::json!({"errors": [error]})),
292 )
293 .into_response()
294 }
295
296 Self::ParseRequest(e) => {
297 let error = async_graphql::Error::new_with_source(e);
298 (
299 StatusCode::BAD_REQUEST,
300 Json(serde_json::json!({"errors": [error]})),
301 )
302 .into_response()
303 }
304 };
305
306 (sentry_event_id, response).into_response()
307 }
308}
309
310async fn get_requester(
311 undocumented_oauth2_access: bool,
312 clock: &impl Clock,
313 activity_tracker: &BoundActivityTracker,
314 mut repo: BoxRepository,
315 session_info: &SessionInfo,
316 user_agent: Option<String>,
317 token: Option<&str>,
318) -> Result<Requester, RouteError> {
319 let entity = if let Some(token) = token {
320 if !undocumented_oauth2_access {
322 return Err(RouteError::InvalidToken);
323 }
324
325 let token = repo
326 .oauth2_access_token()
327 .find_by_token(token)
328 .await?
329 .ok_or(RouteError::InvalidToken)?;
330
331 let session = repo
332 .oauth2_session()
333 .lookup(token.session_id)
334 .await?
335 .ok_or(RouteError::LoadFailed)?;
336
337 activity_tracker
338 .record_oauth2_session(clock, &session)
339 .await;
340
341 let user = if let Some(user_id) = session.user_id {
343 let user = repo
344 .user()
345 .lookup(user_id)
346 .await?
347 .ok_or(RouteError::LoadFailed)?;
348 Some(user)
349 } else {
350 None
351 };
352
353 let user_valid = user.as_ref().is_none_or(User::is_valid);
355
356 if !token.is_valid(clock.now()) || !session.is_valid() || !user_valid {
357 return Err(RouteError::InvalidToken);
358 }
359
360 if !session.scope.contains("urn:mas:graphql:*") {
361 return Err(RouteError::MissingScope);
362 }
363
364 if let Some(user) = &user {
365 user.maybe_record_as_requester();
366 }
367
368 RequestingEntity::OAuth2Session(Box::new((session, user)))
369 } else {
370 let maybe_session = session_info.load_active_session(&mut repo).await?;
371
372 if let Some(session) = maybe_session.as_ref() {
373 activity_tracker
374 .record_browser_session(clock, session)
375 .await;
376 }
377
378 RequestingEntity::from(maybe_session)
379 };
380
381 let requester = Requester {
382 entity,
383 ip_address: activity_tracker.ip(),
384 user_agent,
385 };
386
387 repo.cancel().await?;
388 Ok(requester)
389}
390
391fn record_internal_errors(response: &async_graphql::Response) {
400 for error in &response.errors {
401 let Some(source) = error.source.as_deref() else {
402 continue;
403 };
404
405 let source: &(dyn std::error::Error + 'static) =
406 if let Some(err) = source.downcast_ref::<anyhow::Error>() {
407 err.as_ref()
408 } else if let Some(err) = source.downcast_ref::<mas_storage::RepositoryError>() {
409 err
410 } else {
411 continue;
412 };
413
414 let path = error
417 .path
418 .iter()
419 .filter_map(|segment| match segment {
420 async_graphql::PathSegment::Field(name) => Some(name.as_str()),
421 async_graphql::PathSegment::Index(_) => None,
422 })
423 .collect::<Vec<_>>()
424 .join(".");
425
426 tracing::error!(
427 error = source,
428 tags.graphql.path = path,
429 "Internal error while resolving a GraphQL request"
430 );
431 }
432}
433
434pub async fn post(
435 AxumState(schema): AxumState<Schema>,
436 Extension(ExtraRouterParameters {
437 undocumented_oauth2_access,
438 }): Extension<ExtraRouterParameters>,
439 clock: BoxClock,
440 repo: BoxRepository,
441 activity_tracker: BoundActivityTracker,
442 cookie_jar: CookieJar,
443 content_type: Option<TypedHeader<ContentType>>,
444 authorization: Option<TypedHeader<Authorization<Bearer>>>,
445 user_agent: Option<TypedHeader<headers::UserAgent>>,
446 body: Body,
447) -> Result<impl IntoResponse, RouteError> {
448 let body = body.into_data_stream();
449 let token = authorization
450 .as_ref()
451 .map(|TypedHeader(Authorization(bearer))| bearer.token());
452 let user_agent = user_agent.map(|TypedHeader(h)| h.to_string());
453 let (session_info, mut cookie_jar) = cookie_jar.session_info();
454 let requester = get_requester(
455 undocumented_oauth2_access,
456 &clock,
457 &activity_tracker,
458 repo,
459 &session_info,
460 user_agent,
461 token,
462 )
463 .await?;
464
465 let content_type = content_type.map(|TypedHeader(h)| h.to_string());
466
467 let mut request = async_graphql::http::receive_body(
468 content_type,
469 body.map_err(std::io::Error::other).into_async_read(),
470 MultipartOptions::default(),
471 )
472 .await?
473 .data(requester); let (span, operation) = span_and_operation_for_graphql_request(&mut request);
476 let mut response = schema.execute(request).instrument(span).await;
477 record_internal_errors(&response);
478
479 if has_session_ended(&mut response) {
480 let session_info = session_info.mark_session_ended(clock.now());
481 cookie_jar = cookie_jar.update_session_info(&session_info);
482 }
483
484 let cache_control = response
485 .cache_control
486 .value()
487 .and_then(|v| HeaderValue::from_str(&v).ok())
488 .map(|h| [(CACHE_CONTROL, h)]);
489
490 let headers = response.http_headers.clone();
491
492 Ok((
493 headers,
494 cache_control,
495 cookie_jar,
496 Extension(operation),
497 Json(response),
498 ))
499}
500
501pub async fn get(
502 AxumState(schema): AxumState<Schema>,
503 Extension(ExtraRouterParameters {
504 undocumented_oauth2_access,
505 }): Extension<ExtraRouterParameters>,
506 clock: BoxClock,
507 repo: BoxRepository,
508 activity_tracker: BoundActivityTracker,
509 cookie_jar: CookieJar,
510 authorization: Option<TypedHeader<Authorization<Bearer>>>,
511 user_agent: Option<TypedHeader<headers::UserAgent>>,
512 RawQuery(query): RawQuery,
513) -> Result<impl IntoResponse, InternalError> {
514 let token = authorization
515 .as_ref()
516 .map(|TypedHeader(Authorization(bearer))| bearer.token());
517 let user_agent = user_agent.map(|TypedHeader(h)| h.to_string());
518 let (session_info, mut cookie_jar) = cookie_jar.session_info();
519 let requester = get_requester(
520 undocumented_oauth2_access,
521 &clock,
522 &activity_tracker,
523 repo,
524 &session_info,
525 user_agent,
526 token,
527 )
528 .await?;
529
530 let mut request =
531 async_graphql::http::parse_query_string(&query.unwrap_or_default())?.data(requester);
532
533 let (span, operation) = span_and_operation_for_graphql_request(&mut request);
534 let mut response = schema.execute(request).instrument(span).await;
535 record_internal_errors(&response);
536
537 if has_session_ended(&mut response) {
538 let session_info = session_info.mark_session_ended(clock.now());
539 cookie_jar = cookie_jar.update_session_info(&session_info);
540 }
541
542 let cache_control = response
543 .cache_control
544 .value()
545 .and_then(|v| HeaderValue::from_str(&v).ok())
546 .map(|h| [(CACHE_CONTROL, h)]);
547
548 let headers = response.http_headers.clone();
549
550 Ok((
551 headers,
552 cache_control,
553 cookie_jar,
554 Extension(operation),
555 Json(response),
556 ))
557}
558
559pub type Schema = async_graphql::Schema<Query, Mutation, EmptySubscription>;
560pub type SchemaBuilder = async_graphql::SchemaBuilder<Query, Mutation, EmptySubscription>;
561
562#[must_use]
563pub fn schema_builder() -> SchemaBuilder {
564 async_graphql::Schema::build(Query::new(), Mutation::new(), EmptySubscription)
565 .register_output_type::<Node>()
566 .register_output_type::<CreationEvent>()
567}
568
569pub struct Requester {
570 entity: RequestingEntity,
571 ip_address: Option<IpAddr>,
572 user_agent: Option<String>,
573}
574
575impl Requester {
576 pub fn fingerprint(&self) -> RequesterFingerprint {
577 if let Some(ip) = self.ip_address {
578 RequesterFingerprint::new(ip)
579 } else {
580 RequesterFingerprint::EMPTY
581 }
582 }
583
584 pub fn for_policy(&self) -> mas_policy::Requester {
585 mas_policy::Requester {
586 ip_address: self.ip_address,
587 user_agent: self.user_agent.clone(),
588 }
589 }
590}
591
592impl Deref for Requester {
593 type Target = RequestingEntity;
594
595 fn deref(&self) -> &Self::Target {
596 &self.entity
597 }
598}
599
600#[derive(Debug, Clone, Default, PartialEq, Eq)]
602pub enum RequestingEntity {
603 #[default]
605 Anonymous,
606
607 BrowserSession(Box<BrowserSession>),
609
610 OAuth2Session(Box<(Session, Option<User>)>),
612}
613
614trait OwnerId {
615 fn owner_id(&self) -> Option<Ulid>;
616}
617
618impl OwnerId for User {
619 fn owner_id(&self) -> Option<Ulid> {
620 Some(self.id)
621 }
622}
623
624impl OwnerId for BrowserSession {
625 fn owner_id(&self) -> Option<Ulid> {
626 Some(self.user.id)
627 }
628}
629
630impl OwnerId for mas_data_model::UserEmail {
631 fn owner_id(&self) -> Option<Ulid> {
632 Some(self.user_id)
633 }
634}
635
636impl OwnerId for Session {
637 fn owner_id(&self) -> Option<Ulid> {
638 self.user_id
639 }
640}
641
642impl OwnerId for mas_data_model::CompatSession {
643 fn owner_id(&self) -> Option<Ulid> {
644 Some(self.user_id)
645 }
646}
647
648impl OwnerId for mas_data_model::UpstreamOAuthLink {
649 fn owner_id(&self) -> Option<Ulid> {
650 self.user_id
651 }
652}
653
654pub struct UserId(Ulid);
656
657impl OwnerId for UserId {
658 fn owner_id(&self) -> Option<Ulid> {
659 Some(self.0)
660 }
661}
662
663impl RequestingEntity {
664 fn browser_session(&self) -> Option<&BrowserSession> {
665 match self {
666 Self::BrowserSession(session) => Some(session),
667 Self::OAuth2Session(_) | Self::Anonymous => None,
668 }
669 }
670
671 fn user(&self) -> Option<&User> {
672 match self {
673 Self::BrowserSession(session) => Some(&session.user),
674 Self::OAuth2Session(tuple) => tuple.1.as_ref(),
675 Self::Anonymous => None,
676 }
677 }
678
679 fn oauth2_session(&self) -> Option<&Session> {
680 match self {
681 Self::OAuth2Session(tuple) => Some(&tuple.0),
682 Self::BrowserSession(_) | Self::Anonymous => None,
683 }
684 }
685
686 fn is_owner_or_admin(&self, resource: &impl OwnerId) -> bool {
688 if self.is_admin() {
690 return true;
691 }
692
693 let Some(owner_id) = resource.owner_id() else {
695 return false;
696 };
697
698 let Some(user) = self.user() else {
699 return false;
700 };
701
702 user.id == owner_id
703 }
704
705 fn is_admin(&self) -> bool {
706 match self {
707 Self::OAuth2Session(tuple) => {
708 tuple.0.scope.contains("urn:mas:admin")
711 }
712 Self::BrowserSession(_) | Self::Anonymous => false,
713 }
714 }
715}
716
717impl From<BrowserSession> for RequestingEntity {
718 fn from(session: BrowserSession) -> Self {
719 Self::BrowserSession(Box::new(session))
720 }
721}
722
723impl<T> From<Option<T>> for RequestingEntity
724where
725 T: Into<RequestingEntity>,
726{
727 fn from(session: Option<T>) -> Self {
728 session.map(Into::into).unwrap_or_default()
729 }
730}
731
732#[derive(InputObject, Default, Clone, Copy)]
734pub struct DateFilter {
735 after: Option<DateTime<Utc>>,
737
738 before: Option<DateTime<Utc>>,
740}