1use std::sync::Arc;
9
10use axum::{
11 Extension, Router,
12 http::StatusCode,
13 middleware,
14 routing::{delete, get, post, put},
15};
16use tower_http::{catch_panic::CatchPanicLayer, timeout::TimeoutLayer};
17use utoipa::OpenApi as _;
18use utoipa_swagger_ui::SwaggerUi;
19
20use super::ServerState;
21use super::api_doc::ApiDoc;
22use super::audit::audit_requests;
23use super::auth_middleware::{
24 AuthRateLimiter, rate_limit, strip_body_for_logs,
25};
26use super::handlers;
27
28pub fn build_router(state: Arc<ServerState>) -> Router {
46 let api = Router::new()
47 .route("/sessions", get(handlers::get_sessions))
49 .route("/analysis/images", get(handlers::get_image_analysis))
50 .route("/configurations", get(handlers::get_configurations))
51 .route("/nodes", get(handlers::get_nodes))
52 .route("/groups", get(handlers::get_groups))
53 .route("/groups/available", get(handlers::get_available_groups))
54 .route("/images", get(handlers::get_images))
55 .route("/templates", get(handlers::get_templates))
56 .route("/boot-parameters", get(handlers::get_boot_parameters))
57 .route("/kernel-parameters", get(handlers::get_kernel_parameters))
58 .route("/redfish-endpoints", get(handlers::get_redfish_endpoints))
59 .route("/groups/nodes", get(handlers::get_groups_nodes))
61 .route("/groups/hardware", get(handlers::get_groups_hardware))
62 .route("/clusters", get(handlers::get_clusters_deprecated))
65 .route(
66 "/hardware-clusters",
67 get(handlers::get_hardware_clusters_deprecated),
68 )
69 .route(
70 "/hardware-nodes-list",
71 get(handlers::get_hardware_nodes_list),
72 )
73 .route("/nodes", post(handlers::add_node))
76 .route("/nodes/{id}", delete(handlers::delete_node))
77 .route("/groups", post(handlers::create_group))
79 .route("/groups/{label}", delete(handlers::delete_group))
80 .route(
81 "/groups/{name}/members",
82 post(handlers::add_nodes_to_group).delete(handlers::delete_group_members),
83 )
84 .route(
86 "/boot-parameters",
87 post(handlers::add_boot_parameters)
88 .put(handlers::update_boot_parameters)
89 .delete(handlers::delete_boot_parameters),
90 )
91 .route(
93 "/redfish-endpoints",
94 post(handlers::add_redfish_endpoint)
95 .put(handlers::update_redfish_endpoint),
96 )
97 .route(
98 "/redfish-endpoints/{id}",
99 delete(handlers::delete_redfish_endpoint),
100 )
101 .route("/sessions/{name}", delete(handlers::delete_session))
103 .route("/sessions", post(handlers::create_session))
105 .route("/images", delete(handlers::delete_images))
107 .route("/configurations", delete(handlers::delete_configurations))
109 .route("/boot-config", post(handlers::apply_boot_config))
111 .route(
114 "/runtime-configuration",
115 put(handlers::apply_runtime_configuration),
116 )
117 .route(
119 "/kernel-parameters/apply",
120 post(handlers::apply_kernel_parameters),
121 )
122 .route(
123 "/kernel-parameters/add",
124 post(handlers::add_kernel_parameters),
125 )
126 .route(
127 "/kernel-parameters",
128 delete(handlers::delete_kernel_parameters),
129 )
130 .route("/migrate/nodes", post(handlers::migrate_nodes))
132 .route("/migrate/backup", post(handlers::migrate_backup))
133 .route("/migrate/restore", post(handlers::migrate_restore))
134 .route("/ephemeral-env", post(handlers::create_ephemeral_env))
136 .route("/power", post(handlers::post_power))
139 .route(
140 "/power/transitions/{id}",
141 get(handlers::get_power_transition),
142 )
143 .route(
145 "/templates/{name}/sessions",
146 post(handlers::post_template_session),
147 )
148 .route("/sessions/{name}/logs", get(handlers::get_session_logs))
150 .route(
155 "/sat-file/configurations",
156 post(handlers::post_sat_configuration),
157 )
158 .route(
159 "/sat-file/images/cfs-session",
160 post(handlers::post_sat_image_cfs_session),
161 )
162 .route(
163 "/sat-file/images/stamp",
164 post(handlers::post_sat_image_stamp),
165 )
166 .route(
167 "/sat-file/session-templates",
168 post(handlers::post_sat_session_template),
169 )
170 .route("/health", get(handlers::health))
172 .route(
174 "/hardware-clusters/{target}/members",
175 post(handlers::add_hw_component).delete(handlers::delete_hw_component),
176 )
177 .route(
179 "/hardware-clusters/{target}/configuration",
180 post(handlers::apply_hw_configuration),
181 )
182 .merge(build_ws_routes());
183 let api = with_api_layers(api, &state);
184
185 let limiter = AuthRateLimiter::new();
189 let auth = Router::new()
190 .route("/token", post(handlers::auth_token))
191 .route("/validate", post(handlers::auth_validate))
192 .layer(middleware::from_fn(strip_body_for_logs))
193 .layer(middleware::from_fn_with_state(state.clone(), rate_limit))
194 .layer(Extension(limiter));
195
196 Router::new()
197 .nest("/v2", api)
198 .nest("/v2/auth", auth)
199 .merge(SwaggerUi::new("/docs").url("/openapi.json", ApiDoc::openapi()))
200 .layer(middleware::from_fn(add_hsts_header))
206 .with_state(state)
207}
208
209fn with_api_layers(
211 api: Router<Arc<ServerState>>,
212 state: &Arc<ServerState>,
213) -> Router<Arc<ServerState>> {
214 api
215 .layer(TimeoutLayer::with_status_code(
218 StatusCode::REQUEST_TIMEOUT,
219 state.request_timeout,
220 ))
221 .layer(CatchPanicLayer::new())
224 .layer(middleware::from_fn_with_state(
228 state.clone(),
229 audit_requests,
230 ))
231}
232
233async fn add_hsts_header(
236 request: axum::extract::Request,
237 next: middleware::Next,
238) -> axum::response::Response {
239 let mut response = next.run(request).await;
240 response.headers_mut().insert(
241 axum::http::header::STRICT_TRANSPORT_SECURITY,
242 axum::http::HeaderValue::from_static("max-age=31536000; includeSubDomains"),
243 );
244 response
245}
246
247fn build_ws_routes() -> Router<Arc<ServerState>> {
250 Router::new()
251 .route("/nodes/{xname}/console", get(handlers::console_node_ws))
252 .route(
253 "/sessions/{name}/console",
254 get(handlers::console_session_ws),
255 )
256}
257
258#[cfg(test)]
259mod tests {
260 use std::collections::HashMap;
261 use std::time::Duration;
262
263 use axum::{body::Body, http::Request};
264 use tower::ServiceExt as _;
265
266 use super::*;
267 use crate::server::audit::{AuditLog, CaptureSink, event::Verdict};
268
269 #[tokio::test]
272 async fn panicking_handler_is_audited_as_failed() {
273 let sink = Arc::new(CaptureSink::default());
274 let state = Arc::new(ServerState {
275 sites: HashMap::new(),
276 console_inactivity_timeout: Duration::from_secs(1),
277 audit: AuditLog::with_sinks(vec![sink.clone()]),
278 auth_rate_limit_per_minute: None,
279 request_timeout: Duration::from_secs(60),
280 shutdown_grace_period: Duration::from_secs(1),
281 migrate_backup_root: None,
282 });
283 let api = Router::new().route(
284 "/boom",
285 axum::routing::delete(|| async {
286 panic!("handler bug");
287 #[allow(unreachable_code)]
288 ()
289 }),
290 );
291 let app = Router::new()
292 .nest("/v2", with_api_layers(api, &state))
293 .with_state(state);
294
295 let response = app
296 .oneshot(Request::delete("/v2/boom").body(Body::empty()).unwrap())
297 .await
298 .unwrap();
299
300 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
301 let events = sink.0.lock().unwrap();
302 assert_eq!(events.len(), 1);
303 assert_eq!(events[0].outcome.result, Verdict::Failed);
304 assert_eq!(events[0].request.route, "/v2/boom");
305 }
306}