manta_server/server/handlers/
auth.rs1use std::net::SocketAddr;
29use std::sync::Arc;
30
31use crate::server::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.audit,
115 true,
116 &req.username,
117 &source_ip,
118 &site_name,
119 );
120 Ok(Json(AuthTokenResponse { token }))
121 }
122 Err(e) => {
123 tracing::warn!(
124 "auth_token: backend rejected user={} site={} from={}: {}",
125 req.username,
126 site_name,
127 source_ip,
128 e
129 );
130 audit::send_auth_audit(
131 &state.audit,
132 false,
133 &req.username,
134 &source_ip,
135 &site_name,
136 );
137 Err(generic_invalid_credentials())
138 }
139 }
140}
141
142#[utoipa::path(post, path = "/auth/validate", tag = "auth",
144 params(SiteHeader),
145 request_body = ValidateTokenRequest,
146 responses(
147 (status = 200, description = "Token is valid"),
148 (status = 401, description = "Token rejected", body = ErrorResponse),
149 (status = 404, description = "Unknown site", body = ErrorResponse),
150 (status = 429, description = "Rate limit exceeded", body = ErrorResponse),
151 (status = 500, description = "Internal error", body = ErrorResponse),
152 )
153)]
154#[tracing::instrument(skip_all)]
155pub async fn auth_validate(
156 State(state): State<Arc<ServerState>>,
157 SiteName(site_name): SiteName,
158 Json(req): Json<ValidateTokenRequest>,
159) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
160 let infra = state.infra_context(&site_name).map_err(|e| {
161 tracing::warn!("auth_validate: site lookup failed: {}", e);
162 site_not_found(&site_name)
163 })?;
164 tracing::info!(site = %site_name, "auth_validate: token check requested");
165 match service::auth::validate_api_token(&infra, &req.token).await {
166 Ok(()) => {
167 tracing::info!(site = %site_name, "auth_validate: token accepted");
168 Ok(StatusCode::OK)
169 }
170 Err(e) => {
171 tracing::warn!("auth_validate: backend rejected token: {}", e);
172 Err(generic_invalid_credentials())
173 }
174 }
175}