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::{Api, Client};
43#[cfg(feature = "k8s-token-review")]
44use std::env;
45#[cfg(feature = "k8s-token-review")]
46use tracing::error;
47
48#[derive(Serialize)]
50pub struct AuthError {
51 pub error: String,
52}
53
54#[cfg(feature = "k8s-token-review")]
56#[derive(Debug, Clone)]
57pub struct TokenReviewConfig {
58 pub audiences: Vec<String>,
60 pub allowed_namespaces: Vec<String>,
62 pub allowed_service_accounts: Vec<String>,
64}
65
66#[cfg(feature = "k8s-token-review")]
67impl TokenReviewConfig {
68 pub fn from_env() -> Self {
70 let audiences = match env::var("BIND_TOKEN_AUDIENCES") {
71 Ok(val) if !val.trim().is_empty() => val
72 .split(',')
73 .map(|s| s.trim().to_string())
74 .filter(|s| !s.is_empty())
75 .collect(),
76 _ => vec!["bindcar".to_string()],
77 };
78
79 let allowed_namespaces = env::var("BIND_ALLOWED_NAMESPACES")
80 .unwrap_or_default()
81 .split(',')
82 .map(|s| s.trim().to_string())
83 .filter(|s| !s.is_empty())
84 .collect();
85
86 let allowed_service_accounts = env::var("BIND_ALLOWED_SERVICE_ACCOUNTS")
87 .unwrap_or_default()
88 .split(',')
89 .map(|s| s.trim().to_string())
90 .filter(|s| !s.is_empty())
91 .collect();
92
93 let config = Self {
94 audiences,
95 allowed_namespaces,
96 allowed_service_accounts,
97 };
98
99 debug!("TokenReview config loaded: audiences={:?}, allowed_namespaces={:?}, allowed_service_accounts={:?}",
100 config.audiences, config.allowed_namespaces, config.allowed_service_accounts);
101
102 config
103 }
104
105 pub(crate) fn is_namespace_allowed(&self, namespace: &str) -> bool {
107 if self.allowed_namespaces.is_empty() {
109 return true;
110 }
111 self.allowed_namespaces.contains(&namespace.to_string())
112 }
113
114 pub(crate) fn is_service_account_allowed(&self, username: &str) -> bool {
116 if self.allowed_service_accounts.is_empty() {
118 return true;
119 }
120 self.allowed_service_accounts
121 .contains(&username.to_string())
122 }
123
124 pub(crate) fn extract_namespace(username: &str) -> Option<String> {
127 let parts: Vec<&str> = username.split(':').collect();
128 if parts.len() != 4 || parts[0] != "system" || parts[1] != "serviceaccount" {
129 return None;
130 }
131 Some(parts[2].to_string())
132 }
133}
134
135pub async fn authenticate(
149 headers: HeaderMap,
150 request: Request,
151 next: Next,
152) -> Result<Response, (StatusCode, Json<AuthError>)> {
153 let auth_header = headers
155 .get("authorization")
156 .and_then(|h| h.to_str().ok())
157 .ok_or_else(|| {
158 warn!("Missing Authorization header");
159 (
160 StatusCode::UNAUTHORIZED,
161 Json(AuthError {
162 error: "Missing Authorization header".to_string(),
163 }),
164 )
165 })?;
166
167 if !auth_header.starts_with("Bearer ") {
169 warn!("Invalid Authorization header format");
170 return Err((
171 StatusCode::UNAUTHORIZED,
172 Json(AuthError {
173 error: "Invalid Authorization header format. Expected: Bearer <token>".to_string(),
174 }),
175 ));
176 }
177
178 let token = &auth_header[7..]; if token.is_empty() {
182 warn!("Empty token in Authorization header");
183 return Err((
184 StatusCode::UNAUTHORIZED,
185 Json(AuthError {
186 error: "Empty token".to_string(),
187 }),
188 ));
189 }
190
191 #[cfg(feature = "k8s-token-review")]
193 if let Err(e) = validate_token_with_k8s(token).await {
194 warn!("Token validation failed: {}", e);
195 return Err((
196 StatusCode::UNAUTHORIZED,
197 Json(AuthError {
198 error: format!("Token validation failed: {}", e),
199 }),
200 ));
201 }
202
203 #[cfg(feature = "k8s-token-review")]
204 debug!("Token validated with Kubernetes TokenReview API");
205
206 #[cfg(not(feature = "k8s-token-review"))]
207 debug!("Token validation: basic mode (presence check only)");
208
209 Ok(next.run(request).await)
210}
211
212#[cfg(feature = "k8s-token-review")]
230pub(crate) async fn validate_token_with_k8s(token: &str) -> Result<(), String> {
231 let config = TokenReviewConfig::from_env();
233
234 let client = Client::try_default()
236 .await
237 .map_err(|e| format!("Failed to create Kubernetes client: {}", e))?;
238
239 let token_reviews: Api<TokenReview> = Api::all(client);
241
242 let audiences = if !config.audiences.is_empty() {
244 Some(config.audiences.clone())
245 } else {
246 None
247 };
248
249 let token_review = TokenReview {
250 metadata: Default::default(),
251 spec: k8s_openapi::api::authentication::v1::TokenReviewSpec {
252 token: Some(token.to_string()),
253 audiences,
254 },
255 status: None,
256 };
257
258 let result = token_reviews
260 .create(&Default::default(), &token_review)
261 .await
262 .map_err(|e| {
263 error!("TokenReview API call failed: {}", e);
264 format!("Failed to validate token with Kubernetes API: {}", e)
265 })?;
266
267 let status = result
269 .status
270 .ok_or_else(|| "TokenReview status not available".to_string())?;
271
272 if status.authenticated != Some(true) {
273 let error_msg = status
274 .error
275 .unwrap_or_else(|| "Token not authenticated".to_string());
276 warn!("Token authentication failed: {}", error_msg);
277 return Err(error_msg);
278 }
279
280 debug!("Token authenticated successfully");
281
282 let user = status.user.ok_or_else(|| {
284 warn!("TokenReview succeeded but no user information returned");
285 "No user information in TokenReview response".to_string()
286 })?;
287
288 let username = user.username.as_deref().unwrap_or("");
289 debug!("Authenticated user: {}", username);
290
291 if let Some(namespace) = TokenReviewConfig::extract_namespace(username) {
293 if !config.is_namespace_allowed(&namespace) {
294 warn!(
295 "ServiceAccount from unauthorized namespace: {} (from {})",
296 namespace, username
297 );
298 return Err(format!(
299 "ServiceAccount from unauthorized namespace: {}",
300 namespace
301 ));
302 }
303 debug!("Namespace {} is allowed", namespace);
304 }
305
306 if !config.is_service_account_allowed(username) {
308 warn!("ServiceAccount not in allowlist: {}", username);
309 return Err(format!("ServiceAccount not authorized: {}", username));
310 }
311
312 debug!("ServiceAccount {} is allowed", username);
313 Ok(())
314}