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
38fn env_flag_enabled(name: &str) -> bool {
50 std::env::var(name)
51 .map(|v| matches!(v.to_lowercase().as_str(), "true" | "1" | "yes"))
52 .unwrap_or(false)
53}
54
55static DEV_MODE_ENABLED: LazyLock<bool> = LazyLock::new(|| {
56 let via_dev = env_flag_enabled("ALLSOURCE_DEV_MODE");
57 let via_auth_off = env_flag_enabled("ALLSOURCE_AUTH_DISABLED");
58 let enabled = via_dev || via_auth_off;
59 if enabled {
60 let source = if via_auth_off && via_dev {
61 "ALLSOURCE_DEV_MODE + ALLSOURCE_AUTH_DISABLED"
62 } else if via_auth_off {
63 "ALLSOURCE_AUTH_DISABLED"
64 } else {
65 "ALLSOURCE_DEV_MODE"
66 };
67 tracing::warn!(
68 "⚠️ Auth disabled via {source} — all requests run as admin with no rate limits. DO NOT use in production."
69 );
70 }
71 enabled
72});
73
74#[inline]
76pub fn is_dev_mode() -> bool {
77 *DEV_MODE_ENABLED
78}
79
80fn dev_mode_auth_context() -> AuthContext {
82 AuthContext {
83 claims: Claims::new(
84 "dev-user".to_string(),
85 "dev-tenant".to_string(),
86 Role::Admin,
87 chrono::Duration::hours(24),
88 ),
89 }
90}
91
92#[derive(Clone)]
94pub struct AuthState {
95 pub auth_manager: Arc<AuthManager>,
96}
97
98#[derive(Clone)]
100pub struct RateLimitState {
101 pub rate_limiter: Arc<RateLimiter>,
102}
103
104#[derive(Debug, Clone)]
106pub struct AuthContext {
107 pub claims: Claims,
108}
109
110impl AuthContext {
111 pub fn require_permission(&self, permission: Permission) -> Result<(), AllSourceError> {
113 if self.claims.has_permission(permission) {
114 Ok(())
115 } else {
116 Err(AllSourceError::ValidationError(
117 "Insufficient permissions".to_string(),
118 ))
119 }
120 }
121
122 pub fn tenant_id(&self) -> &str {
124 &self.claims.tenant_id
125 }
126
127 pub fn user_id(&self) -> &str {
129 &self.claims.sub
130 }
131}
132
133fn extract_token(headers: &HeaderMap) -> Result<String, AllSourceError> {
135 let auth_header = if let Some(val) = headers.get("authorization") {
138 val.to_str()
139 .map_err(|_| {
140 AllSourceError::ValidationError("Invalid authorization header".to_string())
141 })?
142 .to_string()
143 } else if let Some(val) = headers.get("x-api-key") {
144 val.to_str()
145 .map_err(|_| AllSourceError::ValidationError("Invalid X-API-Key header".to_string()))?
146 .to_string()
147 } else {
148 return Err(AllSourceError::ValidationError(
149 "Missing authorization header".to_string(),
150 ));
151 };
152
153 let token = if auth_header.starts_with("Bearer ") {
155 auth_header.trim_start_matches("Bearer ").trim()
156 } else if auth_header.starts_with("bearer ") {
157 auth_header.trim_start_matches("bearer ").trim()
158 } else {
159 auth_header.trim()
160 };
161
162 if token.is_empty() {
163 return Err(AllSourceError::ValidationError(
164 "Empty authorization token".to_string(),
165 ));
166 }
167
168 Ok(token.to_string())
169}
170
171#[inline]
179pub fn is_admin_only_path(path: &str, method: &str) -> bool {
180 (path == "/api/v1/auth/api-keys" && method == "POST") || path.starts_with("/api/v1/tenants")
181}
182
183pub async fn auth_middleware(
185 State(auth_state): State<AuthState>,
186 mut request: Request,
187 next: Next,
188) -> Result<Response, AuthError> {
189 let path = request.uri().path();
191 if should_skip_auth(path) {
192 return Ok(next.run(request).await);
193 }
194
195 if is_dev_mode() {
199 let headers = request.headers();
200 let auth_ctx = match extract_token(headers) {
201 Ok(token) => {
202 let claims = if token.starts_with("ask_") {
203 auth_state.auth_manager.validate_api_key(&token).ok()
204 } else {
205 auth_state.auth_manager.validate_token(&token).ok()
206 };
207 claims.map_or_else(dev_mode_auth_context, |c| AuthContext { claims: c })
208 }
209 Err(_) => dev_mode_auth_context(),
210 };
211 request.extensions_mut().insert(auth_ctx);
212 return Ok(next.run(request).await);
213 }
214
215 let headers = request.headers();
216
217 let token = extract_token(headers)?;
219
220 let claims = if token.starts_with("ask_") {
221 auth_state.auth_manager.validate_api_key(&token)?
223 } else {
224 auth_state.auth_manager.validate_token(&token)?
226 };
227
228 let auth_ctx = AuthContext { claims };
229
230 let path = request.uri().path();
234 let method = request.method().as_str();
235 let is_admin_only_path = is_admin_only_path(path, method);
236
237 if is_admin_only_path {
238 auth_ctx
239 .require_permission(Permission::Admin)
240 .map_err(|_| {
241 AuthError(AllSourceError::ValidationError(
242 "Admin permission required".to_string(),
243 ))
244 })?;
245 }
246
247 request.extensions_mut().insert(auth_ctx);
249
250 Ok(next.run(request).await)
251}
252
253pub async fn optional_auth_middleware(
255 State(auth_state): State<AuthState>,
256 mut request: Request,
257 next: Next,
258) -> Response {
259 let headers = request.headers();
260
261 if let Ok(token) = extract_token(headers) {
262 let claims = if token.starts_with("ask_") {
264 auth_state.auth_manager.validate_api_key(&token).ok()
265 } else {
266 auth_state.auth_manager.validate_token(&token).ok()
267 };
268
269 if let Some(claims) = claims {
270 request.extensions_mut().insert(AuthContext { claims });
271 }
272 }
273
274 next.run(request).await
275}
276
277#[derive(Debug)]
279pub struct AuthError(AllSourceError);
280
281impl From<AllSourceError> for AuthError {
282 fn from(err: AllSourceError) -> Self {
283 AuthError(err)
284 }
285}
286
287impl IntoResponse for AuthError {
288 fn into_response(self) -> Response {
289 let (status, message) = match self.0 {
290 AllSourceError::ValidationError(msg) => (StatusCode::UNAUTHORIZED, msg),
291 _ => (
292 StatusCode::INTERNAL_SERVER_ERROR,
293 "Internal server error".to_string(),
294 ),
295 };
296
297 (status, message).into_response()
298 }
299}
300
301pub struct Authenticated(pub AuthContext);
303
304impl<S> axum::extract::FromRequestParts<S> for Authenticated
305where
306 S: Send + Sync,
307{
308 type Rejection = (StatusCode, &'static str);
309
310 async fn from_request_parts(
311 parts: &mut axum::http::request::Parts,
312 _state: &S,
313 ) -> Result<Self, Self::Rejection> {
314 parts
315 .extensions
316 .get::<AuthContext>()
317 .cloned()
318 .map(Authenticated)
319 .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))
320 }
321}
322
323pub struct OptionalAuth(pub Option<AuthContext>);
326
327impl<S> axum::extract::FromRequestParts<S> for OptionalAuth
328where
329 S: Send + Sync,
330{
331 type Rejection = std::convert::Infallible;
332
333 async fn from_request_parts(
334 parts: &mut axum::http::request::Parts,
335 _state: &S,
336 ) -> Result<Self, Self::Rejection> {
337 Ok(OptionalAuth(parts.extensions.get::<AuthContext>().cloned()))
338 }
339}
340
341pub struct Admin(pub AuthContext);
343
344impl<S> axum::extract::FromRequestParts<S> for Admin
345where
346 S: Send + Sync,
347{
348 type Rejection = (StatusCode, &'static str);
349
350 async fn from_request_parts(
351 parts: &mut axum::http::request::Parts,
352 _state: &S,
353 ) -> Result<Self, Self::Rejection> {
354 let auth_ctx = parts
355 .extensions
356 .get::<AuthContext>()
357 .cloned()
358 .ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))?;
359
360 auth_ctx
361 .require_permission(Permission::Admin)
362 .map_err(|_| (StatusCode::FORBIDDEN, "Admin permission required"))?;
363
364 Ok(Admin(auth_ctx))
365 }
366}
367
368pub async fn rate_limit_middleware(
371 State(rate_limit_state): State<RateLimitState>,
372 request: Request,
373 next: Next,
374) -> Result<Response, RateLimitError> {
375 let path = request.uri().path();
377 if should_skip_auth(path) {
378 return Ok(next.run(request).await);
379 }
380
381 if is_dev_mode() {
383 return Ok(next.run(request).await);
384 }
385
386 let auth_ctx = request
388 .extensions()
389 .get::<AuthContext>()
390 .ok_or(RateLimitError::Unauthorized)?;
391
392 let result = rate_limit_state
394 .rate_limiter
395 .check_rate_limit(auth_ctx.tenant_id());
396
397 if !result.allowed {
398 return Err(RateLimitError::RateLimitExceeded {
399 retry_after: result.retry_after.unwrap_or_default().as_secs(),
400 limit: result.limit,
401 });
402 }
403
404 let mut response = next.run(request).await;
406 let headers = response.headers_mut();
407 headers.insert(
408 "X-RateLimit-Limit",
409 result.limit.to_string().parse().unwrap(),
410 );
411 headers.insert(
412 "X-RateLimit-Remaining",
413 result.remaining.to_string().parse().unwrap(),
414 );
415
416 Ok(response)
417}
418
419#[derive(Debug)]
421pub enum RateLimitError {
422 RateLimitExceeded { retry_after: u64, limit: u32 },
423 Unauthorized,
424}
425
426impl IntoResponse for RateLimitError {
427 fn into_response(self) -> Response {
428 match self {
429 RateLimitError::RateLimitExceeded { retry_after, limit } => {
430 let mut response = (
431 StatusCode::TOO_MANY_REQUESTS,
432 format!("Rate limit exceeded. Limit: {limit} requests/min"),
433 )
434 .into_response();
435
436 if retry_after > 0 {
437 response
438 .headers_mut()
439 .insert("Retry-After", retry_after.to_string().parse().unwrap());
440 }
441
442 response
443 }
444 RateLimitError::Unauthorized => (
445 StatusCode::UNAUTHORIZED,
446 "Authentication required for rate limiting",
447 )
448 .into_response(),
449 }
450 }
451}
452
453#[macro_export]
455macro_rules! require_permission {
456 ($auth:expr, $perm:expr) => {
457 $auth.0.require_permission($perm).map_err(|_| {
458 (
459 axum::http::StatusCode::FORBIDDEN,
460 "Insufficient permissions",
461 )
462 })?
463 };
464}
465
466use crate::domain::{entities::Tenant, repositories::TenantRepository, value_objects::TenantId};
471
472#[derive(Clone)]
474pub struct TenantState<R: TenantRepository> {
475 pub tenant_repository: Arc<R>,
476}
477
478#[derive(Debug, Clone)]
483pub struct TenantContext {
484 pub tenant: Tenant,
485}
486
487impl TenantContext {
488 pub fn tenant_id(&self) -> &TenantId {
490 self.tenant.id()
491 }
492
493 pub fn is_active(&self) -> bool {
495 self.tenant.is_active()
496 }
497}
498
499pub async fn tenant_isolation_middleware<R: TenantRepository + 'static>(
513 State(tenant_state): State<TenantState<R>>,
514 mut request: Request,
515 next: Next,
516) -> Result<Response, TenantError> {
517 let auth_ctx = request
519 .extensions()
520 .get::<AuthContext>()
521 .ok_or(TenantError::Unauthorized)?
522 .clone();
523
524 let tenant_id =
526 TenantId::new(auth_ctx.tenant_id().to_string()).map_err(|_| TenantError::InvalidTenant)?;
527
528 let tenant = tenant_state
530 .tenant_repository
531 .find_by_id(&tenant_id)
532 .await
533 .map_err(|e| TenantError::RepositoryError(e.to_string()))?
534 .ok_or(TenantError::TenantNotFound)?;
535
536 if !tenant.is_active() {
538 return Err(TenantError::TenantInactive);
539 }
540
541 request.extensions_mut().insert(TenantContext { tenant });
543
544 Ok(next.run(request).await)
546}
547
548#[derive(Debug)]
550pub enum TenantError {
551 Unauthorized,
552 InvalidTenant,
553 TenantNotFound,
554 TenantInactive,
555 RepositoryError(String),
556}
557
558impl IntoResponse for TenantError {
559 fn into_response(self) -> Response {
560 let (status, message) = match self {
561 TenantError::Unauthorized => (
562 StatusCode::UNAUTHORIZED,
563 "Authentication required for tenant access",
564 ),
565 TenantError::InvalidTenant => (StatusCode::BAD_REQUEST, "Invalid tenant identifier"),
566 TenantError::TenantNotFound => (StatusCode::NOT_FOUND, "Tenant not found"),
567 TenantError::TenantInactive => (StatusCode::FORBIDDEN, "Tenant is inactive"),
568 TenantError::RepositoryError(_) => (
569 StatusCode::INTERNAL_SERVER_ERROR,
570 "Failed to validate tenant",
571 ),
572 };
573
574 (status, message).into_response()
575 }
576}
577
578use uuid::Uuid;
583
584#[derive(Debug, Clone)]
586pub struct RequestId(pub String);
587
588impl Default for RequestId {
589 fn default() -> Self {
590 Self::new()
591 }
592}
593
594impl RequestId {
595 pub fn new() -> Self {
597 Self(Uuid::new_v4().to_string())
598 }
599
600 pub fn as_str(&self) -> &str {
602 &self.0
603 }
604}
605
606pub async fn request_id_middleware(mut request: Request, next: Next) -> Response {
621 let request_id = request
623 .headers()
624 .get("x-request-id")
625 .and_then(|v| v.to_str().ok())
626 .map_or_else(RequestId::new, |s| RequestId(s.to_string()));
627
628 request.extensions_mut().insert(request_id.clone());
630
631 let mut response = next.run(request).await;
633
634 response
636 .headers_mut()
637 .insert("x-request-id", request_id.0.parse().unwrap());
638
639 response
640}
641
642#[derive(Debug, Clone)]
648pub struct SecurityConfig {
649 pub enable_hsts: bool,
651 pub hsts_max_age: u32,
653 pub enable_frame_options: bool,
655 pub frame_options: FrameOptions,
657 pub enable_content_type_options: bool,
659 pub enable_xss_protection: bool,
661 pub csp: Option<String>,
663 pub cors_origins: Vec<String>,
665 pub cors_methods: Vec<String>,
667 pub cors_headers: Vec<String>,
669 pub cors_max_age: u32,
671}
672
673#[derive(Debug, Clone)]
674pub enum FrameOptions {
675 Deny,
676 SameOrigin,
677 AllowFrom(String),
678}
679
680impl Default for SecurityConfig {
681 fn default() -> Self {
682 Self {
683 enable_hsts: true,
684 hsts_max_age: 31_536_000, enable_frame_options: true,
686 frame_options: FrameOptions::Deny,
687 enable_content_type_options: true,
688 enable_xss_protection: true,
689 csp: Some("default-src 'self'".to_string()),
690 cors_origins: vec!["*".to_string()],
691 cors_methods: vec![
692 "GET".to_string(),
693 "POST".to_string(),
694 "PUT".to_string(),
695 "DELETE".to_string(),
696 ],
697 cors_headers: vec!["Content-Type".to_string(), "Authorization".to_string()],
698 cors_max_age: 3600,
699 }
700 }
701}
702
703#[derive(Clone)]
704pub struct SecurityState {
705 pub config: SecurityConfig,
706}
707
708pub async fn security_headers_middleware(
726 State(security_state): State<SecurityState>,
727 request: Request,
728 next: Next,
729) -> Response {
730 let mut response = next.run(request).await;
731 let headers = response.headers_mut();
732 let config = &security_state.config;
733
734 if config.enable_hsts {
736 headers.insert(
737 "strict-transport-security",
738 format!("max-age={}", config.hsts_max_age).parse().unwrap(),
739 );
740 }
741
742 if config.enable_frame_options {
744 let value = match &config.frame_options {
745 FrameOptions::Deny => "DENY",
746 FrameOptions::SameOrigin => "SAMEORIGIN",
747 FrameOptions::AllowFrom(origin) => origin,
748 };
749 headers.insert("x-frame-options", value.parse().unwrap());
750 }
751
752 if config.enable_content_type_options {
754 headers.insert("x-content-type-options", "nosniff".parse().unwrap());
755 }
756
757 if config.enable_xss_protection {
759 headers.insert("x-xss-protection", "1; mode=block".parse().unwrap());
760 }
761
762 if let Some(csp) = &config.csp {
764 headers.insert("content-security-policy", csp.parse().unwrap());
765 }
766
767 headers.insert(
769 "access-control-allow-origin",
770 config.cors_origins.join(", ").parse().unwrap(),
771 );
772 headers.insert(
773 "access-control-allow-methods",
774 config.cors_methods.join(", ").parse().unwrap(),
775 );
776 headers.insert(
777 "access-control-allow-headers",
778 config.cors_headers.join(", ").parse().unwrap(),
779 );
780 headers.insert(
781 "access-control-max-age",
782 config.cors_max_age.to_string().parse().unwrap(),
783 );
784
785 response
786}
787
788use crate::infrastructure::security::IpFilter;
793use std::net::SocketAddr;
794
795#[derive(Clone)]
796pub struct IpFilterState {
797 pub ip_filter: Arc<IpFilter>,
798}
799
800pub async fn ip_filter_middleware(
812 State(ip_filter_state): State<IpFilterState>,
813 request: Request,
814 next: Next,
815) -> Result<Response, IpFilterError> {
816 let client_ip = request
818 .extensions()
819 .get::<axum::extract::ConnectInfo<SocketAddr>>()
820 .map(|connect_info| connect_info.0.ip())
821 .ok_or(IpFilterError::NoIpAddress)?;
822
823 let result = if let Some(tenant_ctx) = request.extensions().get::<TenantContext>() {
825 ip_filter_state
827 .ip_filter
828 .is_allowed_for_tenant(tenant_ctx.tenant_id(), &client_ip)
829 } else {
830 ip_filter_state.ip_filter.is_allowed(&client_ip)
832 };
833
834 if !result.allowed {
836 return Err(IpFilterError::Blocked {
837 reason: result.reason,
838 });
839 }
840
841 Ok(next.run(request).await)
843}
844
845#[derive(Debug)]
847pub enum IpFilterError {
848 NoIpAddress,
849 Blocked { reason: String },
850}
851
852impl IntoResponse for IpFilterError {
853 fn into_response(self) -> Response {
854 match self {
855 IpFilterError::NoIpAddress => (
856 StatusCode::BAD_REQUEST,
857 "Unable to determine client IP address",
858 )
859 .into_response(),
860 IpFilterError::Blocked { reason } => {
861 (StatusCode::FORBIDDEN, format!("Access denied: {reason}")).into_response()
862 }
863 }
864 }
865}
866
867#[cfg(test)]
868mod tests {
869 use super::*;
870 use crate::infrastructure::security::auth::Role;
871
872 #[test]
873 fn test_extract_bearer_token() {
874 let mut headers = HeaderMap::new();
875 headers.insert("authorization", "Bearer test_token_123".parse().unwrap());
876
877 let token = extract_token(&headers).unwrap();
878 assert_eq!(token, "test_token_123");
879 }
880
881 #[test]
882 fn test_extract_lowercase_bearer() {
883 let mut headers = HeaderMap::new();
884 headers.insert("authorization", "bearer test_token_123".parse().unwrap());
885
886 let token = extract_token(&headers).unwrap();
887 assert_eq!(token, "test_token_123");
888 }
889
890 #[test]
891 fn test_extract_plain_token() {
892 let mut headers = HeaderMap::new();
893 headers.insert("authorization", "test_token_123".parse().unwrap());
894
895 let token = extract_token(&headers).unwrap();
896 assert_eq!(token, "test_token_123");
897 }
898
899 #[test]
900 fn test_missing_auth_header() {
901 let headers = HeaderMap::new();
902 assert!(extract_token(&headers).is_err());
903 }
904
905 #[test]
906 fn test_empty_auth_header() {
907 let mut headers = HeaderMap::new();
908 headers.insert("authorization", "".parse().unwrap());
909 assert!(extract_token(&headers).is_err());
910 }
911
912 #[test]
913 fn test_bearer_with_empty_token() {
914 let mut headers = HeaderMap::new();
915 headers.insert("authorization", "Bearer ".parse().unwrap());
916 assert!(extract_token(&headers).is_err());
917 }
918
919 #[test]
924 fn test_service_account_blocked_on_admin_paths() {
925 let claims = Claims::new(
928 "agent-key".to_string(),
929 "tenant1".to_string(),
930 Role::ServiceAccount,
931 chrono::Duration::hours(1),
932 );
933 let ctx = AuthContext { claims };
934 assert!(
935 ctx.require_permission(Permission::Admin).is_err(),
936 "ServiceAccount must not have Admin permission"
937 );
938 assert!(ctx.require_permission(Permission::Read).is_ok());
940 assert!(ctx.require_permission(Permission::Write).is_ok());
941 }
942
943 #[test]
944 fn test_admin_role_passes_admin_paths() {
945 let claims = Claims::new(
947 "admin-user".to_string(),
948 "tenant1".to_string(),
949 Role::Admin,
950 chrono::Duration::hours(1),
951 );
952 let ctx = AuthContext { claims };
953 assert!(
954 ctx.require_permission(Permission::Admin).is_ok(),
955 "Admin must have Admin permission"
956 );
957 }
958
959 #[test]
960 fn test_developer_blocked_on_admin_paths() {
961 let claims = Claims::new(
963 "dev-user".to_string(),
964 "tenant1".to_string(),
965 Role::Developer,
966 chrono::Duration::hours(1),
967 );
968 let ctx = AuthContext { claims };
969 assert!(
970 ctx.require_permission(Permission::Admin).is_err(),
971 "Developer must not have Admin permission"
972 );
973 }
974
975 #[test]
976 fn test_readonly_blocked_on_admin_paths() {
977 let claims = Claims::new(
978 "ro-user".to_string(),
979 "tenant1".to_string(),
980 Role::ReadOnly,
981 chrono::Duration::hours(1),
982 );
983 let ctx = AuthContext { claims };
984 assert!(ctx.require_permission(Permission::Admin).is_err());
985 assert!(ctx.require_permission(Permission::Read).is_ok());
986 assert!(ctx.require_permission(Permission::Write).is_err());
987 }
988
989 #[test]
990 fn test_is_admin_only_path_api_keys_create() {
991 assert!(is_admin_only_path("/api/v1/auth/api-keys", "POST"));
993 assert!(!is_admin_only_path("/api/v1/auth/api-keys", "GET"));
995 assert!(!is_admin_only_path("/api/v1/auth/api-keys", "DELETE"));
996 }
997
998 #[test]
999 fn test_is_admin_only_path_tenants() {
1000 assert!(is_admin_only_path("/api/v1/tenants", "GET"));
1002 assert!(is_admin_only_path("/api/v1/tenants", "POST"));
1003 assert!(is_admin_only_path("/api/v1/tenants/some-id", "DELETE"));
1004 assert!(is_admin_only_path("/api/v1/tenants/some-id/config", "PUT"));
1005 }
1006
1007 #[test]
1008 fn test_is_admin_only_path_normal_paths() {
1009 assert!(!is_admin_only_path("/api/v1/events", "POST"));
1011 assert!(!is_admin_only_path("/api/v1/events/query", "GET"));
1012 assert!(!is_admin_only_path("/api/v1/auth/me", "GET"));
1013 assert!(!is_admin_only_path("/api/v1/auth/login", "POST"));
1014 assert!(!is_admin_only_path("/api/v1/schemas", "GET"));
1015 }
1016
1017 #[test]
1018 fn test_auth_context_permissions() {
1019 let claims = Claims::new(
1020 "user1".to_string(),
1021 "tenant1".to_string(),
1022 Role::Developer,
1023 chrono::Duration::hours(1),
1024 );
1025
1026 let ctx = AuthContext { claims };
1027
1028 assert!(ctx.require_permission(Permission::Read).is_ok());
1029 assert!(ctx.require_permission(Permission::Write).is_ok());
1030 assert!(ctx.require_permission(Permission::Admin).is_err());
1031 }
1032
1033 #[test]
1034 fn test_auth_context_admin_permissions() {
1035 let claims = Claims::new(
1036 "admin1".to_string(),
1037 "tenant1".to_string(),
1038 Role::Admin,
1039 chrono::Duration::hours(1),
1040 );
1041
1042 let ctx = AuthContext { claims };
1043
1044 assert!(ctx.require_permission(Permission::Read).is_ok());
1045 assert!(ctx.require_permission(Permission::Write).is_ok());
1046 assert!(ctx.require_permission(Permission::Admin).is_ok());
1047 }
1048
1049 #[test]
1050 fn test_auth_context_readonly_permissions() {
1051 let claims = Claims::new(
1052 "readonly1".to_string(),
1053 "tenant1".to_string(),
1054 Role::ReadOnly,
1055 chrono::Duration::hours(1),
1056 );
1057
1058 let ctx = AuthContext { claims };
1059
1060 assert!(ctx.require_permission(Permission::Read).is_ok());
1061 assert!(ctx.require_permission(Permission::Write).is_err());
1062 assert!(ctx.require_permission(Permission::Admin).is_err());
1063 }
1064
1065 #[test]
1066 fn test_auth_context_tenant_id() {
1067 let claims = Claims::new(
1068 "user1".to_string(),
1069 "my-tenant".to_string(),
1070 Role::Developer,
1071 chrono::Duration::hours(1),
1072 );
1073
1074 let ctx = AuthContext { claims };
1075 assert_eq!(ctx.tenant_id(), "my-tenant");
1076 }
1077
1078 #[test]
1079 fn test_auth_context_user_id() {
1080 let claims = Claims::new(
1081 "my-user".to_string(),
1082 "tenant1".to_string(),
1083 Role::Developer,
1084 chrono::Duration::hours(1),
1085 );
1086
1087 let ctx = AuthContext { claims };
1088 assert_eq!(ctx.user_id(), "my-user");
1089 }
1090
1091 #[test]
1092 fn test_request_id_new() {
1093 let id1 = RequestId::new();
1094 let id2 = RequestId::new();
1095
1096 assert_ne!(id1.as_str(), id2.as_str());
1098 assert_eq!(id1.as_str().len(), 36);
1100 }
1101
1102 #[test]
1103 fn test_request_id_default() {
1104 let id = RequestId::default();
1105 assert_eq!(id.as_str().len(), 36);
1106 }
1107
1108 #[test]
1109 fn test_security_config_default() {
1110 let config = SecurityConfig::default();
1111
1112 assert!(config.enable_hsts);
1113 assert_eq!(config.hsts_max_age, 31536000);
1114 assert!(config.enable_frame_options);
1115 assert!(config.enable_content_type_options);
1116 assert!(config.enable_xss_protection);
1117 assert!(config.csp.is_some());
1118 }
1119
1120 #[test]
1121 fn test_frame_options_variants() {
1122 let deny = FrameOptions::Deny;
1123 let same_origin = FrameOptions::SameOrigin;
1124 let allow_from = FrameOptions::AllowFrom("https://example.com".to_string());
1125
1126 assert!(format!("{deny:?}").contains("Deny"));
1128 assert!(format!("{same_origin:?}").contains("SameOrigin"));
1129 assert!(format!("{allow_from:?}").contains("AllowFrom"));
1130 }
1131
1132 #[test]
1133 fn test_auth_error_from_validation_error() {
1134 let error = AllSourceError::ValidationError("test error".to_string());
1135 let auth_error = AuthError::from(error);
1136 assert!(format!("{auth_error:?}").contains("ValidationError"));
1137 }
1138
1139 #[test]
1140 fn test_rate_limit_error_display() {
1141 let error = RateLimitError::RateLimitExceeded {
1142 retry_after: 60,
1143 limit: 100,
1144 };
1145 assert!(format!("{error:?}").contains("RateLimitExceeded"));
1146
1147 let unauth_error = RateLimitError::Unauthorized;
1148 assert!(format!("{unauth_error:?}").contains("Unauthorized"));
1149 }
1150
1151 #[test]
1152 fn test_tenant_error_variants() {
1153 let errors = vec![
1154 TenantError::Unauthorized,
1155 TenantError::InvalidTenant,
1156 TenantError::TenantNotFound,
1157 TenantError::TenantInactive,
1158 TenantError::RepositoryError("test".to_string()),
1159 ];
1160
1161 for error in errors {
1162 let _ = format!("{error:?}");
1164 }
1165 }
1166
1167 #[test]
1168 fn test_ip_filter_error_variants() {
1169 let errors = vec![
1170 IpFilterError::NoIpAddress,
1171 IpFilterError::Blocked {
1172 reason: "blocked".to_string(),
1173 },
1174 ];
1175
1176 for error in errors {
1177 let _ = format!("{error:?}");
1178 }
1179 }
1180
1181 #[test]
1182 fn test_security_state_clone() {
1183 let config = SecurityConfig::default();
1184 let state = SecurityState {
1185 config: config.clone(),
1186 };
1187 let cloned = state.clone();
1188 assert_eq!(cloned.config.hsts_max_age, config.hsts_max_age);
1189 }
1190
1191 #[test]
1192 fn test_auth_state_clone() {
1193 let auth_manager = Arc::new(AuthManager::new("test-secret"));
1194 let state = AuthState { auth_manager };
1195 let cloned = state.clone();
1196 assert!(Arc::ptr_eq(&state.auth_manager, &cloned.auth_manager));
1197 }
1198
1199 #[test]
1200 fn test_rate_limit_state_clone() {
1201 use crate::infrastructure::security::rate_limit::RateLimitConfig;
1202 let rate_limiter = Arc::new(RateLimiter::new(RateLimitConfig::free_tier()));
1203 let state = RateLimitState { rate_limiter };
1204 let cloned = state.clone();
1205 assert!(Arc::ptr_eq(&state.rate_limiter, &cloned.rate_limiter));
1206 }
1207
1208 #[test]
1209 fn test_auth_skip_paths_contains_expected() {
1210 assert!(should_skip_auth("/health"));
1212 assert!(should_skip_auth("/metrics"));
1213 assert!(should_skip_auth("/api/v1/auth/register"));
1214 assert!(should_skip_auth("/api/v1/auth/login"));
1215 assert!(should_skip_auth("/api/v1/demo/seed"));
1216
1217 assert!(should_skip_auth("/internal/promote"));
1219 assert!(should_skip_auth("/internal/repoint"));
1220 assert!(should_skip_auth("/internal/anything"));
1221
1222 assert!(!should_skip_auth("/api/v1/events"));
1224 assert!(!should_skip_auth("/api/v1/auth/me"));
1225 assert!(!should_skip_auth("/api/v1/tenants"));
1226 }
1227
1228 #[test]
1229 fn test_dev_mode_auth_context() {
1230 let ctx = dev_mode_auth_context();
1231
1232 assert_eq!(ctx.tenant_id(), "dev-tenant");
1234 assert_eq!(ctx.user_id(), "dev-user");
1235 assert!(ctx.require_permission(Permission::Admin).is_ok());
1236 assert!(ctx.require_permission(Permission::Read).is_ok());
1237 assert!(ctx.require_permission(Permission::Write).is_ok());
1238 }
1239
1240 #[test]
1241 fn test_dev_mode_disabled_by_default() {
1242 let env_value = std::env::var("ALLSOURCE_DEV_MODE").unwrap_or_default();
1246 if env_value.is_empty() {
1247 assert!(!is_dev_mode());
1248 }
1249 }
1250}