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::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 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 .route(
143 "/api/spec.json",
144 axum::routing::get({
145 let api = api.clone();
146 move |State(url_builder): State<UrlBuilder>| {
147 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 .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 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}