manta_server/server/
routes.rs

1//! Axum router registration: maps every `/api/v1/` path to its handler.
2//!
3//! The OpenAPI JSON spec is served at `GET /openapi.json` and the
4//! Swagger UI is served at `GET /docs`.
5
6use std::sync::Arc;
7
8use axum::{
9  Extension, Router,
10  http::StatusCode,
11  middleware,
12  routing::{delete, get, post},
13};
14use tower_http::timeout::TimeoutLayer;
15use utoipa::OpenApi as _;
16use utoipa_swagger_ui::SwaggerUi;
17
18use super::ServerState;
19use super::api_doc::ApiDoc;
20use super::auth_middleware::{
21  AuthRateLimiter, rate_limit, strip_body_for_logs,
22};
23use super::handlers;
24
25/// Build the axum router with all API endpoints and OpenAPI doc routes.
26///
27/// The single global `request_timeout` is applied to every route as an
28/// outer `TimeoutLayer`. `POST /power` now returns immediately with a
29/// PCS transition id (the polling loop runs CLI-side), so it fits
30/// well under the default timeout — no per-route override is needed.
31pub fn build_router(state: Arc<ServerState>) -> Router {
32  let api = Router::new()
33    // --- GET endpoints ---
34    .route("/sessions", get(handlers::get_sessions))
35    .route("/analysis/images", get(handlers::get_image_analysis))
36    .route("/configurations", get(handlers::get_configurations))
37    .route("/nodes", get(handlers::get_nodes))
38    .route("/groups", get(handlers::get_groups))
39    .route("/groups/available", get(handlers::get_available_groups))
40    .route("/images", get(handlers::get_images))
41    .route("/templates", get(handlers::get_templates))
42    .route("/boot-parameters", get(handlers::get_boot_parameters))
43    .route("/kernel-parameters", get(handlers::get_kernel_parameters))
44    .route("/redfish-endpoints", get(handlers::get_redfish_endpoints))
45    // Canonical (group-centric) read endpoints
46    .route("/groups/nodes", get(handlers::get_groups_nodes))
47    .route("/groups/hardware", get(handlers::get_groups_hardware))
48    // Deprecated aliases retained for one release. Each handler logs
49    // a server-side warning and forwards to the canonical impl.
50    .route("/clusters", get(handlers::get_clusters_deprecated))
51    .route(
52      "/hardware-clusters",
53      get(handlers::get_hardware_clusters_deprecated),
54    )
55    .route(
56      "/hardware-nodes-list",
57      get(handlers::get_hardware_nodes_list),
58    )
59    // --- Write endpoints ---
60    // Nodes
61    .route("/nodes", post(handlers::add_node))
62    .route("/nodes/{id}", delete(handlers::delete_node))
63    // Groups
64    .route("/groups", post(handlers::create_group))
65    .route("/groups/{label}", delete(handlers::delete_group))
66    .route(
67      "/groups/{name}/members",
68      post(handlers::add_nodes_to_group).delete(handlers::delete_group_members),
69    )
70    // Boot parameters
71    .route(
72      "/boot-parameters",
73      post(handlers::add_boot_parameters)
74        .put(handlers::update_boot_parameters)
75        .delete(handlers::delete_boot_parameters),
76    )
77    // Redfish endpoints
78    .route(
79      "/redfish-endpoints",
80      post(handlers::add_redfish_endpoint)
81        .put(handlers::update_redfish_endpoint),
82    )
83    .route(
84      "/redfish-endpoints/{id}",
85      delete(handlers::delete_redfish_endpoint),
86    )
87    // Sessions (delete with dry_run)
88    .route("/sessions/{name}", delete(handlers::delete_session))
89    // Sessions (create)
90    .route("/sessions", post(handlers::create_session))
91    // Images (delete with dry_run)
92    .route("/images", delete(handlers::delete_images))
93    // Configurations (delete with dry_run)
94    .route("/configurations", delete(handlers::delete_configurations))
95    // Boot config (apply with dry_run)
96    .route("/boot-config", post(handlers::apply_boot_config))
97    // Kernel parameters (apply, add, delete)
98    .route(
99      "/kernel-parameters/apply",
100      post(handlers::apply_kernel_parameters),
101    )
102    .route(
103      "/kernel-parameters/add",
104      post(handlers::add_kernel_parameters),
105    )
106    .route(
107      "/kernel-parameters",
108      delete(handlers::delete_kernel_parameters),
109    )
110    // Migrate
111    .route("/migrate/nodes", post(handlers::migrate_nodes))
112    .route("/migrate/backup", post(handlers::migrate_backup))
113    .route("/migrate/restore", post(handlers::migrate_restore))
114    // Ephemeral environment
115    .route("/ephemeral-env", post(handlers::create_ephemeral_env))
116    // Power management — POST starts a PCS transition and returns
117    // immediately; GET snapshots the transition for the CLI poll loop.
118    .route("/power", post(handlers::post_power))
119    .route(
120      "/power/transitions/{id}",
121      get(handlers::get_power_transition),
122    )
123    // BOS session from template
124    .route(
125      "/templates/{name}/sessions",
126      post(handlers::post_template_session),
127    )
128    // CFS session logs (SSE)
129    .route("/sessions/{name}/logs", get(handlers::get_session_logs))
130    // SAT file apply — per-element endpoints. The CLI's `build_plan`
131    // walks the SAT file and dispatches one POST per artifact;
132    // `images[]` further splits into the three-step
133    // cfs-session/monitor/stamp pipeline that the CLI orchestrates.
134    .route(
135      "/sat-file/configurations",
136      post(handlers::post_sat_configuration),
137    )
138    .route(
139      "/sat-file/images/cfs-session",
140      post(handlers::post_sat_image_cfs_session),
141    )
142    .route(
143      "/sat-file/images/stamp",
144      post(handlers::post_sat_image_stamp),
145    )
146    .route(
147      "/sat-file/session-templates",
148      post(handlers::post_sat_session_template),
149    )
150    .route("/sat-file/validate", post(handlers::post_sat_validate))
151    // Health check
152    .route("/health", get(handlers::health))
153    // Hardware cluster member management
154    .route(
155      "/hardware-clusters/{target}/members",
156      post(handlers::add_hw_component).delete(handlers::delete_hw_component),
157    )
158    // Hardware cluster configuration (pin/unpin)
159    .route(
160      "/hardware-clusters/{target}/configuration",
161      post(handlers::apply_hw_configuration),
162    )
163    .merge(build_ws_routes())
164    // Apply the global request timeout to every route in the api
165    // sub-router.
166    .layer(TimeoutLayer::with_status_code(
167      StatusCode::REQUEST_TIMEOUT,
168      state.request_timeout,
169    ));
170
171  // /api/v1/auth/* — credential-handling sub-router. No Bearer
172  // extractor (chicken-and-egg). Two layered defences applied:
173  // (1) per-IP rate limit, (2) body redaction from any log span.
174  let limiter = AuthRateLimiter::new();
175  let auth = Router::new()
176    .route("/token", post(handlers::auth_token))
177    .route("/validate", post(handlers::auth_validate))
178    .layer(middleware::from_fn(strip_body_for_logs))
179    .layer(middleware::from_fn_with_state(state.clone(), rate_limit))
180    .layer(Extension(limiter));
181
182  Router::new()
183    .nest("/api/v1", api)
184    .nest("/api/v1/auth", auth)
185    .merge(SwaggerUi::new("/docs").url("/openapi.json", ApiDoc::openapi()))
186    // HSTS on every response. Browsers ignore HSTS over plain HTTP
187    // per RFC 6797, so this is a no-op when `allow_http = true`
188    // and active otherwise. Conservative one-year max-age; bump to
189    // include `preload` only after confirming the deployment can
190    // sustain it.
191    .layer(middleware::from_fn(add_hsts_header))
192    .with_state(state)
193}
194
195/// Inject `Strict-Transport-Security: max-age=31536000; includeSubDomains`
196/// on every outgoing response. Cheap; the header is constant.
197async fn add_hsts_header(
198  request: axum::extract::Request,
199  next: middleware::Next,
200) -> axum::response::Response {
201  let mut response = next.run(request).await;
202  response.headers_mut().insert(
203    axum::http::header::STRICT_TRANSPORT_SECURITY,
204    axum::http::HeaderValue::from_static("max-age=31536000; includeSubDomains"),
205  );
206  response
207}
208
209/// WebSocket upgrade routes — kept separate so they're easy to identify
210/// and so the upgrade protocol is not mixed with plain HTTP routes.
211fn build_ws_routes() -> Router<Arc<ServerState>> {
212  Router::new()
213    .route("/nodes/{xname}/console", get(handlers::console_node_ws))
214    .route(
215      "/sessions/{name}/console",
216      get(handlers::console_session_ws),
217    )
218}