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::{AppVersion, 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 schemars::transform::AddNullable;
33use tower_http::cors::{Any, CorsLayer};
34
35mod call_context;
36mod model;
37mod params;
38mod response;
39mod schema;
40mod v1;
41
42use self::call_context::CallContext;
43use crate::passwords::PasswordManager;
44
45fn finish(t: TransformOpenApi) -> TransformOpenApi {
46 t.title("Matrix Authentication Service admin API")
47 .tag(Tag {
48 name: "server".to_owned(),
49 description: Some("Information about the server".to_owned()),
50 ..Tag::default()
51 })
52 .tag(Tag {
53 name: "compat-session".to_owned(),
54 description: Some("Manage compatibility sessions from legacy clients".to_owned()),
55 ..Tag::default()
56 })
57 .tag(Tag {
58 name: "policy-data".to_owned(),
59 description: Some("Manage the dynamic policy data".to_owned()),
60 ..Tag::default()
61 })
62 .tag(Tag {
63 name: "oauth2-session".to_owned(),
64 description: Some("Manage OAuth2 sessions".to_owned()),
65 ..Tag::default()
66 })
67 .tag(Tag {
68 name: "user".to_owned(),
69 description: Some("Manage users".to_owned()),
70 ..Tag::default()
71 })
72 .tag(Tag {
73 name: "user-email".to_owned(),
74 description: Some("Manage emails associated with users".to_owned()),
75 ..Tag::default()
76 })
77 .tag(Tag {
78 name: "user-session".to_owned(),
79 description: Some("Manage browser sessions of users".to_owned()),
80 ..Tag::default()
81 })
82 .tag(Tag {
83 name: "user-registration-token".to_owned(),
84 description: Some("Manage user registration tokens".to_owned()),
85 ..Tag::default()
86 })
87 .tag(Tag {
88 name: "upstream-oauth-link".to_owned(),
89 description: Some(
90 "Manage links between local users and identities from upstream OAuth 2.0 providers"
91 .to_owned(),
92 ),
93 ..Default::default()
94 })
95 .tag(Tag {
96 name: "upstream-oauth-provider".to_owned(),
97 description: Some("Manage upstream OAuth 2.0 providers".to_owned()),
98 ..Tag::default()
99 })
100 .security_scheme("oauth2", oauth_security_scheme(None))
101 .security_scheme(
102 "token",
103 SecurityScheme::Http {
104 scheme: "bearer".to_owned(),
105 bearer_format: None,
106 description: Some("An access token with access to the admin API".to_owned()),
107 extensions: IndexMap::default(),
108 },
109 )
110 .security_requirement_scopes("oauth2", ["urn:mas:admin"])
111 .security_requirement_scopes("bearer", ["urn:mas:admin"])
112}
113
114fn oauth_security_scheme(url_builder: Option<&UrlBuilder>) -> SecurityScheme {
115 let (authorization_url, token_url) = if let Some(url_builder) = url_builder {
116 (
117 url_builder.oauth_authorization_endpoint().to_string(),
118 url_builder.oauth_token_endpoint().to_string(),
119 )
120 } else {
121 (
126 format!(".{}", OAuth2AuthorizationEndpoint::PATH),
127 format!(".{}", OAuth2TokenEndpoint::PATH),
128 )
129 };
130
131 let scopes = IndexMap::from([(
132 "urn:mas:admin".to_owned(),
133 "Grant access to the admin API".to_owned(),
134 )]);
135
136 SecurityScheme::OAuth2 {
137 flows: OAuth2Flows {
138 client_credentials: Some(OAuth2Flow::ClientCredentials {
139 refresh_url: Some(token_url.clone()),
140 token_url: token_url.clone(),
141 scopes: scopes.clone(),
142 }),
143 authorization_code: Some(OAuth2Flow::AuthorizationCode {
144 authorization_url,
145 refresh_url: Some(token_url.clone()),
146 token_url,
147 scopes,
148 }),
149 implicit: None,
150 password: None,
151 },
152 description: None,
153 extensions: IndexMap::default(),
154 }
155}
156
157pub fn router<S>() -> (OpenApi, Router<S>)
158where
159 S: Clone + Send + Sync + 'static,
160 Arc<dyn HomeserverConnection>: FromRef<S>,
161 PasswordManager: FromRef<S>,
162 BoxRng: FromRequestParts<S>,
163 CallContext: FromRequestParts<S>,
164 Templates: FromRef<S>,
165 UrlBuilder: FromRef<S>,
166 Arc<PolicyFactory>: FromRef<S>,
167 SiteConfig: FromRef<S>,
168 AppVersion: FromRef<S>,
169{
170 aide::generate::infer_responses(false);
173
174 aide::generate::in_context(|ctx| {
175 ctx.schema = schemars::generate::SchemaGenerator::new(
176 schemars::generate::SchemaSettings::openapi3().with(|settings| {
177 settings
181 .transforms
182 .retain(|transform| !transform.is::<AddNullable>());
183 }),
184 );
185 });
186
187 let mut api = OpenApi::default();
188 let router = ApiRouter::<S>::new()
189 .nest("/api/admin/v1", self::v1::router())
190 .finish_api_with(&mut api, finish);
191
192 let router = router
193 .route(
195 "/api/spec.json",
196 axum::routing::get({
197 let api = api.clone();
198 move |State(url_builder): State<UrlBuilder>| {
199 let mut api = api.clone();
201
202 let _ = TransformOpenApi::new(&mut api)
203 .server(Server {
204 url: url_builder.http_base().to_string(),
205 ..Server::default()
206 })
207 .security_scheme("oauth2", oauth_security_scheme(Some(&url_builder)));
208
209 std::future::ready(Json(api))
210 }
211 }),
212 )
213 .route(ApiDoc::route(), axum::routing::get(swagger))
215 .route(
216 ApiDocCallback::route(),
217 axum::routing::get(swagger_callback),
218 )
219 .layer(
220 CorsLayer::new()
221 .allow_origin(Any)
222 .allow_methods(Any)
223 .allow_otel_headers([
224 AUTHORIZATION,
225 ACCEPT,
226 CONTENT_TYPE,
227 HeaderName::from_static("x-requested-with"),
229 ]),
230 );
231
232 (api, router)
233}
234
235async fn swagger(
236 State(url_builder): State<UrlBuilder>,
237 State(templates): State<Templates>,
238) -> Result<Html<String>, InternalError> {
239 let ctx = ApiDocContext::from_url_builder(&url_builder);
240 let res = templates.render_swagger(&ctx)?;
241 Ok(Html(res))
242}
243
244async fn swagger_callback(
245 State(url_builder): State<UrlBuilder>,
246 State(templates): State<Templates>,
247) -> Result<Html<String>, InternalError> {
248 let ctx = ApiDocContext::from_url_builder(&url_builder);
249 let res = templates.render_swagger_callback(&ctx)?;
250 Ok(Html(res))
251}