manta_server/server/handlers/
auth.rs1use std::net::SocketAddr;
29use std::sync::Arc;
30
31use crate::server::common::audit;
32use axum::{
33 Json,
34 extract::{ConnectInfo, State},
35 http::StatusCode,
36 response::IntoResponse,
37};
38use manta_shared::types::auth::{
39 AuthTokenRequest, AuthTokenResponse, ValidateTokenRequest,
40};
41
42use super::{ErrorResponse, ServerState, SiteHeader, SiteName};
43use crate::service;
44
45fn generic_invalid_credentials() -> (StatusCode, Json<ErrorResponse>) {
48 (
49 StatusCode::UNAUTHORIZED,
50 Json(ErrorResponse {
51 error: "invalid credentials".to_string(),
52 }),
53 )
54}
55
56fn site_not_found(site: &str) -> (StatusCode, Json<ErrorResponse>) {
64 (
65 StatusCode::NOT_FOUND,
66 Json(ErrorResponse {
67 error: format!("site '{site}' not found"),
68 }),
69 )
70}
71
72#[utoipa::path(post, path = "/auth/token", tag = "auth",
74 params(SiteHeader),
75 request_body = AuthTokenRequest,
76 responses(
77 (status = 200, description = "Token issued", body = AuthTokenResponse),
78 (status = 401, description = "Invalid credentials", body = ErrorResponse),
79 (status = 404, description = "Unknown site", body = ErrorResponse),
80 (status = 429, description = "Rate limit exceeded", body = ErrorResponse),
81 (status = 500, description = "Internal error", body = ErrorResponse),
82 )
83)]
84#[tracing::instrument(skip_all)]
85pub async fn auth_token(
86 State(state): State<Arc<ServerState>>,
87 SiteName(site_name): SiteName,
88 ConnectInfo(peer): ConnectInfo<SocketAddr>,
89 Json(req): Json<AuthTokenRequest>,
90) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
91 let infra = state.infra_context(&site_name).map_err(|e| {
92 tracing::warn!("auth_token: site lookup failed: {}", e);
93 site_not_found(&site_name)
94 })?;
95 let source_ip = peer.ip().to_string();
96
97 tracing::info!(
98 user = %req.username,
99 site = %site_name,
100 from = %source_ip,
101 "auth_token: credential exchange requested"
102 );
103
104 match service::auth::get_api_token(&infra, &req.username, &req.password).await
105 {
106 Ok(token) => {
107 tracing::info!(
108 user = %req.username,
109 site = %site_name,
110 from = %source_ip,
111 "auth_token: token issued"
112 );
113 audit::send_auth_audit(
114 state.auditor.as_ref(),
115 "success",
116 &req.username,
117 &source_ip,
118 &site_name,
119 )
120 .await;
121 Ok(Json(AuthTokenResponse { token }))
122 }
123 Err(e) => {
124 tracing::warn!(
125 "auth_token: backend rejected user={} site={} from={}: {}",
126 req.username,
127 site_name,
128 source_ip,
129 e
130 );
131 audit::send_auth_audit(
132 state.auditor.as_ref(),
133 "failure",
134 &req.username,
135 &source_ip,
136 &site_name,
137 )
138 .await;
139 Err(generic_invalid_credentials())
140 }
141 }
142}
143
144#[utoipa::path(post, path = "/auth/validate", tag = "auth",
146 params(SiteHeader),
147 request_body = ValidateTokenRequest,
148 responses(
149 (status = 200, description = "Token is valid"),
150 (status = 401, description = "Token rejected", body = ErrorResponse),
151 (status = 404, description = "Unknown site", body = ErrorResponse),
152 (status = 429, description = "Rate limit exceeded", body = ErrorResponse),
153 (status = 500, description = "Internal error", body = ErrorResponse),
154 )
155)]
156#[tracing::instrument(skip_all)]
157pub async fn auth_validate(
158 State(state): State<Arc<ServerState>>,
159 SiteName(site_name): SiteName,
160 Json(req): Json<ValidateTokenRequest>,
161) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
162 let infra = state.infra_context(&site_name).map_err(|e| {
163 tracing::warn!("auth_validate: site lookup failed: {}", e);
164 site_not_found(&site_name)
165 })?;
166 tracing::info!(site = %site_name, "auth_validate: token check requested");
167 match service::auth::validate_api_token(&infra, &req.token).await {
168 Ok(()) => {
169 tracing::info!(site = %site_name, "auth_validate: token accepted");
170 Ok(StatusCode::OK)
171 }
172 Err(e) => {
173 tracing::warn!("auth_validate: backend rejected token: {}", e);
174 Err(generic_invalid_credentials())
175 }
176 }
177}