mas_handlers/admin/
mod.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 OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7use std::sync::Arc;
8
9use aide::{
10    axum::ApiRouter,
11    openapi::{OAuth2Flow, OAuth2Flows, OpenApi, SecurityScheme, Server, Tag},
12    transform::TransformOpenApi,
13};
14use axum::{
15    Json, Router,
16    extract::{FromRef, FromRequestParts, State},
17    http::HeaderName,
18    response::Html,
19};
20use hyper::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
21use indexmap::IndexMap;
22use mas_axum_utils::InternalError;
23use mas_data_model::{BoxRng, SiteConfig};
24use mas_http::CorsLayerExt;
25use mas_matrix::HomeserverConnection;
26use mas_policy::PolicyFactory;
27use mas_router::{
28    ApiDoc, ApiDocCallback, OAuth2AuthorizationEndpoint, OAuth2TokenEndpoint, Route, SimpleRoute,
29    UrlBuilder,
30};
31use mas_templates::{ApiDocContext, Templates};
32use tower_http::cors::{Any, CorsLayer};
33
34mod call_context;
35mod model;
36mod params;
37mod response;
38mod schema;
39mod v1;
40
41use self::call_context::CallContext;
42use crate::passwords::PasswordManager;
43
44fn finish(t: TransformOpenApi) -> TransformOpenApi {
45    t.title("Matrix Authentication Service admin API")
46        .tag(Tag {
47            name: "server".to_owned(),
48            description: Some("Information about the server".to_owned()),
49            ..Tag::default()
50        })
51        .tag(Tag {
52            name: "compat-session".to_owned(),
53            description: Some("Manage compatibility sessions from legacy clients".to_owned()),
54            ..Tag::default()
55        })
56        .tag(Tag {
57            name: "policy-data".to_owned(),
58            description: Some("Manage the dynamic policy data".to_owned()),
59            ..Tag::default()
60        })
61        .tag(Tag {
62            name: "oauth2-session".to_owned(),
63            description: Some("Manage OAuth2 sessions".to_owned()),
64            ..Tag::default()
65        })
66        .tag(Tag {
67            name: "user".to_owned(),
68            description: Some("Manage users".to_owned()),
69            ..Tag::default()
70        })
71        .tag(Tag {
72            name: "user-email".to_owned(),
73            description: Some("Manage emails associated with users".to_owned()),
74            ..Tag::default()
75        })
76        .tag(Tag {
77            name: "user-session".to_owned(),
78            description: Some("Manage browser sessions of users".to_owned()),
79            ..Tag::default()
80        })
81        .tag(Tag {
82            name: "user-registration-token".to_owned(),
83            description: Some("Manage user registration tokens".to_owned()),
84            ..Tag::default()
85        })
86        .tag(Tag {
87            name: "upstream-oauth-link".to_owned(),
88            description: Some(
89                "Manage links between local users and identities from upstream OAuth 2.0 providers"
90                    .to_owned(),
91            ),
92            ..Default::default()
93        })
94        .tag(Tag {
95            name: "upstream-oauth-provider".to_owned(),
96            description: Some("Manage upstream OAuth 2.0 providers".to_owned()),
97            ..Tag::default()
98        })
99        .security_scheme("oauth2", oauth_security_scheme(None))
100        .security_scheme(
101            "token",
102            SecurityScheme::Http {
103                scheme: "bearer".to_owned(),
104                bearer_format: None,
105                description: Some("An access token with access to the admin API".to_owned()),
106                extensions: IndexMap::default(),
107            },
108        )
109        .security_requirement_scopes("oauth2", ["urn:mas:admin"])
110        .security_requirement_scopes("bearer", ["urn:mas:admin"])
111}
112
113fn oauth_security_scheme(url_builder: Option<&UrlBuilder>) -> SecurityScheme {
114    let (authorization_url, token_url) = if let Some(url_builder) = url_builder {
115        (
116            url_builder.oauth_authorization_endpoint().to_string(),
117            url_builder.oauth_token_endpoint().to_string(),
118        )
119    } else {
120        // This is a dirty fix for Swagger UI: when it joins the URLs with the
121        // base URL, if the path starts with a slash, it will go to the root of
122        // the domain instead of the API root.
123        // It works if we make it explicitly relative
124        (
125            format!(".{}", OAuth2AuthorizationEndpoint::PATH),
126            format!(".{}", OAuth2TokenEndpoint::PATH),
127        )
128    };
129
130    let scopes = IndexMap::from([(
131        "urn:mas:admin".to_owned(),
132        "Grant access to the admin API".to_owned(),
133    )]);
134
135    SecurityScheme::OAuth2 {
136        flows: OAuth2Flows {
137            client_credentials: Some(OAuth2Flow::ClientCredentials {
138                refresh_url: Some(token_url.clone()),
139                token_url: token_url.clone(),
140                scopes: scopes.clone(),
141            }),
142            authorization_code: Some(OAuth2Flow::AuthorizationCode {
143                authorization_url,
144                refresh_url: Some(token_url.clone()),
145                token_url,
146                scopes,
147            }),
148            implicit: None,
149            password: None,
150        },
151        description: None,
152        extensions: IndexMap::default(),
153    }
154}
155
156pub fn router<S>() -> (OpenApi, Router<S>)
157where
158    S: Clone + Send + Sync + 'static,
159    Arc<dyn HomeserverConnection>: FromRef<S>,
160    PasswordManager: FromRef<S>,
161    BoxRng: FromRequestParts<S>,
162    CallContext: FromRequestParts<S>,
163    Templates: FromRef<S>,
164    UrlBuilder: FromRef<S>,
165    Arc<PolicyFactory>: FromRef<S>,
166    SiteConfig: FromRef<S>,
167{
168    // We *always* want to explicitly set the possible responses, beacuse the
169    // infered ones are not necessarily correct
170    aide::generate::infer_responses(false);
171
172    aide::generate::in_context(|ctx| {
173        ctx.schema =
174            schemars::r#gen::SchemaGenerator::new(schemars::r#gen::SchemaSettings::openapi3());
175    });
176
177    let mut api = OpenApi::default();
178    let router = ApiRouter::<S>::new()
179        .nest("/api/admin/v1", self::v1::router())
180        .finish_api_with(&mut api, finish);
181
182    let router = router
183        // Serve the OpenAPI spec as JSON
184        .route(
185            "/api/spec.json",
186            axum::routing::get({
187                let api = api.clone();
188                move |State(url_builder): State<UrlBuilder>| {
189                    // Let's set the servers to the HTTP base URL
190                    let mut api = api.clone();
191
192                    let _ = TransformOpenApi::new(&mut api)
193                        .server(Server {
194                            url: url_builder.http_base().to_string(),
195                            ..Server::default()
196                        })
197                        .security_scheme("oauth2", oauth_security_scheme(Some(&url_builder)));
198
199                    std::future::ready(Json(api))
200                }
201            }),
202        )
203        // Serve the Swagger API reference
204        .route(ApiDoc::route(), axum::routing::get(swagger))
205        .route(
206            ApiDocCallback::route(),
207            axum::routing::get(swagger_callback),
208        )
209        .layer(
210            CorsLayer::new()
211                .allow_origin(Any)
212                .allow_methods(Any)
213                .allow_otel_headers([
214                    AUTHORIZATION,
215                    ACCEPT,
216                    CONTENT_TYPE,
217                    // Swagger will send this header, so we have to allow it to avoid CORS errors
218                    HeaderName::from_static("x-requested-with"),
219                ]),
220        );
221
222    (api, router)
223}
224
225async fn swagger(
226    State(url_builder): State<UrlBuilder>,
227    State(templates): State<Templates>,
228) -> Result<Html<String>, InternalError> {
229    let ctx = ApiDocContext::from_url_builder(&url_builder);
230    let res = templates.render_swagger(&ctx)?;
231    Ok(Html(res))
232}
233
234async fn swagger_callback(
235    State(url_builder): State<UrlBuilder>,
236    State(templates): State<Templates>,
237) -> Result<Html<String>, InternalError> {
238    let ctx = ApiDocContext::from_url_builder(&url_builder);
239    let res = templates.render_swagger_callback(&ctx)?;
240    Ok(Html(res))
241}