diff --git a/Cargo.lock b/Cargo.lock index 7649aa1e..ab05b293 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,41 @@ dependencies = [ "memchr", ] +[[package]] +name = "aide" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0e3b97a21e41ec5c19bfd9b4fc1f7086be104f8b988681230247ffc91cc8ed" +dependencies = [ + "aide-macros", + "axum", + "axum-extra", + "bytes", + "cfg-if", + "http", + "indexmap 2.2.6", + "schemars", + "serde", + "serde_json", + "serde_qs", + "thiserror", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "aide-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0487f8598afe49e6bc950a613a678bd962c4a6f431022ded62643c8b990301a" +dependencies = [ + "darling 0.20.9", + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -3283,6 +3318,7 @@ dependencies = [ name = "mas-handlers" version = "0.9.0" dependencies = [ + "aide", "anyhow", "argon2", "async-graphql", @@ -3298,6 +3334,7 @@ dependencies = [ "futures-util", "headers", "hyper", + "indexmap 2.2.6", "insta", "lettre", "mas-axum-utils", @@ -3325,6 +3362,7 @@ dependencies = [ "rand", "rand_chacha", "rustls 0.23.12", + "schemars", "sentry", "serde", "serde_json", @@ -5249,6 +5287,7 @@ dependencies = [ "chrono", "dyn-clone", "indexmap 1.9.3", + "indexmap 2.2.6", "schemars_derive", "serde", "serde_json", @@ -5553,6 +5592,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_qs" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6" +dependencies = [ + "axum", + "futures", + "percent-encoding", + "serde", + "thiserror", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index 9361c5ce..f7227724 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,11 @@ mas-templates = { path = "./crates/templates/", version = "=0.9.0" } mas-tower = { path = "./crates/tower/", version = "=0.9.0" } oauth2-types = { path = "./crates/oauth2-types/", version = "=0.9.0" } +# OpenAPI schema generation and validation +[workspace.dependencies.aide] +version = "0.13.4" +features = ["axum", "axum-headers", "macros"] + # GraphQL server [workspace.dependencies.async-graphql] version = "7.0.7" diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 5b32e9c4..a0623672 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -36,7 +36,9 @@ axum-macros = "0.4.1" axum-extra.workspace = true rustls.workspace = true +aide.workspace = true async-graphql.workspace = true +schemars.workspace = true # Emails lettre.workspace = true @@ -65,6 +67,7 @@ zeroize = "1.8.1" base64ct = "1.6.0" camino.workspace = true chrono.workspace = true +indexmap = "2.2.6" psl = "2.1.55" time = "0.3.36" url.workspace = true diff --git a/crates/handlers/src/admin/mod.rs b/crates/handlers/src/admin/mod.rs new file mode 100644 index 00000000..3beefba4 --- /dev/null +++ b/crates/handlers/src/admin/mod.rs @@ -0,0 +1,94 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use aide::{ + axum::ApiRouter, + openapi::{OAuth2Flow, OAuth2Flows, OpenApi, SecurityScheme, Server, ServerVariable}, +}; +use axum::{Json, Router}; +use hyper::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE}; +use indexmap::IndexMap; +use mas_http::CorsLayerExt; +use mas_router::{OAuth2AuthorizationEndpoint, OAuth2TokenEndpoint, SimpleRoute}; +use tower_http::cors::{Any, CorsLayer}; + +pub fn router() -> (OpenApi, Router) +where + S: Clone + Send + Sync + 'static, +{ + let mut api = OpenApi::default(); + let router = ApiRouter::::new() + // TODO: add routes + .finish_api_with(&mut api, |t| { + t.title("Matrix Authentication Service admin API") + .security_scheme( + "oauth2", + SecurityScheme::OAuth2 { + flows: OAuth2Flows { + client_credentials: Some(OAuth2Flow::ClientCredentials { + refresh_url: Some(OAuth2TokenEndpoint::PATH.to_owned()), + token_url: OAuth2TokenEndpoint::PATH.to_owned(), + scopes: IndexMap::from([( + "urn:mas:admin".to_owned(), + "Grant access to the admin API".to_owned(), + )]), + }), + authorization_code: Some(OAuth2Flow::AuthorizationCode { + authorization_url: OAuth2AuthorizationEndpoint::PATH.to_owned(), + refresh_url: Some(OAuth2TokenEndpoint::PATH.to_owned()), + token_url: OAuth2TokenEndpoint::PATH.to_owned(), + scopes: IndexMap::from([( + "urn:mas:admin".to_owned(), + "Grant access to the admin API".to_owned(), + )]), + }), + implicit: None, + password: None, + }, + description: None, + extensions: IndexMap::default(), + }, + ) + .security_requirement_scopes("oauth2", ["urn:mas:admin"]) + .server(Server { + url: "{base}".to_owned(), + variables: IndexMap::from([( + "base".to_owned(), + ServerVariable { + default: "/".to_owned(), + ..ServerVariable::default() + }, + )]), + ..Server::default() + }) + }); + + let router = router + // Serve the OpenAPI spec as JSON + .route( + "/api/spec.json", + axum::routing::get({ + let res = Json(api.clone()); + move || std::future::ready(res.clone()) + }), + ) + .layer( + CorsLayer::new() + .allow_origin(Any) + .allow_methods(Any) + .allow_otel_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]), + ); + + (api, router) +} diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index cafbae81..d87e4f61 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -53,6 +53,7 @@ use sqlx::PgPool; use tower::util::AndThenLayer; use tower_http::cors::{Any, CorsLayer}; +mod admin; mod compat; mod graphql; mod health; @@ -89,6 +90,7 @@ pub use mas_axum_utils::{ pub use self::{ activity_tracker::{ActivityTracker, Bound as BoundActivityTracker}, + admin::router as admin_api_router, graphql::{ schema as graphql_schema, schema_builder as graphql_schema_builder, Schema as GraphQLSchema, },