diff --git a/crates/cli/src/server.rs b/crates/cli/src/server.rs index 9b753aa6e..40dc2fc33 100644 --- a/crates/cli/src/server.rs +++ b/crates/cli/src/server.rs @@ -201,9 +201,13 @@ pub fn build_router( mas_config::HttpResource::Human => { router.merge(mas_handlers::human_router::(templates.clone())) } - mas_config::HttpResource::GraphQL { playground } => { - router.merge(mas_handlers::graphql_router::(*playground)) - } + mas_config::HttpResource::GraphQL { + playground, + undocumented_oauth2_access, + } => router.merge(mas_handlers::graphql_router::( + *playground, + *undocumented_oauth2_access, + )), mas_config::HttpResource::Assets { path } => { let static_service = ServeDir::new(path) .append_index_html_on_directories(false) diff --git a/crates/config/src/sections/http.rs b/crates/config/src/sections/http.rs index 39e202073..7c51a2674 100644 --- a/crates/config/src/sections/http.rs +++ b/crates/config/src/sections/http.rs @@ -291,8 +291,12 @@ pub enum Resource { /// GraphQL endpoint GraphQL { /// Enabled the GraphQL playground - #[serde(default)] + #[serde(default, skip_serializing_if = "std::ops::Not::not")] playground: bool, + + /// Allow access for OAuth 2.0 clients (undocumented) + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + undocumented_oauth2_access: bool, }, /// OAuth-related APIs @@ -379,7 +383,10 @@ impl Default for HttpConfig { Resource::Human, Resource::OAuth, Resource::Compat, - Resource::GraphQL { playground: true }, + Resource::GraphQL { + playground: false, + undocumented_oauth2_access: false, + }, Resource::Assets { path: http_listener_assets_path_default(), }, diff --git a/crates/handlers/src/graphql/mod.rs b/crates/handlers/src/graphql/mod.rs index f56d50943..021d736bd 100644 --- a/crates/handlers/src/graphql/mod.rs +++ b/crates/handlers/src/graphql/mod.rs @@ -27,7 +27,7 @@ use axum::{ extract::{RawQuery, State as AxumState}, http::StatusCode, response::{Html, IntoResponse, Response}, - Json, + Extension, Json, }; use axum_extra::typed_header::TypedHeader; use chrono::{DateTime, Utc}; @@ -65,6 +65,13 @@ use crate::{impl_from_error_for_route, passwords::PasswordManager, BoundActivity #[cfg(test)] mod tests; +/// Extra parameters we get from the listener configuration, because they are +/// per-listener options. We pass them through request extensions. +#[derive(Debug, Clone)] +pub struct ExtraRouterParameters { + pub undocumented_oauth2_access: bool, +} + struct GraphQLState { pool: PgPool, homeserver_connection: Arc>, @@ -217,6 +224,7 @@ impl IntoResponse for RouteError { } async fn get_requester( + undocumented_oauth2_access: bool, clock: &impl Clock, activity_tracker: &BoundActivityTracker, mut repo: BoxRepository, @@ -224,6 +232,11 @@ async fn get_requester( token: Option<&str>, ) -> Result { let requester = if let Some(token) = token { + // If we haven't enabled undocumented_oauth2_access on the listener, we bail out + if !undocumented_oauth2_access { + return Err(RouteError::InvalidToken); + } + let token = repo .oauth2_access_token() .find_by_token(token) @@ -281,6 +294,9 @@ async fn get_requester( pub async fn post( AxumState(schema): AxumState, + Extension(ExtraRouterParameters { + undocumented_oauth2_access, + }): Extension, clock: BoxClock, repo: BoxRepository, activity_tracker: BoundActivityTracker, @@ -294,7 +310,15 @@ pub async fn post( .as_ref() .map(|TypedHeader(Authorization(bearer))| bearer.token()); let (session_info, _cookie_jar) = cookie_jar.session_info(); - let requester = get_requester(&clock, &activity_tracker, repo, session_info, token).await?; + let requester = get_requester( + undocumented_oauth2_access, + &clock, + &activity_tracker, + repo, + session_info, + token, + ) + .await?; let content_type = content_type.map(|TypedHeader(h)| h.to_string()); @@ -323,6 +347,9 @@ pub async fn post( pub async fn get( AxumState(schema): AxumState, + Extension(ExtraRouterParameters { + undocumented_oauth2_access, + }): Extension, clock: BoxClock, repo: BoxRepository, activity_tracker: BoundActivityTracker, @@ -334,7 +361,15 @@ pub async fn get( .as_ref() .map(|TypedHeader(Authorization(bearer))| bearer.token()); let (session_info, _cookie_jar) = cookie_jar.session_info(); - let requester = get_requester(&clock, &activity_tracker, repo, session_info, token).await?; + let requester = get_requester( + undocumented_oauth2_access, + &clock, + &activity_tracker, + repo, + session_info, + token, + ) + .await?; let request = async_graphql::http::parse_query_string(&query.unwrap_or_default())?.data(requester); diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 28375cbee..dc2132da1 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -30,8 +30,9 @@ use axum::{ http::Method, response::{Html, IntoResponse}, routing::{get, post}, - Router, + Extension, Router, }; +use graphql::ExtraRouterParameters; use headers::HeaderName; use hyper::{ header::{ @@ -108,7 +109,7 @@ where Router::new().route(mas_router::Healthcheck::route(), get(self::health::get)) } -pub fn graphql_router(playground: bool) -> Router +pub fn graphql_router(playground: bool, undocumented_oauth2_access: bool) -> Router where S: Clone + Send + Sync + 'static, graphql::Schema: FromRef, @@ -123,6 +124,11 @@ where mas_router::GraphQL::route(), get(self::graphql::get).post(self::graphql::post), ) + // Pass the undocumented_oauth2_access parameter through the request extension, as it is + // per-listener + .layer(Extension(ExtraRouterParameters { + undocumented_oauth2_access, + })) .layer( CorsLayer::new() .allow_origin(Any) diff --git a/crates/handlers/src/test_utils.rs b/crates/handlers/src/test_utils.rs index 497e11170..62674c17c 100644 --- a/crates/handlers/src/test_utils.rs +++ b/crates/handlers/src/test_utils.rs @@ -249,7 +249,9 @@ impl TestState { .merge(crate::api_router()) .merge(crate::compat_router()) .merge(crate::human_router(self.templates.clone())) - .merge(crate::graphql_router(false)) + // We enable undocumented_oauth2_access for the tests, as it is easier to query the API + // with it + .merge(crate::graphql_router(false, true)) .merge(crate::admin_api_router().1) .with_state(self.clone()) .into_service(); diff --git a/docs/config.schema.json b/docs/config.schema.json index 887cea4d8..fe7c91c17 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -35,8 +35,7 @@ "name": "compat" }, { - "name": "graphql", - "playground": true + "name": "graphql" }, { "name": "assets" @@ -742,7 +741,10 @@ }, "playground": { "description": "Enabled the GraphQL playground", - "default": false, + "type": "boolean" + }, + "undocumented_oauth2_access": { + "description": "Allow access for OAuth 2.0 clients (undocumented)", "type": "boolean" } }