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::{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 Policy: FromRequestParts<S>,
171 Templates: FromRef<S>,
172 UrlBuilder: FromRef<S>,
173 Arc<PolicyFactory>: FromRef<S>,
174 SiteConfig: FromRef<S>,
175 AppVersion: FromRef<S>,
176{
177 aide::generate::infer_responses(false);
180
181 aide::generate::in_context(|ctx| {
182 ctx.schema = schemars::generate::SchemaGenerator::new(
183 schemars::generate::SchemaSettings::openapi3().with(|settings| {
184 settings
188 .transforms
189 .retain(|transform| !transform.is::<AddNullable>());
190 }),
191 );
192 });
193
194 let mut api = OpenApi::default();
195 let router = ApiRouter::<S>::new()
196 .nest("/api/admin/v1", self::v1::router())
197 .finish_api_with(&mut api, finish);
198
199 let router = router
200 .route(
202 "/api/spec.json",
203 axum::routing::get({
204 let api = api.clone();
205 move |State(url_builder): State<UrlBuilder>| {
206 let mut api = api.clone();
208
209 let _ = TransformOpenApi::new(&mut api)
210 .server(Server {
211 url: url_builder.http_base().to_string(),
212 ..Server::default()
213 })
214 .security_scheme("oauth2", oauth_security_scheme(Some(&url_builder)));
215
216 std::future::ready(Json(api))
217 }
218 }),
219 )
220 .route(ApiDoc::route(), axum::routing::get(swagger))
222 .route(
223 ApiDocCallback::route(),
224 axum::routing::get(swagger_callback),
225 )
226 .layer(
227 CorsLayer::new()
228 .allow_origin(Any)
229 .allow_methods(Any)
230 .allow_otel_headers([
231 AUTHORIZATION,
232 ACCEPT,
233 CONTENT_TYPE,
234 HeaderName::from_static("x-requested-with"),
236 ]),
237 );
238
239 (api, router)
240}
241
242async fn swagger(
243 State(url_builder): State<UrlBuilder>,
244 State(templates): State<Templates>,
245) -> Result<Html<String>, InternalError> {
246 let ctx = ApiDocContext::from_url_builder(&url_builder);
247 let res = templates.render_swagger(&ctx)?;
248 Ok(Html(res))
249}
250
251async fn swagger_callback(
252 State(url_builder): State<UrlBuilder>,
253 State(templates): State<Templates>,
254) -> Result<Html<String>, InternalError> {
255 let ctx = ApiDocContext::from_url_builder(&url_builder);
256 let res = templates.render_swagger_callback(&ctx)?;
257 Ok(Html(res))
258}