1use axum::{
30 extract::Request,
31 http::{HeaderMap, StatusCode},
32 middleware::Next,
33 response::Response,
34 Json,
35};
36use serde::Serialize;
37use tracing::{debug, warn};
38
39#[cfg(feature = "k8s-token-review")]
40use k8s_openapi::api::authentication::v1::TokenReview;
41#[cfg(feature = "k8s-token-review")]
42use kube::{
43 config::{
44 AuthInfo, Cluster, KubeConfigOptions, Kubeconfig, NamedAuthInfo, NamedCluster, NamedContext,
45 },
46 Api, Client, Config,
47};
48#[cfg(feature = "k8s-token-review")]
49use std::env;
50#[cfg(feature = "k8s-token-review")]
51use tracing::error;
52
53#[derive(Serialize)]
55pub struct AuthError {
56 pub error: String,
57}
58
59#[cfg(feature = "k8s-token-review")]
61#[derive(Debug, Clone)]
62pub struct TokenReviewConfig {
63 pub audiences: Vec<String>,
65 pub allowed_namespaces: Vec<String>,
67 pub allowed_service_accounts: Vec<String>,
69}
70
71#[cfg(feature = "k8s-token-review")]
72impl TokenReviewConfig {
73 pub fn from_env() -> Self {
75 let audiences = match env::var("BIND_TOKEN_AUDIENCES") {
76 Ok(val) if !val.trim().is_empty() => val
77 .split(',')
78 .map(|s| s.trim().to_string())
79 .filter(|s| !s.is_empty())
80 .collect(),
81 _ => vec!["bindcar".to_string()],
82 };
83
84 let allowed_namespaces = env::var("BIND_ALLOWED_NAMESPACES")
85 .unwrap_or_default()
86 .split(',')
87 .map(|s| s.trim().to_string())
88 .filter(|s| !s.is_empty())
89 .collect();
90
91 let allowed_service_accounts = env::var("BIND_ALLOWED_SERVICE_ACCOUNTS")
92 .unwrap_or_default()
93 .split(',')
94 .map(|s| s.trim().to_string())
95 .filter(|s| !s.is_empty())
96 .collect();
97
98 let config = Self {
99 audiences,
100 allowed_namespaces,
101 allowed_service_accounts,
102 };
103
104 debug!("TokenReview config loaded: audiences={:?}, allowed_namespaces={:?}, allowed_service_accounts={:?}",
105 config.audiences, config.allowed_namespaces, config.allowed_service_accounts);
106
107 config
108 }
109
110 pub(crate) fn is_namespace_allowed(&self, namespace: &str) -> bool {
112 if self.allowed_namespaces.is_empty() {
114 return true;
115 }
116 self.allowed_namespaces.contains(&namespace.to_string())
117 }
118
119 pub(crate) fn is_service_account_allowed(&self, username: &str) -> bool {
121 if self.allowed_service_accounts.is_empty() {
123 return true;
124 }
125 self.allowed_service_accounts
126 .contains(&username.to_string())
127 }
128
129 pub(crate) fn extract_namespace(username: &str) -> Option<String> {
132 let parts: Vec<&str> = username.split(':').collect();
133 if parts.len() != 4 || parts[0] != "system" || parts[1] != "serviceaccount" {
134 return None;
135 }
136 Some(parts[2].to_string())
137 }
138}
139
140#[cfg(feature = "k8s-token-review")]
148#[derive(Debug)]
149pub enum KubeAuthMode {
150 Explicit {
152 server: String,
153 token_path: String,
154 ca_cert_path: String,
155 },
156 Default,
158}
159
160#[cfg(feature = "k8s-token-review")]
167pub fn detect_kube_auth_mode() -> KubeAuthMode {
168 let api_server = env::var("KUBE_API_SERVER").ok();
169 let token_path = env::var("KUBE_TOKEN_PATH").ok();
170 let ca_cert_path = env::var("KUBE_CA_CERT_PATH").ok();
171
172 let set_count = [&api_server, &token_path, &ca_cert_path]
174 .iter()
175 .filter(|v| v.is_some())
176 .count();
177
178 if set_count > 0 && set_count < 3 {
179 warn!(
180 "Partial KUBE_* env vars set ({}/3 present): \
181 KUBE_API_SERVER={}, KUBE_TOKEN_PATH={}, KUBE_CA_CERT_PATH={}. \
182 All three must be set for explicit auth. Falling back to try_default().",
183 set_count,
184 api_server.as_deref().unwrap_or("(not set)"),
185 token_path.as_deref().unwrap_or("(not set)"),
186 ca_cert_path.as_deref().unwrap_or("(not set)"),
187 );
188 }
189
190 match (api_server, token_path, ca_cert_path) {
191 (Some(server), Some(token_path), Some(ca_cert_path)) => KubeAuthMode::Explicit {
192 server,
193 token_path,
194 ca_cert_path,
195 },
196 _ => KubeAuthMode::Default,
197 }
198}
199
200#[cfg(feature = "k8s-token-review")]
209pub(crate) async fn build_explicit_kube_client(
210 server: String,
211 token_path: String,
212 ca_cert_path: String,
213) -> Result<Client, String> {
214 tokio::fs::read_to_string(&token_path)
218 .await
219 .map_err(|e| format!("failed to read token file '{}': {}", token_path, e))?;
220
221 let ca_bytes = tokio::fs::read(&ca_cert_path).await.map_err(|e| {
225 format!(
226 "failed to read CA certificate file '{}': {}",
227 ca_cert_path, e
228 )
229 })?;
230 let ca_pem = String::from_utf8_lossy(&ca_bytes);
231 if !ca_pem.contains("-----BEGIN CERTIFICATE-----") {
232 return Err(format!(
233 "CA certificate file '{}' does not contain a valid PEM certificate block",
234 ca_cert_path
235 ));
236 }
237
238 let kubeconfig = Kubeconfig {
242 clusters: vec![NamedCluster {
243 name: "standalone".to_string(),
244 cluster: Some(Cluster {
245 server: Some(server),
246 certificate_authority: Some(ca_cert_path),
247 ..Default::default()
248 }),
249 }],
250 auth_infos: vec![NamedAuthInfo {
251 name: "standalone".to_string(),
252 auth_info: Some(AuthInfo {
253 token_file: Some(token_path),
255 ..Default::default()
256 }),
257 }],
258 contexts: vec![NamedContext {
259 name: "standalone".to_string(),
260 context: Some(kube::config::Context {
261 cluster: "standalone".to_string(),
262 user: Some("standalone".to_string()),
263 ..Default::default()
264 }),
265 }],
266 current_context: Some("standalone".to_string()),
267 ..Default::default()
268 };
269
270 let config = Config::from_custom_kubeconfig(kubeconfig, &KubeConfigOptions::default())
271 .await
272 .map_err(|e| format!("failed to build Kubernetes client config: {}", e))?;
273
274 Client::try_from(config).map_err(|e| format!("failed to create Kubernetes client: {}", e))
275}
276
277#[cfg(feature = "k8s-token-review")]
283async fn build_kube_client() -> Result<Client, String> {
284 match detect_kube_auth_mode() {
285 KubeAuthMode::Explicit {
286 server,
287 token_path,
288 ca_cert_path,
289 } => {
290 debug!(
291 "Kubernetes auth mode: explicit (KUBE_API_SERVER={})",
292 server
293 );
294 build_explicit_kube_client(server, token_path, ca_cert_path).await
295 }
296 KubeAuthMode::Default => {
297 debug!("Kubernetes auth mode: try_default (KUBECONFIG / ~/.kube/config / in-cluster)");
298 Client::try_default()
299 .await
300 .map_err(|e| format!("Failed to create Kubernetes client: {}", e))
301 }
302 }
303}
304
305pub async fn authenticate(
319 headers: HeaderMap,
320 request: Request,
321 next: Next,
322) -> Result<Response, (StatusCode, Json<AuthError>)> {
323 let auth_header = headers
325 .get("authorization")
326 .and_then(|h| h.to_str().ok())
327 .ok_or_else(|| {
328 warn!("Missing Authorization header");
329 (
330 StatusCode::UNAUTHORIZED,
331 Json(AuthError {
332 error: "Missing Authorization header".to_string(),
333 }),
334 )
335 })?;
336
337 if !auth_header.starts_with("Bearer ") {
339 warn!("Invalid Authorization header format");
340 return Err((
341 StatusCode::UNAUTHORIZED,
342 Json(AuthError {
343 error: "Invalid Authorization header format. Expected: Bearer <token>".to_string(),
344 }),
345 ));
346 }
347
348 let token = &auth_header[7..]; if token.is_empty() {
352 warn!("Empty token in Authorization header");
353 return Err((
354 StatusCode::UNAUTHORIZED,
355 Json(AuthError {
356 error: "Empty token".to_string(),
357 }),
358 ));
359 }
360
361 #[cfg(feature = "k8s-token-review")]
363 if let Err(e) = validate_token_with_k8s(token).await {
364 warn!("Token validation failed: {}", e);
365 return Err((
366 StatusCode::UNAUTHORIZED,
367 Json(AuthError {
368 error: format!("Token validation failed: {}", e),
369 }),
370 ));
371 }
372
373 #[cfg(feature = "k8s-token-review")]
374 debug!("Token validated with Kubernetes TokenReview API");
375
376 #[cfg(not(feature = "k8s-token-review"))]
377 debug!("Token validation: basic mode (presence check only)");
378
379 Ok(next.run(request).await)
380}
381
382#[cfg(feature = "k8s-token-review")]
400pub(crate) async fn validate_token_with_k8s(token: &str) -> Result<(), String> {
401 let config = TokenReviewConfig::from_env();
403
404 let client = build_kube_client().await?;
406
407 let token_reviews: Api<TokenReview> = Api::all(client);
409
410 let audiences = if !config.audiences.is_empty() {
412 Some(config.audiences.clone())
413 } else {
414 None
415 };
416
417 let token_review = TokenReview {
418 metadata: Default::default(),
419 spec: k8s_openapi::api::authentication::v1::TokenReviewSpec {
420 token: Some(token.to_string()),
421 audiences,
422 },
423 status: None,
424 };
425
426 let result = token_reviews
428 .create(&Default::default(), &token_review)
429 .await
430 .map_err(|e| {
431 error!("TokenReview API call failed: {}", e);
432 format!("Failed to validate token with Kubernetes API: {}", e)
433 })?;
434
435 let status = result
437 .status
438 .ok_or_else(|| "TokenReview status not available".to_string())?;
439
440 if status.authenticated != Some(true) {
441 let error_msg = status
442 .error
443 .unwrap_or_else(|| "Token not authenticated".to_string());
444 warn!("Token authentication failed: {}", error_msg);
445 return Err(error_msg);
446 }
447
448 debug!("Token authenticated successfully");
449
450 let user = status.user.ok_or_else(|| {
452 warn!("TokenReview succeeded but no user information returned");
453 "No user information in TokenReview response".to_string()
454 })?;
455
456 let username = user.username.as_deref().unwrap_or("");
457 debug!("Authenticated user: {}", username);
458
459 if let Some(namespace) = TokenReviewConfig::extract_namespace(username) {
461 if !config.is_namespace_allowed(&namespace) {
462 warn!(
463 "ServiceAccount from unauthorized namespace: {} (from {})",
464 namespace, username
465 );
466 return Err(format!(
467 "ServiceAccount from unauthorized namespace: {}",
468 namespace
469 ));
470 }
471 debug!("Namespace {} is allowed", namespace);
472 }
473
474 if !config.is_service_account_allowed(username) {
476 warn!("ServiceAccount not in allowlist: {}", username);
477 return Err(format!("ServiceAccount not authorized: {}", username));
478 }
479
480 debug!("ServiceAccount {} is allowed", username);
481 Ok(())
482}