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).is_ok_and(|v| matches!(v.to_lowercase().as_str(), "true" | "1" | "yes"))
86}
87
88static DEV_MODE_ENABLED: LazyLock<bool> = LazyLock::new(|| {
89 let via_dev = env_flag_enabled("ALLSOURCE_DEV_MODE");
90 let via_auth_off = env_flag_enabled("ALLSOURCE_AUTH_DISABLED");
91 let enabled = via_dev || via_auth_off;
92 if enabled {
93 let source = if via_auth_off && via_dev {
94 "ALLSOURCE_DEV_MODE + ALLSOURCE_AUTH_DISABLED"
95 } else if via_auth_off {
96 "ALLSOURCE_AUTH_DISABLED"
97 } else {
98 "ALLSOURCE_DEV_MODE"
99 };
100 tracing::warn!(
101 "⚠️ Auth disabled via {source} — all requests run as admin with no rate limits. DO NOT use in production."
102 );
103 }
104 enabled
105});
106
107#[inline]
109pub fn is_dev_mode() -> bool {
110 *DEV_MODE_ENABLED
111}
112
113fn dev_mode_auth_context() -> AuthContext {
115 AuthContext {
116 claims: Claims::new(
117 "dev-user".to_string(),
118 "dev-tenant".to_string(),
119 Role::Admin,
120 chrono::Duration::hours(24),
121 ),
122 }
123}
124
125#[derive(Clone)]
127pub struct AuthState {
128 pub auth_manager: Arc<AuthManager>,
129}
130
131#[derive(Clone)]
133pub struct RateLimitState {
134 pub rate_limiter: Arc<RateLimiter>,
135}
136
137#[derive(Debug, Clone)]
139pub struct AuthContext {
140 pub claims: Claims,
141}
142
143impl AuthContext {
144 pub fn require_permission(&self, permission: Permission) -> Result<(), AllSourceError> {
146 if self.claims.has_permission(permission) {
147 Ok(())
148 } else {
149 Err(AllSourceError::ValidationError(
150 "Insufficient permissions".to_string(),
151 ))
152 }
153 }
154
155 pub fn tenant_id(&self) -> &str {
157 &self.claims.tenant_id
158 }
159
160 pub fn user_id(&self) -> &str {
162 &self.claims.sub
163 }
164}
165
166fn extract_token(headers: &HeaderMap) -> Result<String, AllSourceError> {
168 let auth_header = if let Some(val) = headers.get("authorization") {
171 val.to_str()
172 .map_err(|_| {
173 AllSourceError::ValidationError("Invalid authorization header".to_string())
174 })?
175 .to_string()
176 } else if let Some(val) = headers.get("x-api-key") {
177 val.to_str()
178 .map_err(|_| AllSourceError::ValidationError("Invalid X-API-Key header".to_string()))?
179 .to_string()
180 } else {
181 return Err(AllSourceError::ValidationError(
182 "Missing authorization header".to_string(),
183 ));
184 };
185
186 let token = if auth_header.starts_with("Bearer ") {
188 auth_header.trim_start_matches("Bearer ").trim()
189 } else if auth_header.starts_with("bearer ") {
190 auth_header.trim_start_matches("bearer ").trim()
191 } else {
192 auth_header.trim()
193 };
194
195 if token.is_empty() {
196 return Err(AllSourceError::ValidationError(
197 "Empty authorization token".to_string(),
198 ));
199 }
200
201 Ok(token.to_string())
202}
203
204#[inline]
212pub fn is_admin_only_path(path: &str, method: &str) -> bool {
213 (path == "/api/v1/auth/api-keys" && method == "POST") || path.starts_with("/api/v1/tenants")
214}
215
216pub async fn auth_middleware(
218 State(auth_state): State<AuthState>,
219 mut request: Request,
220 next: Next,
221) -> Result<Response, AuthError> {
222 let path = request.uri().path();
224 if should_skip_auth(path) {
225 return Ok(next.run(request).await);
226 }
227
228 if is_dev_mode() {
232 let headers = request.headers();
233 let auth_ctx = match extract_token(headers) {
234 Ok(token) => {
235 let claims = if token.starts_with("ask_") {
236 auth_state.auth_manager.validate_api_key(&token).ok()
237 } else {
238 auth_state.auth_manager.validate_token(&token).ok()
239 };
240 claims.map_or_else(dev_mode_auth_context, |c| AuthContext { claims: c })
241 }
242 Err(_) => dev_mode_auth_context(),
243 };
244 request.extensions_mut().insert(auth_ctx);
245 return Ok(next.run(request).await);
246 }
247
248 let headers = request.headers();
249
250 let token = extract_token(headers)?;
252
253 let claims = if token.starts_with("ask_") {
254 auth_state.auth_manager.validate_api_key(&token)?
256 } else {
257 auth_state.auth_manager.validate_token(&token)?
259 };
260
261 let auth_ctx = AuthContext { claims };
262
263 let path = request.uri().path();
267 let method = request.method().as_str();
268 let is_admin_only_path = is_admin_only_path(path, method);
269
270 if is_admin_only_path {
271 auth_ctx
272 .require_permission(Permission::Admin)
273 .map_err(|_| {
274 AuthError(AllSourceError::ValidationError(
275 "Admin permission required".to_string(),
276 ))
277 })?;
278 }
279
280 request.extensions_mut().insert(auth_ctx);
282
283 Ok(next.run(request).await)
284}
285
286pub async fn optional_auth_middleware(
288 State(auth_state): State<AuthState>,
289 mut request: Request,
290 next: Next,
291) -> Response {
292 let headers = request.headers();
293
294 if let Ok(token) = extract_token(headers) {
295 let claims = if token.starts_with("ask_") {
297 auth_state.auth_manager.validate_api_key(&token).ok()
298 } else {
299 auth_state.auth_manager.validate_token(&token).ok()
300 };
301
302 if let Some(claims) = claims {
303 request.extensions_mut().insert(AuthContext { claims });
304 }
305 }
306
307 next.run(request).await
308}
309
310#[derive(Debug)]
312pub struct AuthError(AllSourceError);
313
314impl From<AllSourceError> for AuthError {
315 fn from(err: AllSourceError) -> Self {
316 AuthError(err)
317 }
318}
319
320impl IntoResponse for AuthError {
321 fn into_response(self) -> Response {
322 let (status, message) = match self.0 {
323 AllSourceError::ValidationError(msg) => (StatusCode::UNAUTHORIZED, msg),
324 _ => (
325 StatusCode::INTERNAL_SERVER_ERROR,
326 "Internal server error".to_string(),
327 ),
328 };
329
330 (status, message).into_response()
331 }
332}
333
334pub struct Authenticated(pub AuthContext);
336
337impl<S> axum::extract::FromRequestParts<S> for Authenticated
338where
339 S: Send + Sync,
340{
341 type Rejection = (StatusCode, &'static str);
342
343 #[allow(unknown_lints, clippy::unused_async_trait_impl)]
348 async fn from_request_parts(
349 parts: &mut axum::http::request::Parts,
350 _state: &S,
351 ) -> Result<Self, Self::Rejection> {
352 parts
353 .extensions
354 .get::<AuthContext>()
355 .cloned()
356 .map(Authenticated)
357 .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))
358 }
359}
360
361pub struct OptionalAuth(pub Option<AuthContext>);
364
365impl<S> axum::extract::FromRequestParts<S> for OptionalAuth
366where
367 S: Send + Sync,
368{
369 type Rejection = std::convert::Infallible;
370
371 #[allow(unknown_lints, clippy::unused_async_trait_impl)]
373 async fn from_request_parts(
374 parts: &mut axum::http::request::Parts,
375 _state: &S,
376 ) -> Result<Self, Self::Rejection> {
377 Ok(OptionalAuth(parts.extensions.get::<AuthContext>().cloned()))
378 }
379}
380
381pub struct Admin(pub AuthContext);
383
384impl<S> axum::extract::FromRequestParts<S> for Admin
385where
386 S: Send + Sync,
387{
388 type Rejection = (StatusCode, &'static str);
389
390 #[allow(unknown_lints, clippy::unused_async_trait_impl)]
392 async fn from_request_parts(
393 parts: &mut axum::http::request::Parts,
394 _state: &S,
395 ) -> Result<Self, Self::Rejection> {
396 let auth_ctx = parts
397 .extensions
398 .get::<AuthContext>()
399 .cloned()
400 .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))?;
401
402 auth_ctx
403 .require_permission(Permission::Admin)
404 .map_err(|_| (StatusCode::FORBIDDEN, "Admin permission required"))?;
405
406 Ok(Admin(auth_ctx))
407 }
408}
409
410pub async fn rate_limit_middleware(
413 State(rate_limit_state): State<RateLimitState>,
414 request: Request,
415 next: Next,
416) -> Result<Response, RateLimitError> {
417 let path = request.uri().path();
419 if should_skip_auth(path) {
420 return Ok(next.run(request).await);
421 }
422
423 if is_dev_mode() {
425 return Ok(next.run(request).await);
426 }
427
428 let auth_ctx = request
430 .extensions()
431 .get::<AuthContext>()
432 .ok_or(RateLimitError::Unauthorized)?;
433
434 let result = rate_limit_state
436 .rate_limiter
437 .check_rate_limit(auth_ctx.tenant_id());
438
439 if !result.allowed {
440 return Err(RateLimitError::RateLimitExceeded {
441 retry_after: result.retry_after.unwrap_or_default().as_secs(),
442 limit: result.limit,
443 });
444 }
445
446 let mut response = next.run(request).await;
448 let headers = response.headers_mut();
449 headers.insert(
450 "X-RateLimit-Limit",
451 result.limit.to_string().parse().unwrap(),
452 );
453 headers.insert(
454 "X-RateLimit-Remaining",
455 result.remaining.to_string().parse().unwrap(),
456 );
457
458 Ok(response)
459}
460
461#[derive(Debug)]
463pub enum RateLimitError {
464 RateLimitExceeded { retry_after: u64, limit: u32 },
465 Unauthorized,
466}
467
468impl IntoResponse for RateLimitError {
469 fn into_response(self) -> Response {
470 match self {
471 RateLimitError::RateLimitExceeded { retry_after, limit } => {
472 let mut response = (
473 StatusCode::TOO_MANY_REQUESTS,
474 format!("Rate limit exceeded. Limit: {limit} requests/min"),
475 )
476 .into_response();
477
478 if retry_after > 0 {
479 response
480 .headers_mut()
481 .insert("Retry-After", retry_after.to_string().parse().unwrap());
482 }
483
484 response
485 }
486 RateLimitError::Unauthorized => (
487 StatusCode::UNAUTHORIZED,
488 "Authentication required for rate limiting",
489 )
490 .into_response(),
491 }
492 }
493}
494
495#[macro_export]
497macro_rules! require_permission {
498 ($auth:expr, $perm:expr) => {
499 $auth.0.require_permission($perm).map_err(|_| {
500 (
501 axum::http::StatusCode::FORBIDDEN,
502 "Insufficient permissions",
503 )
504 })?
505 };
506}
507
508use crate::domain::{entities::Tenant, repositories::TenantRepository, value_objects::TenantId};
513
514#[derive(Clone)]
516pub struct TenantState<R: TenantRepository> {
517 pub tenant_repository: Arc<R>,
518}
519
520#[derive(Debug, Clone)]
525pub struct TenantContext {
526 pub tenant: Tenant,
527}
528
529impl TenantContext {
530 pub fn tenant_id(&self) -> &TenantId {
532 self.tenant.id()
533 }
534
535 pub fn is_active(&self) -> bool {
537 self.tenant.is_active()
538 }
539}
540
541pub async fn tenant_isolation_middleware<R: TenantRepository + 'static>(
555 State(tenant_state): State<TenantState<R>>,
556 mut request: Request,
557 next: Next,
558) -> Result<Response, TenantError> {
559 let auth_ctx = request
561 .extensions()
562 .get::<AuthContext>()
563 .ok_or(TenantError::Unauthorized)?
564 .clone();
565
566 let tenant_id =
568 TenantId::new(auth_ctx.tenant_id().to_string()).map_err(|_| TenantError::InvalidTenant)?;
569
570 let tenant = tenant_state
572 .tenant_repository
573 .find_by_id(&tenant_id)
574 .await
575 .map_err(|e| TenantError::RepositoryError(e.to_string()))?
576 .ok_or(TenantError::TenantNotFound)?;
577
578 if !tenant.is_active() {
580 return Err(TenantError::TenantInactive);
581 }
582
583 request.extensions_mut().insert(TenantContext { tenant });
585
586 Ok(next.run(request).await)
588}
589
590#[derive(Debug)]
592pub enum TenantError {
593 Unauthorized,
594 InvalidTenant,
595 TenantNotFound,
596 TenantInactive,
597 RepositoryError(String),
598}
599
600impl IntoResponse for TenantError {
601 fn into_response(self) -> Response {
602 let (status, message) = match self {
603 TenantError::Unauthorized => (
604 StatusCode::UNAUTHORIZED,
605 "Authentication required for tenant access",
606 ),
607 TenantError::InvalidTenant => (StatusCode::BAD_REQUEST, "Invalid tenant identifier"),
608 TenantError::TenantNotFound => (StatusCode::NOT_FOUND, "Tenant not found"),
609 TenantError::TenantInactive => (StatusCode::FORBIDDEN, "Tenant is inactive"),
610 TenantError::RepositoryError(_) => (
611 StatusCode::INTERNAL_SERVER_ERROR,
612 "Failed to validate tenant",
613 ),
614 };
615
616 (status, message).into_response()
617 }
618}
619
620use uuid::Uuid;
625
626#[derive(Debug, Clone)]
628pub struct RequestId(pub String);
629
630impl Default for RequestId {
631 fn default() -> Self {
632 Self::new()
633 }
634}
635
636impl RequestId {
637 pub fn new() -> Self {
639 Self(Uuid::new_v4().to_string())
640 }
641
642 pub fn as_str(&self) -> &str {
644 &self.0
645 }
646}
647
648pub async fn request_id_middleware(mut request: Request, next: Next) -> Response {
663 let request_id = request
665 .headers()
666 .get("x-request-id")
667 .and_then(|v| v.to_str().ok())
668 .map_or_else(RequestId::new, |s| RequestId(s.to_string()));
669
670 request.extensions_mut().insert(request_id.clone());
672
673 let mut response = next.run(request).await;
675
676 response
678 .headers_mut()
679 .insert("x-request-id", request_id.0.parse().unwrap());
680
681 response
682}
683
684#[derive(Debug, Clone)]
690pub struct SecurityConfig {
691 pub enable_hsts: bool,
693 pub hsts_max_age: u32,
695 pub enable_frame_options: bool,
697 pub frame_options: FrameOptions,
699 pub enable_content_type_options: bool,
701 pub enable_xss_protection: bool,
703 pub csp: Option<String>,
705 pub cors_origins: Vec<String>,
707 pub cors_methods: Vec<String>,
709 pub cors_headers: Vec<String>,
711 pub cors_max_age: u32,
713}
714
715#[derive(Debug, Clone)]
716pub enum FrameOptions {
717 Deny,
718 SameOrigin,
719 AllowFrom(String),
720}
721
722impl Default for SecurityConfig {
723 fn default() -> Self {
724 Self {
725 enable_hsts: true,
726 hsts_max_age: 31_536_000, enable_frame_options: true,
728 frame_options: FrameOptions::Deny,
729 enable_content_type_options: true,
730 enable_xss_protection: true,
731 csp: Some("default-src 'self'".to_string()),
732 cors_origins: vec!["*".to_string()],
733 cors_methods: vec![
734 "GET".to_string(),
735 "POST".to_string(),
736 "PUT".to_string(),
737 "DELETE".to_string(),
738 ],
739 cors_headers: vec!["Content-Type".to_string(), "Authorization".to_string()],
740 cors_max_age: 3600,
741 }
742 }
743}
744
745#[derive(Clone)]
746pub struct SecurityState {
747 pub config: SecurityConfig,
748}
749
750pub async fn security_headers_middleware(
768 State(security_state): State<SecurityState>,
769 request: Request,
770 next: Next,
771) -> Response {
772 let mut response = next.run(request).await;
773 let headers = response.headers_mut();
774 let config = &security_state.config;
775
776 if config.enable_hsts {
778 headers.insert(
779 "strict-transport-security",
780 format!("max-age={}", config.hsts_max_age).parse().unwrap(),
781 );
782 }
783
784 if config.enable_frame_options {
786 let value = match &config.frame_options {
787 FrameOptions::Deny => "DENY",
788 FrameOptions::SameOrigin => "SAMEORIGIN",
789 FrameOptions::AllowFrom(origin) => origin,
790 };
791 headers.insert("x-frame-options", value.parse().unwrap());
792 }
793
794 if config.enable_content_type_options {
796 headers.insert("x-content-type-options", "nosniff".parse().unwrap());
797 }
798
799 if config.enable_xss_protection {
801 headers.insert("x-xss-protection", "1; mode=block".parse().unwrap());
802 }
803
804 if let Some(csp) = &config.csp {
806 headers.insert("content-security-policy", csp.parse().unwrap());
807 }
808
809 headers.insert(
811 "access-control-allow-origin",
812 config.cors_origins.join(", ").parse().unwrap(),
813 );
814 headers.insert(
815 "access-control-allow-methods",
816 config.cors_methods.join(", ").parse().unwrap(),
817 );
818 headers.insert(
819 "access-control-allow-headers",
820 config.cors_headers.join(", ").parse().unwrap(),
821 );
822 headers.insert(
823 "access-control-max-age",
824 config.cors_max_age.to_string().parse().unwrap(),
825 );
826
827 response
828}
829
830use crate::infrastructure::security::IpFilter;
835use std::net::SocketAddr;
836
837#[derive(Clone)]
838pub struct IpFilterState {
839 pub ip_filter: Arc<IpFilter>,
840}
841
842pub async fn ip_filter_middleware(
854 State(ip_filter_state): State<IpFilterState>,
855 request: Request,
856 next: Next,
857) -> Result<Response, IpFilterError> {
858 let client_ip = request
860 .extensions()
861 .get::<axum::extract::ConnectInfo<SocketAddr>>()
862 .map(|connect_info| connect_info.0.ip())
863 .ok_or(IpFilterError::NoIpAddress)?;
864
865 let result = if let Some(tenant_ctx) = request.extensions().get::<TenantContext>() {
867 ip_filter_state
869 .ip_filter
870 .is_allowed_for_tenant(tenant_ctx.tenant_id(), &client_ip)
871 } else {
872 ip_filter_state.ip_filter.is_allowed(&client_ip)
874 };
875
876 if !result.allowed {
878 return Err(IpFilterError::Blocked {
879 reason: result.reason,
880 });
881 }
882
883 Ok(next.run(request).await)
885}
886
887#[derive(Debug)]
889pub enum IpFilterError {
890 NoIpAddress,
891 Blocked { reason: String },
892}
893
894impl IntoResponse for IpFilterError {
895 fn into_response(self) -> Response {
896 match self {
897 IpFilterError::NoIpAddress => (
898 StatusCode::BAD_REQUEST,
899 "Unable to determine client IP address",
900 )
901 .into_response(),
902 IpFilterError::Blocked { reason } => {
903 (StatusCode::FORBIDDEN, format!("Access denied: {reason}")).into_response()
904 }
905 }
906 }
907}
908
909#[cfg(test)]
910mod tests {
911 use super::*;
912 use crate::infrastructure::security::auth::Role;
913
914 #[test]
915 fn test_extract_bearer_token() {
916 let mut headers = HeaderMap::new();
917 headers.insert("authorization", "Bearer test_token_123".parse().unwrap());
918
919 let token = extract_token(&headers).unwrap();
920 assert_eq!(token, "test_token_123");
921 }
922
923 #[test]
924 fn test_extract_lowercase_bearer() {
925 let mut headers = HeaderMap::new();
926 headers.insert("authorization", "bearer test_token_123".parse().unwrap());
927
928 let token = extract_token(&headers).unwrap();
929 assert_eq!(token, "test_token_123");
930 }
931
932 #[test]
933 fn test_extract_plain_token() {
934 let mut headers = HeaderMap::new();
935 headers.insert("authorization", "test_token_123".parse().unwrap());
936
937 let token = extract_token(&headers).unwrap();
938 assert_eq!(token, "test_token_123");
939 }
940
941 #[test]
942 fn test_missing_auth_header() {
943 let headers = HeaderMap::new();
944 assert!(extract_token(&headers).is_err());
945 }
946
947 #[test]
948 fn test_empty_auth_header() {
949 let mut headers = HeaderMap::new();
950 headers.insert("authorization", "".parse().unwrap());
951 assert!(extract_token(&headers).is_err());
952 }
953
954 #[test]
955 fn test_bearer_with_empty_token() {
956 let mut headers = HeaderMap::new();
957 headers.insert("authorization", "Bearer ".parse().unwrap());
958 assert!(extract_token(&headers).is_err());
959 }
960
961 #[test]
966 fn test_service_account_blocked_on_admin_paths() {
967 let claims = Claims::new(
970 "agent-key".to_string(),
971 "tenant1".to_string(),
972 Role::ServiceAccount,
973 chrono::Duration::hours(1),
974 );
975 let ctx = AuthContext { claims };
976 assert!(
977 ctx.require_permission(Permission::Admin).is_err(),
978 "ServiceAccount must not have Admin permission"
979 );
980 assert!(ctx.require_permission(Permission::Read).is_ok());
982 assert!(ctx.require_permission(Permission::Write).is_ok());
983 }
984
985 #[test]
986 fn test_admin_role_passes_admin_paths() {
987 let claims = Claims::new(
989 "admin-user".to_string(),
990 "tenant1".to_string(),
991 Role::Admin,
992 chrono::Duration::hours(1),
993 );
994 let ctx = AuthContext { claims };
995 assert!(
996 ctx.require_permission(Permission::Admin).is_ok(),
997 "Admin must have Admin permission"
998 );
999 }
1000
1001 #[test]
1002 fn test_developer_blocked_on_admin_paths() {
1003 let claims = Claims::new(
1005 "dev-user".to_string(),
1006 "tenant1".to_string(),
1007 Role::Developer,
1008 chrono::Duration::hours(1),
1009 );
1010 let ctx = AuthContext { claims };
1011 assert!(
1012 ctx.require_permission(Permission::Admin).is_err(),
1013 "Developer must not have Admin permission"
1014 );
1015 }
1016
1017 #[test]
1018 fn test_readonly_blocked_on_admin_paths() {
1019 let claims = Claims::new(
1020 "ro-user".to_string(),
1021 "tenant1".to_string(),
1022 Role::ReadOnly,
1023 chrono::Duration::hours(1),
1024 );
1025 let ctx = AuthContext { claims };
1026 assert!(ctx.require_permission(Permission::Admin).is_err());
1027 assert!(ctx.require_permission(Permission::Read).is_ok());
1028 assert!(ctx.require_permission(Permission::Write).is_err());
1029 }
1030
1031 #[test]
1032 fn test_is_admin_only_path_api_keys_create() {
1033 assert!(is_admin_only_path("/api/v1/auth/api-keys", "POST"));
1035 assert!(!is_admin_only_path("/api/v1/auth/api-keys", "GET"));
1037 assert!(!is_admin_only_path("/api/v1/auth/api-keys", "DELETE"));
1038 }
1039
1040 #[test]
1041 fn test_is_admin_only_path_tenants() {
1042 assert!(is_admin_only_path("/api/v1/tenants", "GET"));
1044 assert!(is_admin_only_path("/api/v1/tenants", "POST"));
1045 assert!(is_admin_only_path("/api/v1/tenants/some-id", "DELETE"));
1046 assert!(is_admin_only_path("/api/v1/tenants/some-id/config", "PUT"));
1047 }
1048
1049 #[test]
1050 fn test_is_admin_only_path_normal_paths() {
1051 assert!(!is_admin_only_path("/api/v1/events", "POST"));
1053 assert!(!is_admin_only_path("/api/v1/events/query", "GET"));
1054 assert!(!is_admin_only_path("/api/v1/auth/me", "GET"));
1055 assert!(!is_admin_only_path("/api/v1/auth/login", "POST"));
1056 assert!(!is_admin_only_path("/api/v1/schemas", "GET"));
1057 }
1058
1059 #[test]
1060 fn test_auth_context_permissions() {
1061 let claims = Claims::new(
1062 "user1".to_string(),
1063 "tenant1".to_string(),
1064 Role::Developer,
1065 chrono::Duration::hours(1),
1066 );
1067
1068 let ctx = AuthContext { claims };
1069
1070 assert!(ctx.require_permission(Permission::Read).is_ok());
1071 assert!(ctx.require_permission(Permission::Write).is_ok());
1072 assert!(ctx.require_permission(Permission::Admin).is_err());
1073 }
1074
1075 #[test]
1076 fn test_auth_context_admin_permissions() {
1077 let claims = Claims::new(
1078 "admin1".to_string(),
1079 "tenant1".to_string(),
1080 Role::Admin,
1081 chrono::Duration::hours(1),
1082 );
1083
1084 let ctx = AuthContext { claims };
1085
1086 assert!(ctx.require_permission(Permission::Read).is_ok());
1087 assert!(ctx.require_permission(Permission::Write).is_ok());
1088 assert!(ctx.require_permission(Permission::Admin).is_ok());
1089 }
1090
1091 #[test]
1092 fn test_auth_context_readonly_permissions() {
1093 let claims = Claims::new(
1094 "readonly1".to_string(),
1095 "tenant1".to_string(),
1096 Role::ReadOnly,
1097 chrono::Duration::hours(1),
1098 );
1099
1100 let ctx = AuthContext { claims };
1101
1102 assert!(ctx.require_permission(Permission::Read).is_ok());
1103 assert!(ctx.require_permission(Permission::Write).is_err());
1104 assert!(ctx.require_permission(Permission::Admin).is_err());
1105 }
1106
1107 #[test]
1108 fn test_auth_context_tenant_id() {
1109 let claims = Claims::new(
1110 "user1".to_string(),
1111 "my-tenant".to_string(),
1112 Role::Developer,
1113 chrono::Duration::hours(1),
1114 );
1115
1116 let ctx = AuthContext { claims };
1117 assert_eq!(ctx.tenant_id(), "my-tenant");
1118 }
1119
1120 #[test]
1121 fn test_auth_context_user_id() {
1122 let claims = Claims::new(
1123 "my-user".to_string(),
1124 "tenant1".to_string(),
1125 Role::Developer,
1126 chrono::Duration::hours(1),
1127 );
1128
1129 let ctx = AuthContext { claims };
1130 assert_eq!(ctx.user_id(), "my-user");
1131 }
1132
1133 #[test]
1134 fn test_request_id_new() {
1135 let id1 = RequestId::new();
1136 let id2 = RequestId::new();
1137
1138 assert_ne!(id1.as_str(), id2.as_str());
1140 assert_eq!(id1.as_str().len(), 36);
1142 }
1143
1144 #[test]
1145 fn test_request_id_default() {
1146 let id = RequestId::default();
1147 assert_eq!(id.as_str().len(), 36);
1148 }
1149
1150 #[test]
1151 fn test_security_config_default() {
1152 let config = SecurityConfig::default();
1153
1154 assert!(config.enable_hsts);
1155 assert_eq!(config.hsts_max_age, 31536000);
1156 assert!(config.enable_frame_options);
1157 assert!(config.enable_content_type_options);
1158 assert!(config.enable_xss_protection);
1159 assert!(config.csp.is_some());
1160 }
1161
1162 #[test]
1163 fn test_frame_options_variants() {
1164 let deny = FrameOptions::Deny;
1165 let same_origin = FrameOptions::SameOrigin;
1166 let allow_from = FrameOptions::AllowFrom("https://example.com".to_string());
1167
1168 assert!(format!("{deny:?}").contains("Deny"));
1170 assert!(format!("{same_origin:?}").contains("SameOrigin"));
1171 assert!(format!("{allow_from:?}").contains("AllowFrom"));
1172 }
1173
1174 #[test]
1175 fn test_auth_error_from_validation_error() {
1176 let error = AllSourceError::ValidationError("test error".to_string());
1177 let auth_error = AuthError::from(error);
1178 assert!(format!("{auth_error:?}").contains("ValidationError"));
1179 }
1180
1181 #[test]
1182 fn test_rate_limit_error_display() {
1183 let error = RateLimitError::RateLimitExceeded {
1184 retry_after: 60,
1185 limit: 100,
1186 };
1187 assert!(format!("{error:?}").contains("RateLimitExceeded"));
1188
1189 let unauth_error = RateLimitError::Unauthorized;
1190 assert!(format!("{unauth_error:?}").contains("Unauthorized"));
1191 }
1192
1193 #[test]
1194 fn test_tenant_error_variants() {
1195 let errors = vec![
1196 TenantError::Unauthorized,
1197 TenantError::InvalidTenant,
1198 TenantError::TenantNotFound,
1199 TenantError::TenantInactive,
1200 TenantError::RepositoryError("test".to_string()),
1201 ];
1202
1203 for error in errors {
1204 let _ = format!("{error:?}");
1206 }
1207 }
1208
1209 #[test]
1210 fn test_ip_filter_error_variants() {
1211 let errors = vec![
1212 IpFilterError::NoIpAddress,
1213 IpFilterError::Blocked {
1214 reason: "blocked".to_string(),
1215 },
1216 ];
1217
1218 for error in errors {
1219 let _ = format!("{error:?}");
1220 }
1221 }
1222
1223 #[test]
1224 fn test_security_state_clone() {
1225 let config = SecurityConfig::default();
1226 let state = SecurityState {
1227 config: config.clone(),
1228 };
1229 let cloned = state.clone();
1230 assert_eq!(cloned.config.hsts_max_age, config.hsts_max_age);
1231 }
1232
1233 #[test]
1234 fn test_auth_state_clone() {
1235 let auth_manager = Arc::new(AuthManager::new("test-secret"));
1236 let state = AuthState { auth_manager };
1237 let cloned = state.clone();
1238 assert!(Arc::ptr_eq(&state.auth_manager, &cloned.auth_manager));
1239 }
1240
1241 #[test]
1242 fn test_rate_limit_state_clone() {
1243 use crate::infrastructure::security::rate_limit::RateLimitConfig;
1244 let rate_limiter = Arc::new(RateLimiter::new(RateLimitConfig::free_tier()));
1245 let state = RateLimitState { rate_limiter };
1246 let cloned = state.clone();
1247 assert!(Arc::ptr_eq(&state.rate_limiter, &cloned.rate_limiter));
1248 }
1249
1250 #[test]
1251 fn test_auth_skip_paths_contains_expected() {
1252 assert!(should_skip_auth("/health"));
1254 assert!(should_skip_auth("/metrics"));
1255 assert!(should_skip_auth("/api/v1/auth/register"));
1256 assert!(should_skip_auth("/api/v1/auth/login"));
1257 assert!(should_skip_auth("/api/v1/demo/seed"));
1258
1259 assert!(should_skip_auth("/internal/promote"));
1261 assert!(should_skip_auth("/internal/repoint"));
1262 assert!(should_skip_auth("/internal/anything"));
1263
1264 assert!(should_skip_auth("/api/v1/events"));
1267 assert!(should_skip_auth("/api/v1/events/query"));
1268 assert!(should_skip_auth("/api/v1/events/batch"));
1269 assert!(should_skip_auth("/api/v1/projections/foo/bar"));
1270 assert!(should_skip_auth("/api/v1/snapshots"));
1271
1272 assert!(!should_skip_auth("/api/v1/auth/me"));
1274 assert!(!should_skip_auth("/api/v1/auth/api-keys"));
1275 assert!(!should_skip_auth("/api/v1/tenants"));
1276 assert!(!should_skip_auth("/api/v1/tenants/abc"));
1277 }
1278
1279 #[test]
1280 fn test_dev_mode_auth_context() {
1281 let ctx = dev_mode_auth_context();
1282
1283 assert_eq!(ctx.tenant_id(), "dev-tenant");
1285 assert_eq!(ctx.user_id(), "dev-user");
1286 assert!(ctx.require_permission(Permission::Admin).is_ok());
1287 assert!(ctx.require_permission(Permission::Read).is_ok());
1288 assert!(ctx.require_permission(Permission::Write).is_ok());
1289 }
1290
1291 #[test]
1292 fn test_dev_mode_disabled_by_default() {
1293 let env_value = std::env::var("ALLSOURCE_DEV_MODE").unwrap_or_default();
1297 if env_value.is_empty() {
1298 assert!(!is_dev_mode());
1299 }
1300 }
1301}