mas_handlers/admin/
mod.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE 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::FancyError;
23use mas_http::CorsLayerExt;
24use mas_matrix::HomeserverConnection;
25use mas_policy::PolicyFactory;
26use mas_router::{
27    ApiDoc, ApiDocCallback, OAuth2AuthorizationEndpoint, OAuth2TokenEndpoint, Route, SimpleRoute,
28    UrlBuilder,
29};
30use mas_storage::BoxRng;
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: "compat-session".to_owned(),
48            description: Some("Manage compatibility sessions from legacy clients".to_owned()),
49            ..Tag::default()
50        })
51        .tag(Tag {
52            name: "policy-data".to_owned(),
53            description: Some("Manage the dynamic policy data".to_owned()),
54            ..Tag::default()
55        })
56        .tag(Tag {
57            name: "oauth2-session".to_owned(),
58            description: Some("Manage OAuth2 sessions".to_owned()),
59            ..Tag::default()
60        })
61        .tag(Tag {
62            name: "user".to_owned(),
63            description: Some("Manage users".to_owned()),
64            ..Tag::default()
65        })
66        .tag(Tag {
67            name: "user-email".to_owned(),
68            description: Some("Manage emails associated with users".to_owned()),
69            ..Tag::default()
70        })
71        .tag(Tag {
72            name: "user-session".to_owned(),
73            description: Some("Manage browser sessions of users".to_owned()),
74            ..Tag::default()
75        })
76        .tag(Tag {
77            name: "upstream-oauth-link".to_owned(),
78            description: Some(
79                "Manage links between local users and identities from upstream OAuth 2.0 providers"
80                    .to_owned(),
81            ),
82            ..Default::default()
83        })
84        .security_scheme(
85            "oauth2",
86            SecurityScheme::OAuth2 {
87                flows: OAuth2Flows {
88                    client_credentials: Some(OAuth2Flow::ClientCredentials {
89                        refresh_url: Some(OAuth2TokenEndpoint::PATH.to_owned()),
90                        token_url: OAuth2TokenEndpoint::PATH.to_owned(),
91                        scopes: IndexMap::from([(
92                            "urn:mas:admin".to_owned(),
93                            "Grant access to the admin API".to_owned(),
94                        )]),
95                    }),
96                    authorization_code: Some(OAuth2Flow::AuthorizationCode {
97                        authorization_url: OAuth2AuthorizationEndpoint::PATH.to_owned(),
98                        refresh_url: Some(OAuth2TokenEndpoint::PATH.to_owned()),
99                        token_url: OAuth2TokenEndpoint::PATH.to_owned(),
100                        scopes: IndexMap::from([(
101                            "urn:mas:admin".to_owned(),
102                            "Grant access to the admin API".to_owned(),
103                        )]),
104                    }),
105                    implicit: None,
106                    password: None,
107                },
108                description: None,
109                extensions: IndexMap::default(),
110            },
111        )
112        .security_requirement_scopes("oauth2", ["urn:mas:admin"])
113}
114
115pub fn router<S>() -> (OpenApi, Router<S>)
116where
117    S: Clone + Send + Sync + 'static,
118    Arc<dyn HomeserverConnection>: FromRef<S>,
119    PasswordManager: FromRef<S>,
120    BoxRng: FromRequestParts<S>,
121    CallContext: FromRequestParts<S>,
122    Templates: FromRef<S>,
123    UrlBuilder: FromRef<S>,
124    Arc<PolicyFactory>: FromRef<S>,
125{
126    // We *always* want to explicitly set the possible responses, beacuse the
127    // infered ones are not necessarily correct
128    aide::generate::infer_responses(false);
129
130    aide::generate::in_context(|ctx| {
131        ctx.schema =
132            schemars::r#gen::SchemaGenerator::new(schemars::r#gen::SchemaSettings::openapi3());
133    });
134
135    let mut api = OpenApi::default();
136    let router = ApiRouter::<S>::new()
137        .nest("/api/admin/v1", self::v1::router())
138        .finish_api_with(&mut api, finish);
139
140    let router = router
141        // Serve the OpenAPI spec as JSON
142        .route(
143            "/api/spec.json",
144            axum::routing::get({
145                let api = api.clone();
146                move |State(url_builder): State<UrlBuilder>| {
147                    // Let's set the servers to the HTTP base URL
148                    let mut api = api.clone();
149                    api.servers = vec![Server {
150                        url: url_builder.http_base().to_string(),
151                        ..Server::default()
152                    }];
153
154                    std::future::ready(Json(api))
155                }
156            }),
157        )
158        // Serve the Swagger API reference
159        .route(ApiDoc::route(), axum::routing::get(swagger))
160        .route(
161            ApiDocCallback::route(),
162            axum::routing::get(swagger_callback),
163        )
164        .layer(
165            CorsLayer::new()
166                .allow_origin(Any)
167                .allow_methods(Any)
168                .allow_otel_headers([
169                    AUTHORIZATION,
170                    ACCEPT,
171                    CONTENT_TYPE,
172                    // Swagger will send this header, so we have to allow it to avoid CORS errors
173                    HeaderName::from_static("x-requested-with"),
174                ]),
175        );
176
177    (api, router)
178}
179
180async fn swagger(
181    State(url_builder): State<UrlBuilder>,
182    State(templates): State<Templates>,
183) -> Result<Html<String>, FancyError> {
184    let ctx = ApiDocContext::from_url_builder(&url_builder);
185    let res = templates.render_swagger(&ctx)?;
186    Ok(Html(res))
187}
188
189async fn swagger_callback(
190    State(url_builder): State<UrlBuilder>,
191    State(templates): State<Templates>,
192) -> Result<Html<String>, FancyError> {
193    let ctx = ApiDocContext::from_url_builder(&url_builder);
194    let res = templates.render_swagger_callback(&ctx)?;
195    Ok(Html(res))
196}