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