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