1use crate::error::AllSourceError;
2use crate::infrastructure::security::auth::{AuthManager, Claims, Permission, Role};
3use crate::infrastructure::security::rate_limit::RateLimiter;
4use axum::{
5 extract::{Request, State},
6 http::{HeaderMap, StatusCode},
7 middleware::Next,
8 response::{IntoResponse, Response},
9};
10use std::sync::{Arc, LazyLock};
11
12pub const AUTH_SKIP_PATHS: &[&str] = &[
14 "/health",
15 "/metrics",
16 "/api/v1/auth/register",
17 "/api/v1/auth/login",
18];
19
20static DEV_MODE_ENABLED: LazyLock<bool> = LazyLock::new(|| {
27 let enabled = std::env::var("ALLSOURCE_DEV_MODE")
28 .map(|v| v == "true" || v == "1")
29 .unwrap_or(false);
30 if enabled {
31 tracing::warn!("⚠️ ALLSOURCE_DEV_MODE is enabled - authentication and rate limiting are DISABLED");
32 tracing::warn!("⚠️ This should NEVER be used in production!");
33 }
34 enabled
35});
36
37#[inline]
39pub fn is_dev_mode() -> bool {
40 *DEV_MODE_ENABLED
41}
42
43fn dev_mode_auth_context() -> AuthContext {
45 AuthContext {
46 claims: Claims::new(
47 "dev-user".to_string(),
48 "dev-tenant".to_string(),
49 Role::Admin,
50 chrono::Duration::hours(24),
51 ),
52 }
53}
54
55#[derive(Clone)]
57pub struct AuthState {
58 pub auth_manager: Arc<AuthManager>,
59}
60
61#[derive(Clone)]
63pub struct RateLimitState {
64 pub rate_limiter: Arc<RateLimiter>,
65}
66
67#[derive(Debug, Clone)]
69pub struct AuthContext {
70 pub claims: Claims,
71}
72
73impl AuthContext {
74 pub fn require_permission(&self, permission: Permission) -> Result<(), AllSourceError> {
76 if self.claims.has_permission(permission) {
77 Ok(())
78 } else {
79 Err(AllSourceError::ValidationError(
80 "Insufficient permissions".to_string(),
81 ))
82 }
83 }
84
85 pub fn tenant_id(&self) -> &str {
87 &self.claims.tenant_id
88 }
89
90 pub fn user_id(&self) -> &str {
92 &self.claims.sub
93 }
94}
95
96fn extract_token(headers: &HeaderMap) -> Result<String, AllSourceError> {
98 let auth_header = headers
99 .get("authorization")
100 .ok_or_else(|| AllSourceError::ValidationError("Missing authorization header".to_string()))?
101 .to_str()
102 .map_err(|_| AllSourceError::ValidationError("Invalid authorization header".to_string()))?;
103
104 let token = if auth_header.starts_with("Bearer ") {
106 auth_header.trim_start_matches("Bearer ").trim()
107 } else if auth_header.starts_with("bearer ") {
108 auth_header.trim_start_matches("bearer ").trim()
109 } else {
110 auth_header.trim()
111 };
112
113 if token.is_empty() {
114 return Err(AllSourceError::ValidationError(
115 "Empty authorization token".to_string(),
116 ));
117 }
118
119 Ok(token.to_string())
120}
121
122pub async fn auth_middleware(
124 State(auth_state): State<AuthState>,
125 mut request: Request,
126 next: Next,
127) -> Result<Response, AuthError> {
128 let path = request.uri().path();
130 if AUTH_SKIP_PATHS.contains(&path) {
131 return Ok(next.run(request).await);
132 }
133
134 if is_dev_mode() {
136 request.extensions_mut().insert(dev_mode_auth_context());
137 return Ok(next.run(request).await);
138 }
139
140 let headers = request.headers();
141
142 let token = extract_token(headers)?;
144
145 let claims = if token.starts_with("ask_") {
146 auth_state.auth_manager.validate_api_key(&token)?
148 } else {
149 auth_state.auth_manager.validate_token(&token)?
151 };
152
153 request.extensions_mut().insert(AuthContext { claims });
155
156 Ok(next.run(request).await)
157}
158
159pub async fn optional_auth_middleware(
161 State(auth_state): State<AuthState>,
162 mut request: Request,
163 next: Next,
164) -> Response {
165 let headers = request.headers();
166
167 if let Ok(token) = extract_token(headers) {
168 let claims = if token.starts_with("ask_") {
170 auth_state.auth_manager.validate_api_key(&token).ok()
171 } else {
172 auth_state.auth_manager.validate_token(&token).ok()
173 };
174
175 if let Some(claims) = claims {
176 request.extensions_mut().insert(AuthContext { claims });
177 }
178 }
179
180 next.run(request).await
181}
182
183#[derive(Debug)]
185pub struct AuthError(AllSourceError);
186
187impl From<AllSourceError> for AuthError {
188 fn from(err: AllSourceError) -> Self {
189 AuthError(err)
190 }
191}
192
193impl IntoResponse for AuthError {
194 fn into_response(self) -> Response {
195 let (status, message) = match self.0 {
196 AllSourceError::ValidationError(msg) => (StatusCode::UNAUTHORIZED, msg),
197 _ => (
198 StatusCode::INTERNAL_SERVER_ERROR,
199 "Internal server error".to_string(),
200 ),
201 };
202
203 (status, message).into_response()
204 }
205}
206
207pub struct Authenticated(pub AuthContext);
209
210impl<S> axum::extract::FromRequestParts<S> for Authenticated
211where
212 S: Send + Sync,
213{
214 type Rejection = (StatusCode, &'static str);
215
216 async fn from_request_parts(
217 parts: &mut axum::http::request::Parts,
218 _state: &S,
219 ) -> Result<Self, Self::Rejection> {
220 parts
221 .extensions
222 .get::<AuthContext>()
223 .cloned()
224 .map(Authenticated)
225 .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))
226 }
227}
228
229pub struct OptionalAuth(pub Option<AuthContext>);
232
233impl<S> axum::extract::FromRequestParts<S> for OptionalAuth
234where
235 S: Send + Sync,
236{
237 type Rejection = std::convert::Infallible;
238
239 async fn from_request_parts(
240 parts: &mut axum::http::request::Parts,
241 _state: &S,
242 ) -> Result<Self, Self::Rejection> {
243 Ok(OptionalAuth(parts.extensions.get::<AuthContext>().cloned()))
244 }
245}
246
247pub struct Admin(pub AuthContext);
249
250impl<S> axum::extract::FromRequestParts<S> for Admin
251where
252 S: Send + Sync,
253{
254 type Rejection = (StatusCode, &'static str);
255
256 async fn from_request_parts(
257 parts: &mut axum::http::request::Parts,
258 _state: &S,
259 ) -> Result<Self, Self::Rejection> {
260 let auth_ctx = parts
261 .extensions
262 .get::<AuthContext>()
263 .cloned()
264 .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))?;
265
266 auth_ctx
267 .require_permission(Permission::Admin)
268 .map_err(|_| (StatusCode::FORBIDDEN, "Admin permission required"))?;
269
270 Ok(Admin(auth_ctx))
271 }
272}
273
274pub async fn rate_limit_middleware(
277 State(rate_limit_state): State<RateLimitState>,
278 request: Request,
279 next: Next,
280) -> Result<Response, RateLimitError> {
281 let path = request.uri().path();
283 if AUTH_SKIP_PATHS.contains(&path) {
284 return Ok(next.run(request).await);
285 }
286
287 if is_dev_mode() {
289 return Ok(next.run(request).await);
290 }
291
292 let auth_ctx = request
294 .extensions()
295 .get::<AuthContext>()
296 .ok_or(RateLimitError::Unauthorized)?;
297
298 let result = rate_limit_state
300 .rate_limiter
301 .check_rate_limit(auth_ctx.tenant_id());
302
303 if !result.allowed {
304 return Err(RateLimitError::RateLimitExceeded {
305 retry_after: result.retry_after.unwrap_or_default().as_secs(),
306 limit: result.limit,
307 });
308 }
309
310 let mut response = next.run(request).await;
312 let headers = response.headers_mut();
313 headers.insert(
314 "X-RateLimit-Limit",
315 result.limit.to_string().parse().unwrap(),
316 );
317 headers.insert(
318 "X-RateLimit-Remaining",
319 result.remaining.to_string().parse().unwrap(),
320 );
321
322 Ok(response)
323}
324
325#[derive(Debug)]
327pub enum RateLimitError {
328 RateLimitExceeded { retry_after: u64, limit: u32 },
329 Unauthorized,
330}
331
332impl IntoResponse for RateLimitError {
333 fn into_response(self) -> Response {
334 match self {
335 RateLimitError::RateLimitExceeded { retry_after, limit } => {
336 let mut response = (
337 StatusCode::TOO_MANY_REQUESTS,
338 format!("Rate limit exceeded. Limit: {limit} requests/min"),
339 )
340 .into_response();
341
342 if retry_after > 0 {
343 response
344 .headers_mut()
345 .insert("Retry-After", retry_after.to_string().parse().unwrap());
346 }
347
348 response
349 }
350 RateLimitError::Unauthorized => (
351 StatusCode::UNAUTHORIZED,
352 "Authentication required for rate limiting",
353 )
354 .into_response(),
355 }
356 }
357}
358
359#[macro_export]
361macro_rules! require_permission {
362 ($auth:expr, $perm:expr) => {
363 $auth.0.require_permission($perm).map_err(|_| {
364 (
365 axum::http::StatusCode::FORBIDDEN,
366 "Insufficient permissions",
367 )
368 })?
369 };
370}
371
372use crate::domain::entities::Tenant;
377use crate::domain::repositories::TenantRepository;
378use crate::domain::value_objects::TenantId;
379
380#[derive(Clone)]
382pub struct TenantState<R: TenantRepository> {
383 pub tenant_repository: Arc<R>,
384}
385
386#[derive(Debug, Clone)]
391pub struct TenantContext {
392 pub tenant: Tenant,
393}
394
395impl TenantContext {
396 pub fn tenant_id(&self) -> &TenantId {
398 self.tenant.id()
399 }
400
401 pub fn is_active(&self) -> bool {
403 self.tenant.is_active()
404 }
405}
406
407pub async fn tenant_isolation_middleware<R: TenantRepository + 'static>(
421 State(tenant_state): State<TenantState<R>>,
422 mut request: Request,
423 next: Next,
424) -> Result<Response, TenantError> {
425 let auth_ctx = request
427 .extensions()
428 .get::<AuthContext>()
429 .ok_or(TenantError::Unauthorized)?
430 .clone();
431
432 let tenant_id =
434 TenantId::new(auth_ctx.tenant_id().to_string()).map_err(|_| TenantError::InvalidTenant)?;
435
436 let tenant = tenant_state
438 .tenant_repository
439 .find_by_id(&tenant_id)
440 .await
441 .map_err(|e| TenantError::RepositoryError(e.to_string()))?
442 .ok_or(TenantError::TenantNotFound)?;
443
444 if !tenant.is_active() {
446 return Err(TenantError::TenantInactive);
447 }
448
449 request.extensions_mut().insert(TenantContext { tenant });
451
452 Ok(next.run(request).await)
454}
455
456#[derive(Debug)]
458pub enum TenantError {
459 Unauthorized,
460 InvalidTenant,
461 TenantNotFound,
462 TenantInactive,
463 RepositoryError(String),
464}
465
466impl IntoResponse for TenantError {
467 fn into_response(self) -> Response {
468 let (status, message) = match self {
469 TenantError::Unauthorized => (
470 StatusCode::UNAUTHORIZED,
471 "Authentication required for tenant access",
472 ),
473 TenantError::InvalidTenant => (StatusCode::BAD_REQUEST, "Invalid tenant identifier"),
474 TenantError::TenantNotFound => (StatusCode::NOT_FOUND, "Tenant not found"),
475 TenantError::TenantInactive => (StatusCode::FORBIDDEN, "Tenant is inactive"),
476 TenantError::RepositoryError(_) => (
477 StatusCode::INTERNAL_SERVER_ERROR,
478 "Failed to validate tenant",
479 ),
480 };
481
482 (status, message).into_response()
483 }
484}
485
486use uuid::Uuid;
491
492#[derive(Debug, Clone)]
494pub struct RequestId(pub String);
495
496impl Default for RequestId {
497 fn default() -> Self {
498 Self::new()
499 }
500}
501
502impl RequestId {
503 pub fn new() -> Self {
505 Self(Uuid::new_v4().to_string())
506 }
507
508 pub fn as_str(&self) -> &str {
510 &self.0
511 }
512}
513
514pub async fn request_id_middleware(mut request: Request, next: Next) -> Response {
529 let request_id = request
531 .headers()
532 .get("x-request-id")
533 .and_then(|v| v.to_str().ok())
534 .map(|s| RequestId(s.to_string()))
535 .unwrap_or_else(RequestId::new);
536
537 request.extensions_mut().insert(request_id.clone());
539
540 let mut response = next.run(request).await;
542
543 response
545 .headers_mut()
546 .insert("x-request-id", request_id.0.parse().unwrap());
547
548 response
549}
550
551#[derive(Debug, Clone)]
557pub struct SecurityConfig {
558 pub enable_hsts: bool,
560 pub hsts_max_age: u32,
562 pub enable_frame_options: bool,
564 pub frame_options: FrameOptions,
566 pub enable_content_type_options: bool,
568 pub enable_xss_protection: bool,
570 pub csp: Option<String>,
572 pub cors_origins: Vec<String>,
574 pub cors_methods: Vec<String>,
576 pub cors_headers: Vec<String>,
578 pub cors_max_age: u32,
580}
581
582#[derive(Debug, Clone)]
583pub enum FrameOptions {
584 Deny,
585 SameOrigin,
586 AllowFrom(String),
587}
588
589impl Default for SecurityConfig {
590 fn default() -> Self {
591 Self {
592 enable_hsts: true,
593 hsts_max_age: 31536000, enable_frame_options: true,
595 frame_options: FrameOptions::Deny,
596 enable_content_type_options: true,
597 enable_xss_protection: true,
598 csp: Some("default-src 'self'".to_string()),
599 cors_origins: vec!["*".to_string()],
600 cors_methods: vec![
601 "GET".to_string(),
602 "POST".to_string(),
603 "PUT".to_string(),
604 "DELETE".to_string(),
605 ],
606 cors_headers: vec!["Content-Type".to_string(), "Authorization".to_string()],
607 cors_max_age: 3600,
608 }
609 }
610}
611
612#[derive(Clone)]
613pub struct SecurityState {
614 pub config: SecurityConfig,
615}
616
617pub async fn security_headers_middleware(
635 State(security_state): State<SecurityState>,
636 request: Request,
637 next: Next,
638) -> Response {
639 let mut response = next.run(request).await;
640 let headers = response.headers_mut();
641 let config = &security_state.config;
642
643 if config.enable_hsts {
645 headers.insert(
646 "strict-transport-security",
647 format!("max-age={}", config.hsts_max_age).parse().unwrap(),
648 );
649 }
650
651 if config.enable_frame_options {
653 let value = match &config.frame_options {
654 FrameOptions::Deny => "DENY",
655 FrameOptions::SameOrigin => "SAMEORIGIN",
656 FrameOptions::AllowFrom(origin) => origin,
657 };
658 headers.insert("x-frame-options", value.parse().unwrap());
659 }
660
661 if config.enable_content_type_options {
663 headers.insert("x-content-type-options", "nosniff".parse().unwrap());
664 }
665
666 if config.enable_xss_protection {
668 headers.insert("x-xss-protection", "1; mode=block".parse().unwrap());
669 }
670
671 if let Some(csp) = &config.csp {
673 headers.insert("content-security-policy", csp.parse().unwrap());
674 }
675
676 headers.insert(
678 "access-control-allow-origin",
679 config.cors_origins.join(", ").parse().unwrap(),
680 );
681 headers.insert(
682 "access-control-allow-methods",
683 config.cors_methods.join(", ").parse().unwrap(),
684 );
685 headers.insert(
686 "access-control-allow-headers",
687 config.cors_headers.join(", ").parse().unwrap(),
688 );
689 headers.insert(
690 "access-control-max-age",
691 config.cors_max_age.to_string().parse().unwrap(),
692 );
693
694 response
695}
696
697use crate::infrastructure::security::IpFilter;
702use std::net::SocketAddr;
703
704#[derive(Clone)]
705pub struct IpFilterState {
706 pub ip_filter: Arc<IpFilter>,
707}
708
709pub async fn ip_filter_middleware(
721 State(ip_filter_state): State<IpFilterState>,
722 request: Request,
723 next: Next,
724) -> Result<Response, IpFilterError> {
725 let client_ip = request
727 .extensions()
728 .get::<axum::extract::ConnectInfo<SocketAddr>>()
729 .map(|connect_info| connect_info.0.ip())
730 .ok_or(IpFilterError::NoIpAddress)?;
731
732 let result = if let Some(tenant_ctx) = request.extensions().get::<TenantContext>() {
734 ip_filter_state
736 .ip_filter
737 .is_allowed_for_tenant(tenant_ctx.tenant_id(), &client_ip)
738 } else {
739 ip_filter_state.ip_filter.is_allowed(&client_ip)
741 };
742
743 if !result.allowed {
745 return Err(IpFilterError::Blocked {
746 reason: result.reason,
747 });
748 }
749
750 Ok(next.run(request).await)
752}
753
754#[derive(Debug)]
756pub enum IpFilterError {
757 NoIpAddress,
758 Blocked { reason: String },
759}
760
761impl IntoResponse for IpFilterError {
762 fn into_response(self) -> Response {
763 match self {
764 IpFilterError::NoIpAddress => (
765 StatusCode::BAD_REQUEST,
766 "Unable to determine client IP address",
767 )
768 .into_response(),
769 IpFilterError::Blocked { reason } => {
770 (StatusCode::FORBIDDEN, format!("Access denied: {reason}")).into_response()
771 }
772 }
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use crate::infrastructure::security::auth::Role;
780
781 #[test]
782 fn test_extract_bearer_token() {
783 let mut headers = HeaderMap::new();
784 headers.insert("authorization", "Bearer test_token_123".parse().unwrap());
785
786 let token = extract_token(&headers).unwrap();
787 assert_eq!(token, "test_token_123");
788 }
789
790 #[test]
791 fn test_extract_lowercase_bearer() {
792 let mut headers = HeaderMap::new();
793 headers.insert("authorization", "bearer test_token_123".parse().unwrap());
794
795 let token = extract_token(&headers).unwrap();
796 assert_eq!(token, "test_token_123");
797 }
798
799 #[test]
800 fn test_extract_plain_token() {
801 let mut headers = HeaderMap::new();
802 headers.insert("authorization", "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_missing_auth_header() {
810 let headers = HeaderMap::new();
811 assert!(extract_token(&headers).is_err());
812 }
813
814 #[test]
815 fn test_empty_auth_header() {
816 let mut headers = HeaderMap::new();
817 headers.insert("authorization", "".parse().unwrap());
818 assert!(extract_token(&headers).is_err());
819 }
820
821 #[test]
822 fn test_bearer_with_empty_token() {
823 let mut headers = HeaderMap::new();
824 headers.insert("authorization", "Bearer ".parse().unwrap());
825 assert!(extract_token(&headers).is_err());
826 }
827
828 #[test]
829 fn test_auth_context_permissions() {
830 let claims = Claims::new(
831 "user1".to_string(),
832 "tenant1".to_string(),
833 Role::Developer,
834 chrono::Duration::hours(1),
835 );
836
837 let ctx = AuthContext { claims };
838
839 assert!(ctx.require_permission(Permission::Read).is_ok());
840 assert!(ctx.require_permission(Permission::Write).is_ok());
841 assert!(ctx.require_permission(Permission::Admin).is_err());
842 }
843
844 #[test]
845 fn test_auth_context_admin_permissions() {
846 let claims = Claims::new(
847 "admin1".to_string(),
848 "tenant1".to_string(),
849 Role::Admin,
850 chrono::Duration::hours(1),
851 );
852
853 let ctx = AuthContext { claims };
854
855 assert!(ctx.require_permission(Permission::Read).is_ok());
856 assert!(ctx.require_permission(Permission::Write).is_ok());
857 assert!(ctx.require_permission(Permission::Admin).is_ok());
858 }
859
860 #[test]
861 fn test_auth_context_readonly_permissions() {
862 let claims = Claims::new(
863 "readonly1".to_string(),
864 "tenant1".to_string(),
865 Role::ReadOnly,
866 chrono::Duration::hours(1),
867 );
868
869 let ctx = AuthContext { claims };
870
871 assert!(ctx.require_permission(Permission::Read).is_ok());
872 assert!(ctx.require_permission(Permission::Write).is_err());
873 assert!(ctx.require_permission(Permission::Admin).is_err());
874 }
875
876 #[test]
877 fn test_auth_context_tenant_id() {
878 let claims = Claims::new(
879 "user1".to_string(),
880 "my-tenant".to_string(),
881 Role::Developer,
882 chrono::Duration::hours(1),
883 );
884
885 let ctx = AuthContext { claims };
886 assert_eq!(ctx.tenant_id(), "my-tenant");
887 }
888
889 #[test]
890 fn test_auth_context_user_id() {
891 let claims = Claims::new(
892 "my-user".to_string(),
893 "tenant1".to_string(),
894 Role::Developer,
895 chrono::Duration::hours(1),
896 );
897
898 let ctx = AuthContext { claims };
899 assert_eq!(ctx.user_id(), "my-user");
900 }
901
902 #[test]
903 fn test_request_id_new() {
904 let id1 = RequestId::new();
905 let id2 = RequestId::new();
906
907 assert_ne!(id1.as_str(), id2.as_str());
909 assert_eq!(id1.as_str().len(), 36);
911 }
912
913 #[test]
914 fn test_request_id_default() {
915 let id = RequestId::default();
916 assert_eq!(id.as_str().len(), 36);
917 }
918
919 #[test]
920 fn test_security_config_default() {
921 let config = SecurityConfig::default();
922
923 assert!(config.enable_hsts);
924 assert_eq!(config.hsts_max_age, 31536000);
925 assert!(config.enable_frame_options);
926 assert!(config.enable_content_type_options);
927 assert!(config.enable_xss_protection);
928 assert!(config.csp.is_some());
929 }
930
931 #[test]
932 fn test_frame_options_variants() {
933 let deny = FrameOptions::Deny;
934 let same_origin = FrameOptions::SameOrigin;
935 let allow_from = FrameOptions::AllowFrom("https://example.com".to_string());
936
937 assert!(format!("{:?}", deny).contains("Deny"));
939 assert!(format!("{:?}", same_origin).contains("SameOrigin"));
940 assert!(format!("{:?}", allow_from).contains("AllowFrom"));
941 }
942
943 #[test]
944 fn test_auth_error_from_validation_error() {
945 let error = AllSourceError::ValidationError("test error".to_string());
946 let auth_error = AuthError::from(error);
947 assert!(format!("{:?}", auth_error).contains("ValidationError"));
948 }
949
950 #[test]
951 fn test_rate_limit_error_display() {
952 let error = RateLimitError::RateLimitExceeded {
953 retry_after: 60,
954 limit: 100,
955 };
956 assert!(format!("{:?}", error).contains("RateLimitExceeded"));
957
958 let unauth_error = RateLimitError::Unauthorized;
959 assert!(format!("{:?}", unauth_error).contains("Unauthorized"));
960 }
961
962 #[test]
963 fn test_tenant_error_variants() {
964 let errors = vec![
965 TenantError::Unauthorized,
966 TenantError::InvalidTenant,
967 TenantError::TenantNotFound,
968 TenantError::TenantInactive,
969 TenantError::RepositoryError("test".to_string()),
970 ];
971
972 for error in errors {
973 let _ = format!("{:?}", error);
975 }
976 }
977
978 #[test]
979 fn test_ip_filter_error_variants() {
980 let errors = vec![
981 IpFilterError::NoIpAddress,
982 IpFilterError::Blocked {
983 reason: "blocked".to_string(),
984 },
985 ];
986
987 for error in errors {
988 let _ = format!("{:?}", error);
989 }
990 }
991
992 #[test]
993 fn test_security_state_clone() {
994 let config = SecurityConfig::default();
995 let state = SecurityState {
996 config: config.clone(),
997 };
998 let cloned = state.clone();
999 assert_eq!(cloned.config.hsts_max_age, config.hsts_max_age);
1000 }
1001
1002 #[test]
1003 fn test_auth_state_clone() {
1004 let auth_manager = Arc::new(AuthManager::new("test-secret"));
1005 let state = AuthState { auth_manager };
1006 let cloned = state.clone();
1007 assert!(Arc::ptr_eq(&state.auth_manager, &cloned.auth_manager));
1008 }
1009
1010 #[test]
1011 fn test_rate_limit_state_clone() {
1012 use crate::infrastructure::security::rate_limit::RateLimitConfig;
1013 let rate_limiter = Arc::new(RateLimiter::new(RateLimitConfig::free_tier()));
1014 let state = RateLimitState { rate_limiter };
1015 let cloned = state.clone();
1016 assert!(Arc::ptr_eq(&state.rate_limiter, &cloned.rate_limiter));
1017 }
1018
1019 #[test]
1020 fn test_auth_skip_paths_contains_expected() {
1021 assert!(AUTH_SKIP_PATHS.contains(&"/health"));
1023 assert!(AUTH_SKIP_PATHS.contains(&"/metrics"));
1024 assert!(AUTH_SKIP_PATHS.contains(&"/api/v1/auth/register"));
1025 assert!(AUTH_SKIP_PATHS.contains(&"/api/v1/auth/login"));
1026
1027 assert!(!AUTH_SKIP_PATHS.contains(&"/api/v1/events"));
1029 assert!(!AUTH_SKIP_PATHS.contains(&"/api/v1/auth/me"));
1030 }
1031
1032 #[test]
1033 fn test_dev_mode_auth_context() {
1034 let ctx = dev_mode_auth_context();
1035
1036 assert_eq!(ctx.tenant_id(), "dev-tenant");
1038 assert_eq!(ctx.user_id(), "dev-user");
1039 assert!(ctx.require_permission(Permission::Admin).is_ok());
1040 assert!(ctx.require_permission(Permission::Read).is_ok());
1041 assert!(ctx.require_permission(Permission::Write).is_ok());
1042 }
1043
1044 #[test]
1045 fn test_dev_mode_disabled_by_default() {
1046 let env_value = std::env::var("ALLSOURCE_DEV_MODE").unwrap_or_default();
1050 if env_value.is_empty() {
1051 assert!(!is_dev_mode());
1052 }
1053 }
1054}