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
16pub 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
25pub 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#[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
73fn 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#[inline]
111pub fn is_dev_mode() -> bool {
112 *DEV_MODE_ENABLED
113}
114
115fn 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#[derive(Clone)]
129pub struct AuthState {
130 pub auth_manager: Arc<AuthManager>,
131}
132
133#[derive(Clone)]
135pub struct RateLimitState {
136 pub rate_limiter: Arc<RateLimiter>,
137}
138
139#[derive(Debug, Clone)]
141pub struct AuthContext {
142 pub claims: Claims,
143}
144
145impl AuthContext {
146 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 pub fn tenant_id(&self) -> &str {
159 &self.claims.tenant_id
160 }
161
162 pub fn user_id(&self) -> &str {
164 &self.claims.sub
165 }
166}
167
168fn extract_token(headers: &HeaderMap) -> Result<String, AllSourceError> {
170 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 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#[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
218pub async fn auth_middleware(
220 State(auth_state): State<AuthState>,
221 mut request: Request,
222 next: Next,
223) -> Result<Response, AuthError> {
224 let path = request.uri().path();
226 if should_skip_auth(path) {
227 return Ok(next.run(request).await);
228 }
229
230 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 let token = extract_token(headers)?;
254
255 let claims = if token.starts_with("ask_") {
256 auth_state.auth_manager.validate_api_key(&token)?
258 } else {
259 auth_state.auth_manager.validate_token(&token)?
261 };
262
263 let auth_ctx = AuthContext { claims };
264
265 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 request.extensions_mut().insert(auth_ctx);
284
285 Ok(next.run(request).await)
286}
287
288pub 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 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#[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
336pub 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
358pub 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
376pub 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
403pub async fn rate_limit_middleware(
406 State(rate_limit_state): State<RateLimitState>,
407 request: Request,
408 next: Next,
409) -> Result<Response, RateLimitError> {
410 let path = request.uri().path();
412 if should_skip_auth(path) {
413 return Ok(next.run(request).await);
414 }
415
416 if is_dev_mode() {
418 return Ok(next.run(request).await);
419 }
420
421 let auth_ctx = request
423 .extensions()
424 .get::<AuthContext>()
425 .ok_or(RateLimitError::Unauthorized)?;
426
427 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 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#[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#[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
501use crate::domain::{entities::Tenant, repositories::TenantRepository, value_objects::TenantId};
506
507#[derive(Clone)]
509pub struct TenantState<R: TenantRepository> {
510 pub tenant_repository: Arc<R>,
511}
512
513#[derive(Debug, Clone)]
518pub struct TenantContext {
519 pub tenant: Tenant,
520}
521
522impl TenantContext {
523 pub fn tenant_id(&self) -> &TenantId {
525 self.tenant.id()
526 }
527
528 pub fn is_active(&self) -> bool {
530 self.tenant.is_active()
531 }
532}
533
534pub 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 let auth_ctx = request
554 .extensions()
555 .get::<AuthContext>()
556 .ok_or(TenantError::Unauthorized)?
557 .clone();
558
559 let tenant_id =
561 TenantId::new(auth_ctx.tenant_id().to_string()).map_err(|_| TenantError::InvalidTenant)?;
562
563 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 if !tenant.is_active() {
573 return Err(TenantError::TenantInactive);
574 }
575
576 request.extensions_mut().insert(TenantContext { tenant });
578
579 Ok(next.run(request).await)
581}
582
583#[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
613use uuid::Uuid;
618
619#[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 pub fn new() -> Self {
632 Self(Uuid::new_v4().to_string())
633 }
634
635 pub fn as_str(&self) -> &str {
637 &self.0
638 }
639}
640
641pub async fn request_id_middleware(mut request: Request, next: Next) -> Response {
656 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 request.extensions_mut().insert(request_id.clone());
665
666 let mut response = next.run(request).await;
668
669 response
671 .headers_mut()
672 .insert("x-request-id", request_id.0.parse().unwrap());
673
674 response
675}
676
677#[derive(Debug, Clone)]
683pub struct SecurityConfig {
684 pub enable_hsts: bool,
686 pub hsts_max_age: u32,
688 pub enable_frame_options: bool,
690 pub frame_options: FrameOptions,
692 pub enable_content_type_options: bool,
694 pub enable_xss_protection: bool,
696 pub csp: Option<String>,
698 pub cors_origins: Vec<String>,
700 pub cors_methods: Vec<String>,
702 pub cors_headers: Vec<String>,
704 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, 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
743pub 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 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 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 if config.enable_content_type_options {
789 headers.insert("x-content-type-options", "nosniff".parse().unwrap());
790 }
791
792 if config.enable_xss_protection {
794 headers.insert("x-xss-protection", "1; mode=block".parse().unwrap());
795 }
796
797 if let Some(csp) = &config.csp {
799 headers.insert("content-security-policy", csp.parse().unwrap());
800 }
801
802 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
823use crate::infrastructure::security::IpFilter;
828use std::net::SocketAddr;
829
830#[derive(Clone)]
831pub struct IpFilterState {
832 pub ip_filter: Arc<IpFilter>,
833}
834
835pub async fn ip_filter_middleware(
847 State(ip_filter_state): State<IpFilterState>,
848 request: Request,
849 next: Next,
850) -> Result<Response, IpFilterError> {
851 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 let result = if let Some(tenant_ctx) = request.extensions().get::<TenantContext>() {
860 ip_filter_state
862 .ip_filter
863 .is_allowed_for_tenant(tenant_ctx.tenant_id(), &client_ip)
864 } else {
865 ip_filter_state.ip_filter.is_allowed(&client_ip)
867 };
868
869 if !result.allowed {
871 return Err(IpFilterError::Blocked {
872 reason: result.reason,
873 });
874 }
875
876 Ok(next.run(request).await)
878}
879
880#[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 #[test]
959 fn test_service_account_blocked_on_admin_paths() {
960 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 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 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 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 assert!(is_admin_only_path("/api/v1/auth/api-keys", "POST"));
1028 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 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 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 assert_ne!(id1.as_str(), id2.as_str());
1133 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 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 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 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 assert!(should_skip_auth("/internal/promote"));
1254 assert!(should_skip_auth("/internal/repoint"));
1255 assert!(should_skip_auth("/internal/anything"));
1256
1257 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 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 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 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}