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