1use 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 (
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 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 .route(
185 "/api/spec.json",
186 axum::routing::get({
187 let api = api.clone();
188 move |State(url_builder): State<UrlBuilder>| {
189 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 .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 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}