Skip to main content

allsource_core/infrastructure/security/
middleware.rs

1use crate::{
2    error::AllSourceError,
3    infrastructure::security::{
4        auth::{AuthManager, Claims, Permission, Role},
5        rate_limit::RateLimiter,
6    },
7};
8use axum::{
9    extract::{Request, State},
10    http::{HeaderMap, StatusCode},
11    middleware::Next,
12    response::{IntoResponse, Response},
13};
14use std::sync::{Arc, LazyLock};
15
16/// Paths that bypass authentication (exact match)
17pub const AUTH_SKIP_PATHS: &[&str] = &[
18    "/health",
19    "/metrics",
20    "/api/v1/auth/register",
21    "/api/v1/auth/login",
22];
23
24/// Path prefixes that bypass authentication and rate limiting.
25///
26/// Internal endpoints are used by the sentinel process for automated failover
27/// (promote, repoint). They must not require API keys or be rate-limited,
28/// otherwise failover can timeout or fail when credentials are unavailable.
29pub const AUTH_SKIP_PREFIXES: &[&str] = &["/internal/"];
30
31/// Check if a path should skip authentication and rate limiting.
32#[inline]
33pub fn should_skip_auth(path: &str) -> bool {
34    AUTH_SKIP_PATHS.contains(&path) || AUTH_SKIP_PREFIXES.iter().any(|pfx| path.starts_with(pfx))
35}
36
37/// Check if development mode is enabled via environment variable.
38/// When enabled, authentication and rate limiting are bypassed for local development.
39///
40/// Set `ALLSOURCE_DEV_MODE=true` or `ALLSOURCE_DEV_MODE=1` to enable.
41///
42/// **WARNING**: Never enable this in production environments!
43static DEV_MODE_ENABLED: LazyLock<bool> = LazyLock::new(|| {
44    let enabled = std::env::var("ALLSOURCE_DEV_MODE")
45        .map(|v| v == "true" || v == "1")
46        .unwrap_or(false);
47    if enabled {
48        tracing::warn!(
49            "⚠️  ALLSOURCE_DEV_MODE is enabled - authentication and rate limiting are DISABLED"
50        );
51        tracing::warn!("⚠️  This should NEVER be used in production!");
52    }
53    enabled
54});
55
56/// Check if dev mode is enabled
57#[inline]
58pub fn is_dev_mode() -> bool {
59    *DEV_MODE_ENABLED
60}
61
62/// Create a development-mode AuthContext with admin privileges
63fn dev_mode_auth_context() -> AuthContext {
64    AuthContext {
65        claims: Claims::new(
66            "dev-user".to_string(),
67            "dev-tenant".to_string(),
68            Role::Admin,
69            chrono::Duration::hours(24),
70        ),
71    }
72}
73
74/// Authentication state shared across requests
75#[derive(Clone)]
76pub struct AuthState {
77    pub auth_manager: Arc<AuthManager>,
78}
79
80/// Rate limiting state
81#[derive(Clone)]
82pub struct RateLimitState {
83    pub rate_limiter: Arc<RateLimiter>,
84}
85
86/// Authenticated request context
87#[derive(Debug, Clone)]
88pub struct AuthContext {
89    pub claims: Claims,
90}
91
92impl AuthContext {
93    /// Check if user has required permission
94    pub fn require_permission(&self, permission: Permission) -> Result<(), AllSourceError> {
95        if self.claims.has_permission(permission) {
96            Ok(())
97        } else {
98            Err(AllSourceError::ValidationError(
99                "Insufficient permissions".to_string(),
100            ))
101        }
102    }
103
104    /// Get tenant ID from context
105    pub fn tenant_id(&self) -> &str {
106        &self.claims.tenant_id
107    }
108
109    /// Get user ID from context
110    pub fn user_id(&self) -> &str {
111        &self.claims.sub
112    }
113}
114
115/// Extract token from Authorization header
116fn extract_token(headers: &HeaderMap) -> Result<String, AllSourceError> {
117    let auth_header = headers
118        .get("authorization")
119        .ok_or_else(|| AllSourceError::ValidationError("Missing authorization header".to_string()))?
120        .to_str()
121        .map_err(|_| AllSourceError::ValidationError("Invalid authorization header".to_string()))?;
122
123    // Support both "Bearer <token>" and "<token>" formats
124    let token = if auth_header.starts_with("Bearer ") {
125        auth_header.trim_start_matches("Bearer ").trim()
126    } else if auth_header.starts_with("bearer ") {
127        auth_header.trim_start_matches("bearer ").trim()
128    } else {
129        auth_header.trim()
130    };
131
132    if token.is_empty() {
133        return Err(AllSourceError::ValidationError(
134            "Empty authorization token".to_string(),
135        ));
136    }
137
138    Ok(token.to_string())
139}
140
141/// Authentication middleware
142pub async fn auth_middleware(
143    State(auth_state): State<AuthState>,
144    mut request: Request,
145    next: Next,
146) -> Result<Response, AuthError> {
147    // Skip authentication for public and internal paths
148    let path = request.uri().path();
149    if should_skip_auth(path) {
150        return Ok(next.run(request).await);
151    }
152
153    // Dev mode: bypass authentication entirely
154    if is_dev_mode() {
155        request.extensions_mut().insert(dev_mode_auth_context());
156        return Ok(next.run(request).await);
157    }
158
159    let headers = request.headers();
160
161    // Extract and validate token (JWT or API key)
162    let token = extract_token(headers)?;
163
164    let claims = if token.starts_with("ask_") {
165        // API Key authentication
166        auth_state.auth_manager.validate_api_key(&token)?
167    } else {
168        // JWT authentication
169        auth_state.auth_manager.validate_token(&token)?
170    };
171
172    // Insert auth context into request extensions
173    request.extensions_mut().insert(AuthContext { claims });
174
175    Ok(next.run(request).await)
176}
177
178/// Optional authentication middleware (allows unauthenticated requests)
179pub async fn optional_auth_middleware(
180    State(auth_state): State<AuthState>,
181    mut request: Request,
182    next: Next,
183) -> Response {
184    let headers = request.headers();
185
186    if let Ok(token) = extract_token(headers) {
187        // Try to authenticate, but don't fail if invalid
188        let claims = if token.starts_with("ask_") {
189            auth_state.auth_manager.validate_api_key(&token).ok()
190        } else {
191            auth_state.auth_manager.validate_token(&token).ok()
192        };
193
194        if let Some(claims) = claims {
195            request.extensions_mut().insert(AuthContext { claims });
196        }
197    }
198
199    next.run(request).await
200}
201
202/// Error type for authentication failures
203#[derive(Debug)]
204pub struct AuthError(AllSourceError);
205
206impl From<AllSourceError> for AuthError {
207    fn from(err: AllSourceError) -> Self {
208        AuthError(err)
209    }
210}
211
212impl IntoResponse for AuthError {
213    fn into_response(self) -> Response {
214        let (status, message) = match self.0 {
215            AllSourceError::ValidationError(msg) => (StatusCode::UNAUTHORIZED, msg),
216            _ => (
217                StatusCode::INTERNAL_SERVER_ERROR,
218                "Internal server error".to_string(),
219            ),
220        };
221
222        (status, message).into_response()
223    }
224}
225
226/// Axum extractor for authenticated requests
227pub struct Authenticated(pub AuthContext);
228
229impl<S> axum::extract::FromRequestParts<S> for Authenticated
230where
231    S: Send + Sync,
232{
233    type Rejection = (StatusCode, &'static str);
234
235    async fn from_request_parts(
236        parts: &mut axum::http::request::Parts,
237        _state: &S,
238    ) -> Result<Self, Self::Rejection> {
239        parts
240            .extensions
241            .get::<AuthContext>()
242            .cloned()
243            .map(Authenticated)
244            .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))
245    }
246}
247
248/// Axum extractor for optional authentication (never rejects, returns Option)
249/// Use this for routes that work with or without authentication
250pub struct OptionalAuth(pub Option<AuthContext>);
251
252impl<S> axum::extract::FromRequestParts<S> for OptionalAuth
253where
254    S: Send + Sync,
255{
256    type Rejection = std::convert::Infallible;
257
258    async fn from_request_parts(
259        parts: &mut axum::http::request::Parts,
260        _state: &S,
261    ) -> Result<Self, Self::Rejection> {
262        Ok(OptionalAuth(parts.extensions.get::<AuthContext>().cloned()))
263    }
264}
265
266/// Axum extractor for admin-only requests
267pub struct Admin(pub AuthContext);
268
269impl<S> axum::extract::FromRequestParts<S> for Admin
270where
271    S: Send + Sync,
272{
273    type Rejection = (StatusCode, &'static str);
274
275    async fn from_request_parts(
276        parts: &mut axum::http::request::Parts,
277        _state: &S,
278    ) -> Result<Self, Self::Rejection> {
279        let auth_ctx = parts
280            .extensions
281            .get::<AuthContext>()
282            .cloned()
283            .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))?;
284
285        auth_ctx
286            .require_permission(Permission::Admin)
287            .map_err(|_| (StatusCode::FORBIDDEN, "Admin permission required"))?;
288
289        Ok(Admin(auth_ctx))
290    }
291}
292
293/// Rate limiting middleware
294/// Checks rate limits based on tenant_id from auth context
295pub async fn rate_limit_middleware(
296    State(rate_limit_state): State<RateLimitState>,
297    request: Request,
298    next: Next,
299) -> Result<Response, RateLimitError> {
300    // Skip rate limiting for public and internal paths
301    let path = request.uri().path();
302    if should_skip_auth(path) {
303        return Ok(next.run(request).await);
304    }
305
306    // Dev mode: bypass rate limiting entirely
307    if is_dev_mode() {
308        return Ok(next.run(request).await);
309    }
310
311    // Extract auth context from request
312    let auth_ctx = request
313        .extensions()
314        .get::<AuthContext>()
315        .ok_or(RateLimitError::Unauthorized)?;
316
317    // Check rate limit for this tenant
318    let result = rate_limit_state
319        .rate_limiter
320        .check_rate_limit(auth_ctx.tenant_id());
321
322    if !result.allowed {
323        return Err(RateLimitError::RateLimitExceeded {
324            retry_after: result.retry_after.unwrap_or_default().as_secs(),
325            limit: result.limit,
326        });
327    }
328
329    // Add rate limit headers to response
330    let mut response = next.run(request).await;
331    let headers = response.headers_mut();
332    headers.insert(
333        "X-RateLimit-Limit",
334        result.limit.to_string().parse().unwrap(),
335    );
336    headers.insert(
337        "X-RateLimit-Remaining",
338        result.remaining.to_string().parse().unwrap(),
339    );
340
341    Ok(response)
342}
343
344/// Error type for rate limiting failures
345#[derive(Debug)]
346pub enum RateLimitError {
347    RateLimitExceeded { retry_after: u64, limit: u32 },
348    Unauthorized,
349}
350
351impl IntoResponse for RateLimitError {
352    fn into_response(self) -> Response {
353        match self {
354            RateLimitError::RateLimitExceeded { retry_after, limit } => {
355                let mut response = (
356                    StatusCode::TOO_MANY_REQUESTS,
357                    format!("Rate limit exceeded. Limit: {limit} requests/min"),
358                )
359                    .into_response();
360
361                if retry_after > 0 {
362                    response
363                        .headers_mut()
364                        .insert("Retry-After", retry_after.to_string().parse().unwrap());
365                }
366
367                response
368            }
369            RateLimitError::Unauthorized => (
370                StatusCode::UNAUTHORIZED,
371                "Authentication required for rate limiting",
372            )
373                .into_response(),
374        }
375    }
376}
377
378/// Helper macro to require specific permission
379#[macro_export]
380macro_rules! require_permission {
381    ($auth:expr, $perm:expr) => {
382        $auth.0.require_permission($perm).map_err(|_| {
383            (
384                axum::http::StatusCode::FORBIDDEN,
385                "Insufficient permissions",
386            )
387        })?
388    };
389}
390
391// ============================================================================
392// Tenant Isolation Middleware (Phase 5B)
393// ============================================================================
394
395use crate::domain::{entities::Tenant, repositories::TenantRepository, value_objects::TenantId};
396
397/// Tenant isolation state for middleware
398#[derive(Clone)]
399pub struct TenantState<R: TenantRepository> {
400    pub tenant_repository: Arc<R>,
401}
402
403/// Validated tenant context injected into requests
404///
405/// This context is created by the tenant_isolation_middleware after
406/// validating that the tenant exists and is active.
407#[derive(Debug, Clone)]
408pub struct TenantContext {
409    pub tenant: Tenant,
410}
411
412impl TenantContext {
413    /// Get the tenant ID
414    pub fn tenant_id(&self) -> &TenantId {
415        self.tenant.id()
416    }
417
418    /// Check if tenant is active
419    pub fn is_active(&self) -> bool {
420        self.tenant.is_active()
421    }
422}
423
424/// Tenant isolation middleware
425///
426/// Validates that the authenticated tenant exists and is active.
427/// Injects TenantContext into the request for use by handlers.
428///
429/// # Phase 5B: Tenant Isolation
430/// This middleware enforces tenant boundaries by:
431/// 1. Extracting tenant_id from AuthContext
432/// 2. Loading tenant from repository
433/// 3. Validating tenant is active
434/// 4. Injecting TenantContext into request extensions
435///
436/// Must be applied after auth_middleware.
437pub async fn tenant_isolation_middleware<R: TenantRepository + 'static>(
438    State(tenant_state): State<TenantState<R>>,
439    mut request: Request,
440    next: Next,
441) -> Result<Response, TenantError> {
442    // Extract auth context (must be authenticated)
443    let auth_ctx = request
444        .extensions()
445        .get::<AuthContext>()
446        .ok_or(TenantError::Unauthorized)?
447        .clone();
448
449    // Parse tenant ID
450    let tenant_id =
451        TenantId::new(auth_ctx.tenant_id().to_string()).map_err(|_| TenantError::InvalidTenant)?;
452
453    // Load tenant from repository
454    let tenant = tenant_state
455        .tenant_repository
456        .find_by_id(&tenant_id)
457        .await
458        .map_err(|e| TenantError::RepositoryError(e.to_string()))?
459        .ok_or(TenantError::TenantNotFound)?;
460
461    // Validate tenant is active
462    if !tenant.is_active() {
463        return Err(TenantError::TenantInactive);
464    }
465
466    // Inject tenant context into request
467    request.extensions_mut().insert(TenantContext { tenant });
468
469    // Continue to next middleware/handler
470    Ok(next.run(request).await)
471}
472
473/// Error type for tenant isolation failures
474#[derive(Debug)]
475pub enum TenantError {
476    Unauthorized,
477    InvalidTenant,
478    TenantNotFound,
479    TenantInactive,
480    RepositoryError(String),
481}
482
483impl IntoResponse for TenantError {
484    fn into_response(self) -> Response {
485        let (status, message) = match self {
486            TenantError::Unauthorized => (
487                StatusCode::UNAUTHORIZED,
488                "Authentication required for tenant access",
489            ),
490            TenantError::InvalidTenant => (StatusCode::BAD_REQUEST, "Invalid tenant identifier"),
491            TenantError::TenantNotFound => (StatusCode::NOT_FOUND, "Tenant not found"),
492            TenantError::TenantInactive => (StatusCode::FORBIDDEN, "Tenant is inactive"),
493            TenantError::RepositoryError(_) => (
494                StatusCode::INTERNAL_SERVER_ERROR,
495                "Failed to validate tenant",
496            ),
497        };
498
499        (status, message).into_response()
500    }
501}
502
503// ============================================================================
504// Request ID Middleware (Phase 5C)
505// ============================================================================
506
507use uuid::Uuid;
508
509/// Request context with unique ID for tracing
510#[derive(Debug, Clone)]
511pub struct RequestId(pub String);
512
513impl Default for RequestId {
514    fn default() -> Self {
515        Self::new()
516    }
517}
518
519impl RequestId {
520    /// Generate a new request ID
521    pub fn new() -> Self {
522        Self(Uuid::new_v4().to_string())
523    }
524
525    /// Get the request ID as a string
526    pub fn as_str(&self) -> &str {
527        &self.0
528    }
529}
530
531/// Request ID middleware
532///
533/// Generates a unique request ID for each request and injects it into:
534/// - Request extensions (for use in handlers/logging)
535/// - Response headers (X-Request-ID)
536///
537/// If the request already has an X-Request-ID header, it will be used instead.
538///
539/// # Phase 5C: Request Tracing
540/// This middleware enables distributed tracing by:
541/// 1. Generating unique IDs for each request
542/// 2. Propagating IDs through the request lifecycle
543/// 3. Returning IDs in response headers
544/// 4. Supporting client-provided request IDs
545pub async fn request_id_middleware(mut request: Request, next: Next) -> Response {
546    // Check if request already has a request ID
547    let request_id = request
548        .headers()
549        .get("x-request-id")
550        .and_then(|v| v.to_str().ok())
551        .map(|s| RequestId(s.to_string()))
552        .unwrap_or_else(RequestId::new);
553
554    // Store request ID in extensions
555    request.extensions_mut().insert(request_id.clone());
556
557    // Process request
558    let mut response = next.run(request).await;
559
560    // Add request ID to response headers
561    response
562        .headers_mut()
563        .insert("x-request-id", request_id.0.parse().unwrap());
564
565    response
566}
567
568// ============================================================================
569// Security Headers Middleware (Phase 5C)
570// ============================================================================
571
572/// Security headers configuration
573#[derive(Debug, Clone)]
574pub struct SecurityConfig {
575    /// Enable HSTS (HTTP Strict Transport Security)
576    pub enable_hsts: bool,
577    /// HSTS max age in seconds
578    pub hsts_max_age: u32,
579    /// Enable X-Frame-Options
580    pub enable_frame_options: bool,
581    /// X-Frame-Options value
582    pub frame_options: FrameOptions,
583    /// Enable X-Content-Type-Options
584    pub enable_content_type_options: bool,
585    /// Enable X-XSS-Protection
586    pub enable_xss_protection: bool,
587    /// Content Security Policy
588    pub csp: Option<String>,
589    /// CORS allowed origins
590    pub cors_origins: Vec<String>,
591    /// CORS allowed methods
592    pub cors_methods: Vec<String>,
593    /// CORS allowed headers
594    pub cors_headers: Vec<String>,
595    /// CORS max age
596    pub cors_max_age: u32,
597}
598
599#[derive(Debug, Clone)]
600pub enum FrameOptions {
601    Deny,
602    SameOrigin,
603    AllowFrom(String),
604}
605
606impl Default for SecurityConfig {
607    fn default() -> Self {
608        Self {
609            enable_hsts: true,
610            hsts_max_age: 31536000, // 1 year
611            enable_frame_options: true,
612            frame_options: FrameOptions::Deny,
613            enable_content_type_options: true,
614            enable_xss_protection: true,
615            csp: Some("default-src 'self'".to_string()),
616            cors_origins: vec!["*".to_string()],
617            cors_methods: vec![
618                "GET".to_string(),
619                "POST".to_string(),
620                "PUT".to_string(),
621                "DELETE".to_string(),
622            ],
623            cors_headers: vec!["Content-Type".to_string(), "Authorization".to_string()],
624            cors_max_age: 3600,
625        }
626    }
627}
628
629#[derive(Clone)]
630pub struct SecurityState {
631    pub config: SecurityConfig,
632}
633
634/// Security headers middleware
635///
636/// Adds security-related HTTP headers to all responses:
637/// - HSTS (Strict-Transport-Security)
638/// - X-Frame-Options
639/// - X-Content-Type-Options
640/// - X-XSS-Protection
641/// - Content-Security-Policy
642/// - CORS headers
643///
644/// # Phase 5C: Security Hardening
645/// This middleware provides defense-in-depth by:
646/// 1. Preventing clickjacking (X-Frame-Options)
647/// 2. Preventing MIME sniffing (X-Content-Type-Options)
648/// 3. Enforcing HTTPS (HSTS)
649/// 4. Preventing XSS (CSP, X-XSS-Protection)
650/// 5. Enabling CORS for controlled access
651pub async fn security_headers_middleware(
652    State(security_state): State<SecurityState>,
653    request: Request,
654    next: Next,
655) -> Response {
656    let mut response = next.run(request).await;
657    let headers = response.headers_mut();
658    let config = &security_state.config;
659
660    // HSTS
661    if config.enable_hsts {
662        headers.insert(
663            "strict-transport-security",
664            format!("max-age={}", config.hsts_max_age).parse().unwrap(),
665        );
666    }
667
668    // X-Frame-Options
669    if config.enable_frame_options {
670        let value = match &config.frame_options {
671            FrameOptions::Deny => "DENY",
672            FrameOptions::SameOrigin => "SAMEORIGIN",
673            FrameOptions::AllowFrom(origin) => origin,
674        };
675        headers.insert("x-frame-options", value.parse().unwrap());
676    }
677
678    // X-Content-Type-Options
679    if config.enable_content_type_options {
680        headers.insert("x-content-type-options", "nosniff".parse().unwrap());
681    }
682
683    // X-XSS-Protection
684    if config.enable_xss_protection {
685        headers.insert("x-xss-protection", "1; mode=block".parse().unwrap());
686    }
687
688    // Content-Security-Policy
689    if let Some(csp) = &config.csp {
690        headers.insert("content-security-policy", csp.parse().unwrap());
691    }
692
693    // CORS headers
694    headers.insert(
695        "access-control-allow-origin",
696        config.cors_origins.join(", ").parse().unwrap(),
697    );
698    headers.insert(
699        "access-control-allow-methods",
700        config.cors_methods.join(", ").parse().unwrap(),
701    );
702    headers.insert(
703        "access-control-allow-headers",
704        config.cors_headers.join(", ").parse().unwrap(),
705    );
706    headers.insert(
707        "access-control-max-age",
708        config.cors_max_age.to_string().parse().unwrap(),
709    );
710
711    response
712}
713
714// ============================================================================
715// IP Filtering Middleware (Phase 5C)
716// ============================================================================
717
718use crate::infrastructure::security::IpFilter;
719use std::net::SocketAddr;
720
721#[derive(Clone)]
722pub struct IpFilterState {
723    pub ip_filter: Arc<IpFilter>,
724}
725
726/// IP filtering middleware
727///
728/// Blocks or allows requests based on IP address rules.
729/// Supports both global and per-tenant IP filtering.
730///
731/// # Phase 5C: Access Control
732/// This middleware provides IP-based access control by:
733/// 1. Extracting client IP from request
734/// 2. Checking against global and tenant-specific rules
735/// 3. Blocking requests from unauthorized IPs
736/// 4. Supporting both allowlists and blocklists
737pub async fn ip_filter_middleware(
738    State(ip_filter_state): State<IpFilterState>,
739    request: Request,
740    next: Next,
741) -> Result<Response, IpFilterError> {
742    // Extract client IP address
743    let client_ip = request
744        .extensions()
745        .get::<axum::extract::ConnectInfo<SocketAddr>>()
746        .map(|connect_info| connect_info.0.ip())
747        .ok_or(IpFilterError::NoIpAddress)?;
748
749    // Check if this is a tenant-scoped request
750    let result = if let Some(tenant_ctx) = request.extensions().get::<TenantContext>() {
751        // Tenant-specific filtering
752        ip_filter_state
753            .ip_filter
754            .is_allowed_for_tenant(tenant_ctx.tenant_id(), &client_ip)
755    } else {
756        // Global filtering only
757        ip_filter_state.ip_filter.is_allowed(&client_ip)
758    };
759
760    // Block if not allowed
761    if !result.allowed {
762        return Err(IpFilterError::Blocked {
763            reason: result.reason,
764        });
765    }
766
767    // Allow request to proceed
768    Ok(next.run(request).await)
769}
770
771/// Error type for IP filtering failures
772#[derive(Debug)]
773pub enum IpFilterError {
774    NoIpAddress,
775    Blocked { reason: String },
776}
777
778impl IntoResponse for IpFilterError {
779    fn into_response(self) -> Response {
780        match self {
781            IpFilterError::NoIpAddress => (
782                StatusCode::BAD_REQUEST,
783                "Unable to determine client IP address",
784            )
785                .into_response(),
786            IpFilterError::Blocked { reason } => {
787                (StatusCode::FORBIDDEN, format!("Access denied: {reason}")).into_response()
788            }
789        }
790    }
791}
792
793#[cfg(test)]
794mod tests {
795    use super::*;
796    use crate::infrastructure::security::auth::Role;
797
798    #[test]
799    fn test_extract_bearer_token() {
800        let mut headers = HeaderMap::new();
801        headers.insert("authorization", "Bearer test_token_123".parse().unwrap());
802
803        let token = extract_token(&headers).unwrap();
804        assert_eq!(token, "test_token_123");
805    }
806
807    #[test]
808    fn test_extract_lowercase_bearer() {
809        let mut headers = HeaderMap::new();
810        headers.insert("authorization", "bearer test_token_123".parse().unwrap());
811
812        let token = extract_token(&headers).unwrap();
813        assert_eq!(token, "test_token_123");
814    }
815
816    #[test]
817    fn test_extract_plain_token() {
818        let mut headers = HeaderMap::new();
819        headers.insert("authorization", "test_token_123".parse().unwrap());
820
821        let token = extract_token(&headers).unwrap();
822        assert_eq!(token, "test_token_123");
823    }
824
825    #[test]
826    fn test_missing_auth_header() {
827        let headers = HeaderMap::new();
828        assert!(extract_token(&headers).is_err());
829    }
830
831    #[test]
832    fn test_empty_auth_header() {
833        let mut headers = HeaderMap::new();
834        headers.insert("authorization", "".parse().unwrap());
835        assert!(extract_token(&headers).is_err());
836    }
837
838    #[test]
839    fn test_bearer_with_empty_token() {
840        let mut headers = HeaderMap::new();
841        headers.insert("authorization", "Bearer ".parse().unwrap());
842        assert!(extract_token(&headers).is_err());
843    }
844
845    #[test]
846    fn test_auth_context_permissions() {
847        let claims = Claims::new(
848            "user1".to_string(),
849            "tenant1".to_string(),
850            Role::Developer,
851            chrono::Duration::hours(1),
852        );
853
854        let ctx = AuthContext { claims };
855
856        assert!(ctx.require_permission(Permission::Read).is_ok());
857        assert!(ctx.require_permission(Permission::Write).is_ok());
858        assert!(ctx.require_permission(Permission::Admin).is_err());
859    }
860
861    #[test]
862    fn test_auth_context_admin_permissions() {
863        let claims = Claims::new(
864            "admin1".to_string(),
865            "tenant1".to_string(),
866            Role::Admin,
867            chrono::Duration::hours(1),
868        );
869
870        let ctx = AuthContext { claims };
871
872        assert!(ctx.require_permission(Permission::Read).is_ok());
873        assert!(ctx.require_permission(Permission::Write).is_ok());
874        assert!(ctx.require_permission(Permission::Admin).is_ok());
875    }
876
877    #[test]
878    fn test_auth_context_readonly_permissions() {
879        let claims = Claims::new(
880            "readonly1".to_string(),
881            "tenant1".to_string(),
882            Role::ReadOnly,
883            chrono::Duration::hours(1),
884        );
885
886        let ctx = AuthContext { claims };
887
888        assert!(ctx.require_permission(Permission::Read).is_ok());
889        assert!(ctx.require_permission(Permission::Write).is_err());
890        assert!(ctx.require_permission(Permission::Admin).is_err());
891    }
892
893    #[test]
894    fn test_auth_context_tenant_id() {
895        let claims = Claims::new(
896            "user1".to_string(),
897            "my-tenant".to_string(),
898            Role::Developer,
899            chrono::Duration::hours(1),
900        );
901
902        let ctx = AuthContext { claims };
903        assert_eq!(ctx.tenant_id(), "my-tenant");
904    }
905
906    #[test]
907    fn test_auth_context_user_id() {
908        let claims = Claims::new(
909            "my-user".to_string(),
910            "tenant1".to_string(),
911            Role::Developer,
912            chrono::Duration::hours(1),
913        );
914
915        let ctx = AuthContext { claims };
916        assert_eq!(ctx.user_id(), "my-user");
917    }
918
919    #[test]
920    fn test_request_id_new() {
921        let id1 = RequestId::new();
922        let id2 = RequestId::new();
923
924        // IDs should be unique
925        assert_ne!(id1.as_str(), id2.as_str());
926        // IDs should be valid UUIDs (36 chars with hyphens)
927        assert_eq!(id1.as_str().len(), 36);
928    }
929
930    #[test]
931    fn test_request_id_default() {
932        let id = RequestId::default();
933        assert_eq!(id.as_str().len(), 36);
934    }
935
936    #[test]
937    fn test_security_config_default() {
938        let config = SecurityConfig::default();
939
940        assert!(config.enable_hsts);
941        assert_eq!(config.hsts_max_age, 31536000);
942        assert!(config.enable_frame_options);
943        assert!(config.enable_content_type_options);
944        assert!(config.enable_xss_protection);
945        assert!(config.csp.is_some());
946    }
947
948    #[test]
949    fn test_frame_options_variants() {
950        let deny = FrameOptions::Deny;
951        let same_origin = FrameOptions::SameOrigin;
952        let allow_from = FrameOptions::AllowFrom("https://example.com".to_string());
953
954        // Check that variants are distinct via debug formatting
955        assert!(format!("{:?}", deny).contains("Deny"));
956        assert!(format!("{:?}", same_origin).contains("SameOrigin"));
957        assert!(format!("{:?}", allow_from).contains("AllowFrom"));
958    }
959
960    #[test]
961    fn test_auth_error_from_validation_error() {
962        let error = AllSourceError::ValidationError("test error".to_string());
963        let auth_error = AuthError::from(error);
964        assert!(format!("{:?}", auth_error).contains("ValidationError"));
965    }
966
967    #[test]
968    fn test_rate_limit_error_display() {
969        let error = RateLimitError::RateLimitExceeded {
970            retry_after: 60,
971            limit: 100,
972        };
973        assert!(format!("{:?}", error).contains("RateLimitExceeded"));
974
975        let unauth_error = RateLimitError::Unauthorized;
976        assert!(format!("{:?}", unauth_error).contains("Unauthorized"));
977    }
978
979    #[test]
980    fn test_tenant_error_variants() {
981        let errors = vec![
982            TenantError::Unauthorized,
983            TenantError::InvalidTenant,
984            TenantError::TenantNotFound,
985            TenantError::TenantInactive,
986            TenantError::RepositoryError("test".to_string()),
987        ];
988
989        for error in errors {
990            // Ensure each variant can be debug-formatted
991            let _ = format!("{:?}", error);
992        }
993    }
994
995    #[test]
996    fn test_ip_filter_error_variants() {
997        let errors = vec![
998            IpFilterError::NoIpAddress,
999            IpFilterError::Blocked {
1000                reason: "blocked".to_string(),
1001            },
1002        ];
1003
1004        for error in errors {
1005            let _ = format!("{:?}", error);
1006        }
1007    }
1008
1009    #[test]
1010    fn test_security_state_clone() {
1011        let config = SecurityConfig::default();
1012        let state = SecurityState {
1013            config: config.clone(),
1014        };
1015        let cloned = state.clone();
1016        assert_eq!(cloned.config.hsts_max_age, config.hsts_max_age);
1017    }
1018
1019    #[test]
1020    fn test_auth_state_clone() {
1021        let auth_manager = Arc::new(AuthManager::new("test-secret"));
1022        let state = AuthState { auth_manager };
1023        let cloned = state.clone();
1024        assert!(Arc::ptr_eq(&state.auth_manager, &cloned.auth_manager));
1025    }
1026
1027    #[test]
1028    fn test_rate_limit_state_clone() {
1029        use crate::infrastructure::security::rate_limit::RateLimitConfig;
1030        let rate_limiter = Arc::new(RateLimiter::new(RateLimitConfig::free_tier()));
1031        let state = RateLimitState { rate_limiter };
1032        let cloned = state.clone();
1033        assert!(Arc::ptr_eq(&state.rate_limiter, &cloned.rate_limiter));
1034    }
1035
1036    #[test]
1037    fn test_auth_skip_paths_contains_expected() {
1038        // Verify public paths are configured for auth/rate-limit skipping
1039        assert!(should_skip_auth("/health"));
1040        assert!(should_skip_auth("/metrics"));
1041        assert!(should_skip_auth("/api/v1/auth/register"));
1042        assert!(should_skip_auth("/api/v1/auth/login"));
1043
1044        // Verify internal endpoints bypass auth (sentinel failover)
1045        assert!(should_skip_auth("/internal/promote"));
1046        assert!(should_skip_auth("/internal/repoint"));
1047        assert!(should_skip_auth("/internal/anything"));
1048
1049        // Verify protected paths are NOT in skip list
1050        assert!(!should_skip_auth("/api/v1/events"));
1051        assert!(!should_skip_auth("/api/v1/auth/me"));
1052        assert!(!should_skip_auth("/api/v1/tenants"));
1053    }
1054
1055    #[test]
1056    fn test_dev_mode_auth_context() {
1057        let ctx = dev_mode_auth_context();
1058
1059        // Dev user should have admin privileges
1060        assert_eq!(ctx.tenant_id(), "dev-tenant");
1061        assert_eq!(ctx.user_id(), "dev-user");
1062        assert!(ctx.require_permission(Permission::Admin).is_ok());
1063        assert!(ctx.require_permission(Permission::Read).is_ok());
1064        assert!(ctx.require_permission(Permission::Write).is_ok());
1065    }
1066
1067    #[test]
1068    fn test_dev_mode_disabled_by_default() {
1069        // Dev mode should be disabled by default (env var not set in tests)
1070        // Note: This test may fail if ALLSOURCE_DEV_MODE is set in the test environment
1071        // In a clean environment, dev mode is disabled
1072        let env_value = std::env::var("ALLSOURCE_DEV_MODE").unwrap_or_default();
1073        if env_value.is_empty() {
1074            assert!(!is_dev_mode());
1075        }
1076    }
1077}