Skip to main content

allsource_core/infrastructure/security/
middleware.rs

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