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];
23
24pub const AUTH_SKIP_PREFIXES: &[&str] = &["/internal/"];
30
31#[inline]
33pub fn should_skip_auth(path: &str) -> bool {
34 AUTH_SKIP_PATHS.contains(&path) || AUTH_SKIP_PREFIXES.iter().any(|pfx| path.starts_with(pfx))
35}
36
37static DEV_MODE_ENABLED: LazyLock<bool> = LazyLock::new(|| {
44 let enabled = std::env::var("ALLSOURCE_DEV_MODE")
45 .map(|v| v == "true" || v == "1")
46 .unwrap_or(false);
47 if enabled {
48 tracing::warn!(
49 "⚠️ ALLSOURCE_DEV_MODE is enabled - authentication and rate limiting are DISABLED"
50 );
51 tracing::warn!("⚠️ This should NEVER be used in production!");
52 }
53 enabled
54});
55
56#[inline]
58pub fn is_dev_mode() -> bool {
59 *DEV_MODE_ENABLED
60}
61
62fn dev_mode_auth_context() -> AuthContext {
64 AuthContext {
65 claims: Claims::new(
66 "dev-user".to_string(),
67 "dev-tenant".to_string(),
68 Role::Admin,
69 chrono::Duration::hours(24),
70 ),
71 }
72}
73
74#[derive(Clone)]
76pub struct AuthState {
77 pub auth_manager: Arc<AuthManager>,
78}
79
80#[derive(Clone)]
82pub struct RateLimitState {
83 pub rate_limiter: Arc<RateLimiter>,
84}
85
86#[derive(Debug, Clone)]
88pub struct AuthContext {
89 pub claims: Claims,
90}
91
92impl AuthContext {
93 pub fn require_permission(&self, permission: Permission) -> Result<(), AllSourceError> {
95 if self.claims.has_permission(permission) {
96 Ok(())
97 } else {
98 Err(AllSourceError::ValidationError(
99 "Insufficient permissions".to_string(),
100 ))
101 }
102 }
103
104 pub fn tenant_id(&self) -> &str {
106 &self.claims.tenant_id
107 }
108
109 pub fn user_id(&self) -> &str {
111 &self.claims.sub
112 }
113}
114
115fn extract_token(headers: &HeaderMap) -> Result<String, AllSourceError> {
117 let auth_header = headers
118 .get("authorization")
119 .ok_or_else(|| AllSourceError::ValidationError("Missing authorization header".to_string()))?
120 .to_str()
121 .map_err(|_| AllSourceError::ValidationError("Invalid authorization header".to_string()))?;
122
123 let token = if auth_header.starts_with("Bearer ") {
125 auth_header.trim_start_matches("Bearer ").trim()
126 } else if auth_header.starts_with("bearer ") {
127 auth_header.trim_start_matches("bearer ").trim()
128 } else {
129 auth_header.trim()
130 };
131
132 if token.is_empty() {
133 return Err(AllSourceError::ValidationError(
134 "Empty authorization token".to_string(),
135 ));
136 }
137
138 Ok(token.to_string())
139}
140
141pub async fn auth_middleware(
143 State(auth_state): State<AuthState>,
144 mut request: Request,
145 next: Next,
146) -> Result<Response, AuthError> {
147 let path = request.uri().path();
149 if should_skip_auth(path) {
150 return Ok(next.run(request).await);
151 }
152
153 if is_dev_mode() {
155 request.extensions_mut().insert(dev_mode_auth_context());
156 return Ok(next.run(request).await);
157 }
158
159 let headers = request.headers();
160
161 let token = extract_token(headers)?;
163
164 let claims = if token.starts_with("ask_") {
165 auth_state.auth_manager.validate_api_key(&token)?
167 } else {
168 auth_state.auth_manager.validate_token(&token)?
170 };
171
172 request.extensions_mut().insert(AuthContext { claims });
174
175 Ok(next.run(request).await)
176}
177
178pub async fn optional_auth_middleware(
180 State(auth_state): State<AuthState>,
181 mut request: Request,
182 next: Next,
183) -> Response {
184 let headers = request.headers();
185
186 if let Ok(token) = extract_token(headers) {
187 let claims = if token.starts_with("ask_") {
189 auth_state.auth_manager.validate_api_key(&token).ok()
190 } else {
191 auth_state.auth_manager.validate_token(&token).ok()
192 };
193
194 if let Some(claims) = claims {
195 request.extensions_mut().insert(AuthContext { claims });
196 }
197 }
198
199 next.run(request).await
200}
201
202#[derive(Debug)]
204pub struct AuthError(AllSourceError);
205
206impl From<AllSourceError> for AuthError {
207 fn from(err: AllSourceError) -> Self {
208 AuthError(err)
209 }
210}
211
212impl IntoResponse for AuthError {
213 fn into_response(self) -> Response {
214 let (status, message) = match self.0 {
215 AllSourceError::ValidationError(msg) => (StatusCode::UNAUTHORIZED, msg),
216 _ => (
217 StatusCode::INTERNAL_SERVER_ERROR,
218 "Internal server error".to_string(),
219 ),
220 };
221
222 (status, message).into_response()
223 }
224}
225
226pub struct Authenticated(pub AuthContext);
228
229impl<S> axum::extract::FromRequestParts<S> for Authenticated
230where
231 S: Send + Sync,
232{
233 type Rejection = (StatusCode, &'static str);
234
235 async fn from_request_parts(
236 parts: &mut axum::http::request::Parts,
237 _state: &S,
238 ) -> Result<Self, Self::Rejection> {
239 parts
240 .extensions
241 .get::<AuthContext>()
242 .cloned()
243 .map(Authenticated)
244 .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))
245 }
246}
247
248pub struct OptionalAuth(pub Option<AuthContext>);
251
252impl<S> axum::extract::FromRequestParts<S> for OptionalAuth
253where
254 S: Send + Sync,
255{
256 type Rejection = std::convert::Infallible;
257
258 async fn from_request_parts(
259 parts: &mut axum::http::request::Parts,
260 _state: &S,
261 ) -> Result<Self, Self::Rejection> {
262 Ok(OptionalAuth(parts.extensions.get::<AuthContext>().cloned()))
263 }
264}
265
266pub struct Admin(pub AuthContext);
268
269impl<S> axum::extract::FromRequestParts<S> for Admin
270where
271 S: Send + Sync,
272{
273 type Rejection = (StatusCode, &'static str);
274
275 async fn from_request_parts(
276 parts: &mut axum::http::request::Parts,
277 _state: &S,
278 ) -> Result<Self, Self::Rejection> {
279 let auth_ctx = parts
280 .extensions
281 .get::<AuthContext>()
282 .cloned()
283 .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))?;
284
285 auth_ctx
286 .require_permission(Permission::Admin)
287 .map_err(|_| (StatusCode::FORBIDDEN, "Admin permission required"))?;
288
289 Ok(Admin(auth_ctx))
290 }
291}
292
293pub async fn rate_limit_middleware(
296 State(rate_limit_state): State<RateLimitState>,
297 request: Request,
298 next: Next,
299) -> Result<Response, RateLimitError> {
300 let path = request.uri().path();
302 if should_skip_auth(path) {
303 return Ok(next.run(request).await);
304 }
305
306 if is_dev_mode() {
308 return Ok(next.run(request).await);
309 }
310
311 let auth_ctx = request
313 .extensions()
314 .get::<AuthContext>()
315 .ok_or(RateLimitError::Unauthorized)?;
316
317 let result = rate_limit_state
319 .rate_limiter
320 .check_rate_limit(auth_ctx.tenant_id());
321
322 if !result.allowed {
323 return Err(RateLimitError::RateLimitExceeded {
324 retry_after: result.retry_after.unwrap_or_default().as_secs(),
325 limit: result.limit,
326 });
327 }
328
329 let mut response = next.run(request).await;
331 let headers = response.headers_mut();
332 headers.insert(
333 "X-RateLimit-Limit",
334 result.limit.to_string().parse().unwrap(),
335 );
336 headers.insert(
337 "X-RateLimit-Remaining",
338 result.remaining.to_string().parse().unwrap(),
339 );
340
341 Ok(response)
342}
343
344#[derive(Debug)]
346pub enum RateLimitError {
347 RateLimitExceeded { retry_after: u64, limit: u32 },
348 Unauthorized,
349}
350
351impl IntoResponse for RateLimitError {
352 fn into_response(self) -> Response {
353 match self {
354 RateLimitError::RateLimitExceeded { retry_after, limit } => {
355 let mut response = (
356 StatusCode::TOO_MANY_REQUESTS,
357 format!("Rate limit exceeded. Limit: {limit} requests/min"),
358 )
359 .into_response();
360
361 if retry_after > 0 {
362 response
363 .headers_mut()
364 .insert("Retry-After", retry_after.to_string().parse().unwrap());
365 }
366
367 response
368 }
369 RateLimitError::Unauthorized => (
370 StatusCode::UNAUTHORIZED,
371 "Authentication required for rate limiting",
372 )
373 .into_response(),
374 }
375 }
376}
377
378#[macro_export]
380macro_rules! require_permission {
381 ($auth:expr, $perm:expr) => {
382 $auth.0.require_permission($perm).map_err(|_| {
383 (
384 axum::http::StatusCode::FORBIDDEN,
385 "Insufficient permissions",
386 )
387 })?
388 };
389}
390
391use crate::domain::{entities::Tenant, repositories::TenantRepository, value_objects::TenantId};
396
397#[derive(Clone)]
399pub struct TenantState<R: TenantRepository> {
400 pub tenant_repository: Arc<R>,
401}
402
403#[derive(Debug, Clone)]
408pub struct TenantContext {
409 pub tenant: Tenant,
410}
411
412impl TenantContext {
413 pub fn tenant_id(&self) -> &TenantId {
415 self.tenant.id()
416 }
417
418 pub fn is_active(&self) -> bool {
420 self.tenant.is_active()
421 }
422}
423
424pub async fn tenant_isolation_middleware<R: TenantRepository + 'static>(
438 State(tenant_state): State<TenantState<R>>,
439 mut request: Request,
440 next: Next,
441) -> Result<Response, TenantError> {
442 let auth_ctx = request
444 .extensions()
445 .get::<AuthContext>()
446 .ok_or(TenantError::Unauthorized)?
447 .clone();
448
449 let tenant_id =
451 TenantId::new(auth_ctx.tenant_id().to_string()).map_err(|_| TenantError::InvalidTenant)?;
452
453 let tenant = tenant_state
455 .tenant_repository
456 .find_by_id(&tenant_id)
457 .await
458 .map_err(|e| TenantError::RepositoryError(e.to_string()))?
459 .ok_or(TenantError::TenantNotFound)?;
460
461 if !tenant.is_active() {
463 return Err(TenantError::TenantInactive);
464 }
465
466 request.extensions_mut().insert(TenantContext { tenant });
468
469 Ok(next.run(request).await)
471}
472
473#[derive(Debug)]
475pub enum TenantError {
476 Unauthorized,
477 InvalidTenant,
478 TenantNotFound,
479 TenantInactive,
480 RepositoryError(String),
481}
482
483impl IntoResponse for TenantError {
484 fn into_response(self) -> Response {
485 let (status, message) = match self {
486 TenantError::Unauthorized => (
487 StatusCode::UNAUTHORIZED,
488 "Authentication required for tenant access",
489 ),
490 TenantError::InvalidTenant => (StatusCode::BAD_REQUEST, "Invalid tenant identifier"),
491 TenantError::TenantNotFound => (StatusCode::NOT_FOUND, "Tenant not found"),
492 TenantError::TenantInactive => (StatusCode::FORBIDDEN, "Tenant is inactive"),
493 TenantError::RepositoryError(_) => (
494 StatusCode::INTERNAL_SERVER_ERROR,
495 "Failed to validate tenant",
496 ),
497 };
498
499 (status, message).into_response()
500 }
501}
502
503use uuid::Uuid;
508
509#[derive(Debug, Clone)]
511pub struct RequestId(pub String);
512
513impl Default for RequestId {
514 fn default() -> Self {
515 Self::new()
516 }
517}
518
519impl RequestId {
520 pub fn new() -> Self {
522 Self(Uuid::new_v4().to_string())
523 }
524
525 pub fn as_str(&self) -> &str {
527 &self.0
528 }
529}
530
531pub async fn request_id_middleware(mut request: Request, next: Next) -> Response {
546 let request_id = request
548 .headers()
549 .get("x-request-id")
550 .and_then(|v| v.to_str().ok())
551 .map(|s| RequestId(s.to_string()))
552 .unwrap_or_else(RequestId::new);
553
554 request.extensions_mut().insert(request_id.clone());
556
557 let mut response = next.run(request).await;
559
560 response
562 .headers_mut()
563 .insert("x-request-id", request_id.0.parse().unwrap());
564
565 response
566}
567
568#[derive(Debug, Clone)]
574pub struct SecurityConfig {
575 pub enable_hsts: bool,
577 pub hsts_max_age: u32,
579 pub enable_frame_options: bool,
581 pub frame_options: FrameOptions,
583 pub enable_content_type_options: bool,
585 pub enable_xss_protection: bool,
587 pub csp: Option<String>,
589 pub cors_origins: Vec<String>,
591 pub cors_methods: Vec<String>,
593 pub cors_headers: Vec<String>,
595 pub cors_max_age: u32,
597}
598
599#[derive(Debug, Clone)]
600pub enum FrameOptions {
601 Deny,
602 SameOrigin,
603 AllowFrom(String),
604}
605
606impl Default for SecurityConfig {
607 fn default() -> Self {
608 Self {
609 enable_hsts: true,
610 hsts_max_age: 31536000, enable_frame_options: true,
612 frame_options: FrameOptions::Deny,
613 enable_content_type_options: true,
614 enable_xss_protection: true,
615 csp: Some("default-src 'self'".to_string()),
616 cors_origins: vec!["*".to_string()],
617 cors_methods: vec![
618 "GET".to_string(),
619 "POST".to_string(),
620 "PUT".to_string(),
621 "DELETE".to_string(),
622 ],
623 cors_headers: vec!["Content-Type".to_string(), "Authorization".to_string()],
624 cors_max_age: 3600,
625 }
626 }
627}
628
629#[derive(Clone)]
630pub struct SecurityState {
631 pub config: SecurityConfig,
632}
633
634pub async fn security_headers_middleware(
652 State(security_state): State<SecurityState>,
653 request: Request,
654 next: Next,
655) -> Response {
656 let mut response = next.run(request).await;
657 let headers = response.headers_mut();
658 let config = &security_state.config;
659
660 if config.enable_hsts {
662 headers.insert(
663 "strict-transport-security",
664 format!("max-age={}", config.hsts_max_age).parse().unwrap(),
665 );
666 }
667
668 if config.enable_frame_options {
670 let value = match &config.frame_options {
671 FrameOptions::Deny => "DENY",
672 FrameOptions::SameOrigin => "SAMEORIGIN",
673 FrameOptions::AllowFrom(origin) => origin,
674 };
675 headers.insert("x-frame-options", value.parse().unwrap());
676 }
677
678 if config.enable_content_type_options {
680 headers.insert("x-content-type-options", "nosniff".parse().unwrap());
681 }
682
683 if config.enable_xss_protection {
685 headers.insert("x-xss-protection", "1; mode=block".parse().unwrap());
686 }
687
688 if let Some(csp) = &config.csp {
690 headers.insert("content-security-policy", csp.parse().unwrap());
691 }
692
693 headers.insert(
695 "access-control-allow-origin",
696 config.cors_origins.join(", ").parse().unwrap(),
697 );
698 headers.insert(
699 "access-control-allow-methods",
700 config.cors_methods.join(", ").parse().unwrap(),
701 );
702 headers.insert(
703 "access-control-allow-headers",
704 config.cors_headers.join(", ").parse().unwrap(),
705 );
706 headers.insert(
707 "access-control-max-age",
708 config.cors_max_age.to_string().parse().unwrap(),
709 );
710
711 response
712}
713
714use crate::infrastructure::security::IpFilter;
719use std::net::SocketAddr;
720
721#[derive(Clone)]
722pub struct IpFilterState {
723 pub ip_filter: Arc<IpFilter>,
724}
725
726pub async fn ip_filter_middleware(
738 State(ip_filter_state): State<IpFilterState>,
739 request: Request,
740 next: Next,
741) -> Result<Response, IpFilterError> {
742 let client_ip = request
744 .extensions()
745 .get::<axum::extract::ConnectInfo<SocketAddr>>()
746 .map(|connect_info| connect_info.0.ip())
747 .ok_or(IpFilterError::NoIpAddress)?;
748
749 let result = if let Some(tenant_ctx) = request.extensions().get::<TenantContext>() {
751 ip_filter_state
753 .ip_filter
754 .is_allowed_for_tenant(tenant_ctx.tenant_id(), &client_ip)
755 } else {
756 ip_filter_state.ip_filter.is_allowed(&client_ip)
758 };
759
760 if !result.allowed {
762 return Err(IpFilterError::Blocked {
763 reason: result.reason,
764 });
765 }
766
767 Ok(next.run(request).await)
769}
770
771#[derive(Debug)]
773pub enum IpFilterError {
774 NoIpAddress,
775 Blocked { reason: String },
776}
777
778impl IntoResponse for IpFilterError {
779 fn into_response(self) -> Response {
780 match self {
781 IpFilterError::NoIpAddress => (
782 StatusCode::BAD_REQUEST,
783 "Unable to determine client IP address",
784 )
785 .into_response(),
786 IpFilterError::Blocked { reason } => {
787 (StatusCode::FORBIDDEN, format!("Access denied: {reason}")).into_response()
788 }
789 }
790 }
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796 use crate::infrastructure::security::auth::Role;
797
798 #[test]
799 fn test_extract_bearer_token() {
800 let mut headers = HeaderMap::new();
801 headers.insert("authorization", "Bearer test_token_123".parse().unwrap());
802
803 let token = extract_token(&headers).unwrap();
804 assert_eq!(token, "test_token_123");
805 }
806
807 #[test]
808 fn test_extract_lowercase_bearer() {
809 let mut headers = HeaderMap::new();
810 headers.insert("authorization", "bearer test_token_123".parse().unwrap());
811
812 let token = extract_token(&headers).unwrap();
813 assert_eq!(token, "test_token_123");
814 }
815
816 #[test]
817 fn test_extract_plain_token() {
818 let mut headers = HeaderMap::new();
819 headers.insert("authorization", "test_token_123".parse().unwrap());
820
821 let token = extract_token(&headers).unwrap();
822 assert_eq!(token, "test_token_123");
823 }
824
825 #[test]
826 fn test_missing_auth_header() {
827 let headers = HeaderMap::new();
828 assert!(extract_token(&headers).is_err());
829 }
830
831 #[test]
832 fn test_empty_auth_header() {
833 let mut headers = HeaderMap::new();
834 headers.insert("authorization", "".parse().unwrap());
835 assert!(extract_token(&headers).is_err());
836 }
837
838 #[test]
839 fn test_bearer_with_empty_token() {
840 let mut headers = HeaderMap::new();
841 headers.insert("authorization", "Bearer ".parse().unwrap());
842 assert!(extract_token(&headers).is_err());
843 }
844
845 #[test]
846 fn test_auth_context_permissions() {
847 let claims = Claims::new(
848 "user1".to_string(),
849 "tenant1".to_string(),
850 Role::Developer,
851 chrono::Duration::hours(1),
852 );
853
854 let ctx = AuthContext { claims };
855
856 assert!(ctx.require_permission(Permission::Read).is_ok());
857 assert!(ctx.require_permission(Permission::Write).is_ok());
858 assert!(ctx.require_permission(Permission::Admin).is_err());
859 }
860
861 #[test]
862 fn test_auth_context_admin_permissions() {
863 let claims = Claims::new(
864 "admin1".to_string(),
865 "tenant1".to_string(),
866 Role::Admin,
867 chrono::Duration::hours(1),
868 );
869
870 let ctx = AuthContext { claims };
871
872 assert!(ctx.require_permission(Permission::Read).is_ok());
873 assert!(ctx.require_permission(Permission::Write).is_ok());
874 assert!(ctx.require_permission(Permission::Admin).is_ok());
875 }
876
877 #[test]
878 fn test_auth_context_readonly_permissions() {
879 let claims = Claims::new(
880 "readonly1".to_string(),
881 "tenant1".to_string(),
882 Role::ReadOnly,
883 chrono::Duration::hours(1),
884 );
885
886 let ctx = AuthContext { claims };
887
888 assert!(ctx.require_permission(Permission::Read).is_ok());
889 assert!(ctx.require_permission(Permission::Write).is_err());
890 assert!(ctx.require_permission(Permission::Admin).is_err());
891 }
892
893 #[test]
894 fn test_auth_context_tenant_id() {
895 let claims = Claims::new(
896 "user1".to_string(),
897 "my-tenant".to_string(),
898 Role::Developer,
899 chrono::Duration::hours(1),
900 );
901
902 let ctx = AuthContext { claims };
903 assert_eq!(ctx.tenant_id(), "my-tenant");
904 }
905
906 #[test]
907 fn test_auth_context_user_id() {
908 let claims = Claims::new(
909 "my-user".to_string(),
910 "tenant1".to_string(),
911 Role::Developer,
912 chrono::Duration::hours(1),
913 );
914
915 let ctx = AuthContext { claims };
916 assert_eq!(ctx.user_id(), "my-user");
917 }
918
919 #[test]
920 fn test_request_id_new() {
921 let id1 = RequestId::new();
922 let id2 = RequestId::new();
923
924 assert_ne!(id1.as_str(), id2.as_str());
926 assert_eq!(id1.as_str().len(), 36);
928 }
929
930 #[test]
931 fn test_request_id_default() {
932 let id = RequestId::default();
933 assert_eq!(id.as_str().len(), 36);
934 }
935
936 #[test]
937 fn test_security_config_default() {
938 let config = SecurityConfig::default();
939
940 assert!(config.enable_hsts);
941 assert_eq!(config.hsts_max_age, 31536000);
942 assert!(config.enable_frame_options);
943 assert!(config.enable_content_type_options);
944 assert!(config.enable_xss_protection);
945 assert!(config.csp.is_some());
946 }
947
948 #[test]
949 fn test_frame_options_variants() {
950 let deny = FrameOptions::Deny;
951 let same_origin = FrameOptions::SameOrigin;
952 let allow_from = FrameOptions::AllowFrom("https://example.com".to_string());
953
954 assert!(format!("{:?}", deny).contains("Deny"));
956 assert!(format!("{:?}", same_origin).contains("SameOrigin"));
957 assert!(format!("{:?}", allow_from).contains("AllowFrom"));
958 }
959
960 #[test]
961 fn test_auth_error_from_validation_error() {
962 let error = AllSourceError::ValidationError("test error".to_string());
963 let auth_error = AuthError::from(error);
964 assert!(format!("{:?}", auth_error).contains("ValidationError"));
965 }
966
967 #[test]
968 fn test_rate_limit_error_display() {
969 let error = RateLimitError::RateLimitExceeded {
970 retry_after: 60,
971 limit: 100,
972 };
973 assert!(format!("{:?}", error).contains("RateLimitExceeded"));
974
975 let unauth_error = RateLimitError::Unauthorized;
976 assert!(format!("{:?}", unauth_error).contains("Unauthorized"));
977 }
978
979 #[test]
980 fn test_tenant_error_variants() {
981 let errors = vec![
982 TenantError::Unauthorized,
983 TenantError::InvalidTenant,
984 TenantError::TenantNotFound,
985 TenantError::TenantInactive,
986 TenantError::RepositoryError("test".to_string()),
987 ];
988
989 for error in errors {
990 let _ = format!("{:?}", error);
992 }
993 }
994
995 #[test]
996 fn test_ip_filter_error_variants() {
997 let errors = vec![
998 IpFilterError::NoIpAddress,
999 IpFilterError::Blocked {
1000 reason: "blocked".to_string(),
1001 },
1002 ];
1003
1004 for error in errors {
1005 let _ = format!("{:?}", error);
1006 }
1007 }
1008
1009 #[test]
1010 fn test_security_state_clone() {
1011 let config = SecurityConfig::default();
1012 let state = SecurityState {
1013 config: config.clone(),
1014 };
1015 let cloned = state.clone();
1016 assert_eq!(cloned.config.hsts_max_age, config.hsts_max_age);
1017 }
1018
1019 #[test]
1020 fn test_auth_state_clone() {
1021 let auth_manager = Arc::new(AuthManager::new("test-secret"));
1022 let state = AuthState { auth_manager };
1023 let cloned = state.clone();
1024 assert!(Arc::ptr_eq(&state.auth_manager, &cloned.auth_manager));
1025 }
1026
1027 #[test]
1028 fn test_rate_limit_state_clone() {
1029 use crate::infrastructure::security::rate_limit::RateLimitConfig;
1030 let rate_limiter = Arc::new(RateLimiter::new(RateLimitConfig::free_tier()));
1031 let state = RateLimitState { rate_limiter };
1032 let cloned = state.clone();
1033 assert!(Arc::ptr_eq(&state.rate_limiter, &cloned.rate_limiter));
1034 }
1035
1036 #[test]
1037 fn test_auth_skip_paths_contains_expected() {
1038 assert!(should_skip_auth("/health"));
1040 assert!(should_skip_auth("/metrics"));
1041 assert!(should_skip_auth("/api/v1/auth/register"));
1042 assert!(should_skip_auth("/api/v1/auth/login"));
1043
1044 assert!(should_skip_auth("/internal/promote"));
1046 assert!(should_skip_auth("/internal/repoint"));
1047 assert!(should_skip_auth("/internal/anything"));
1048
1049 assert!(!should_skip_auth("/api/v1/events"));
1051 assert!(!should_skip_auth("/api/v1/auth/me"));
1052 assert!(!should_skip_auth("/api/v1/tenants"));
1053 }
1054
1055 #[test]
1056 fn test_dev_mode_auth_context() {
1057 let ctx = dev_mode_auth_context();
1058
1059 assert_eq!(ctx.tenant_id(), "dev-tenant");
1061 assert_eq!(ctx.user_id(), "dev-user");
1062 assert!(ctx.require_permission(Permission::Admin).is_ok());
1063 assert!(ctx.require_permission(Permission::Read).is_ok());
1064 assert!(ctx.require_permission(Permission::Write).is_ok());
1065 }
1066
1067 #[test]
1068 fn test_dev_mode_disabled_by_default() {
1069 let env_value = std::env::var("ALLSOURCE_DEV_MODE").unwrap_or_default();
1073 if env_value.is_empty() {
1074 assert!(!is_dev_mode());
1075 }
1076 }
1077}