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