use crate::{
error::AllSourceError,
infrastructure::security::{
auth::{AuthManager, Claims, Permission, Role},
rate_limit::RateLimiter,
},
};
use axum::{
extract::{Request, State},
http::{HeaderMap, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
};
use std::sync::{Arc, LazyLock};
pub const AUTH_SKIP_PATHS: &[&str] = &[
"/health",
"/metrics",
"/api/v1/auth/register",
"/api/v1/auth/login",
"/api/v1/demo/seed",
];
pub const AUTH_SKIP_PREFIXES: &[&str] = &["/internal/"];
#[inline]
pub fn should_skip_auth(path: &str) -> bool {
AUTH_SKIP_PATHS.contains(&path) || AUTH_SKIP_PREFIXES.iter().any(|pfx| path.starts_with(pfx))
}
fn env_flag_enabled(name: &str) -> bool {
std::env::var(name)
.map(|v| matches!(v.to_lowercase().as_str(), "true" | "1" | "yes"))
.unwrap_or(false)
}
static DEV_MODE_ENABLED: LazyLock<bool> = LazyLock::new(|| {
let via_dev = env_flag_enabled("ALLSOURCE_DEV_MODE");
let via_auth_off = env_flag_enabled("ALLSOURCE_AUTH_DISABLED");
let enabled = via_dev || via_auth_off;
if enabled {
let source = if via_auth_off && via_dev {
"ALLSOURCE_DEV_MODE + ALLSOURCE_AUTH_DISABLED"
} else if via_auth_off {
"ALLSOURCE_AUTH_DISABLED"
} else {
"ALLSOURCE_DEV_MODE"
};
tracing::warn!(
"⚠️ Auth disabled via {source} — all requests run as admin with no rate limits. DO NOT use in production."
);
}
enabled
});
#[inline]
pub fn is_dev_mode() -> bool {
*DEV_MODE_ENABLED
}
fn dev_mode_auth_context() -> AuthContext {
AuthContext {
claims: Claims::new(
"dev-user".to_string(),
"dev-tenant".to_string(),
Role::Admin,
chrono::Duration::hours(24),
),
}
}
#[derive(Clone)]
pub struct AuthState {
pub auth_manager: Arc<AuthManager>,
}
#[derive(Clone)]
pub struct RateLimitState {
pub rate_limiter: Arc<RateLimiter>,
}
#[derive(Debug, Clone)]
pub struct AuthContext {
pub claims: Claims,
}
impl AuthContext {
pub fn require_permission(&self, permission: Permission) -> Result<(), AllSourceError> {
if self.claims.has_permission(permission) {
Ok(())
} else {
Err(AllSourceError::ValidationError(
"Insufficient permissions".to_string(),
))
}
}
pub fn tenant_id(&self) -> &str {
&self.claims.tenant_id
}
pub fn user_id(&self) -> &str {
&self.claims.sub
}
}
fn extract_token(headers: &HeaderMap) -> Result<String, AllSourceError> {
let auth_header = if let Some(val) = headers.get("authorization") {
val.to_str()
.map_err(|_| {
AllSourceError::ValidationError("Invalid authorization header".to_string())
})?
.to_string()
} else if let Some(val) = headers.get("x-api-key") {
val.to_str()
.map_err(|_| AllSourceError::ValidationError("Invalid X-API-Key header".to_string()))?
.to_string()
} else {
return Err(AllSourceError::ValidationError(
"Missing authorization header".to_string(),
));
};
let token = if auth_header.starts_with("Bearer ") {
auth_header.trim_start_matches("Bearer ").trim()
} else if auth_header.starts_with("bearer ") {
auth_header.trim_start_matches("bearer ").trim()
} else {
auth_header.trim()
};
if token.is_empty() {
return Err(AllSourceError::ValidationError(
"Empty authorization token".to_string(),
));
}
Ok(token.to_string())
}
#[inline]
pub fn is_admin_only_path(path: &str, method: &str) -> bool {
(path == "/api/v1/auth/api-keys" && method == "POST") || path.starts_with("/api/v1/tenants")
}
pub async fn auth_middleware(
State(auth_state): State<AuthState>,
mut request: Request,
next: Next,
) -> Result<Response, AuthError> {
let path = request.uri().path();
if should_skip_auth(path) {
return Ok(next.run(request).await);
}
if is_dev_mode() {
let headers = request.headers();
let auth_ctx = match extract_token(headers) {
Ok(token) => {
let claims = if token.starts_with("ask_") {
auth_state.auth_manager.validate_api_key(&token).ok()
} else {
auth_state.auth_manager.validate_token(&token).ok()
};
claims.map_or_else(dev_mode_auth_context, |c| AuthContext { claims: c })
}
Err(_) => dev_mode_auth_context(),
};
request.extensions_mut().insert(auth_ctx);
return Ok(next.run(request).await);
}
let headers = request.headers();
let token = extract_token(headers)?;
let claims = if token.starts_with("ask_") {
auth_state.auth_manager.validate_api_key(&token)?
} else {
auth_state.auth_manager.validate_token(&token)?
};
let auth_ctx = AuthContext { claims };
let path = request.uri().path();
let method = request.method().as_str();
let is_admin_only_path = is_admin_only_path(path, method);
if is_admin_only_path {
auth_ctx
.require_permission(Permission::Admin)
.map_err(|_| {
AuthError(AllSourceError::ValidationError(
"Admin permission required".to_string(),
))
})?;
}
request.extensions_mut().insert(auth_ctx);
Ok(next.run(request).await)
}
pub async fn optional_auth_middleware(
State(auth_state): State<AuthState>,
mut request: Request,
next: Next,
) -> Response {
let headers = request.headers();
if let Ok(token) = extract_token(headers) {
let claims = if token.starts_with("ask_") {
auth_state.auth_manager.validate_api_key(&token).ok()
} else {
auth_state.auth_manager.validate_token(&token).ok()
};
if let Some(claims) = claims {
request.extensions_mut().insert(AuthContext { claims });
}
}
next.run(request).await
}
#[derive(Debug)]
pub struct AuthError(AllSourceError);
impl From<AllSourceError> for AuthError {
fn from(err: AllSourceError) -> Self {
AuthError(err)
}
}
impl IntoResponse for AuthError {
fn into_response(self) -> Response {
let (status, message) = match self.0 {
AllSourceError::ValidationError(msg) => (StatusCode::UNAUTHORIZED, msg),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(),
),
};
(status, message).into_response()
}
}
pub struct Authenticated(pub AuthContext);
impl<S> axum::extract::FromRequestParts<S> for Authenticated
where
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
parts
.extensions
.get::<AuthContext>()
.cloned()
.map(Authenticated)
.ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))
}
}
pub struct OptionalAuth(pub Option<AuthContext>);
impl<S> axum::extract::FromRequestParts<S> for OptionalAuth
where
S: Send + Sync,
{
type Rejection = std::convert::Infallible;
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
Ok(OptionalAuth(parts.extensions.get::<AuthContext>().cloned()))
}
}
pub struct Admin(pub AuthContext);
impl<S> axum::extract::FromRequestParts<S> for Admin
where
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(
parts: &mut axum::http::request::Parts,
_state: &S,
) -> Result<Self, Self::Rejection> {
let auth_ctx = parts
.extensions
.get::<AuthContext>()
.cloned()
.ok_or((StatusCode::UNAUTHORIZED, "Unauthorized"))?;
auth_ctx
.require_permission(Permission::Admin)
.map_err(|_| (StatusCode::FORBIDDEN, "Admin permission required"))?;
Ok(Admin(auth_ctx))
}
}
pub async fn rate_limit_middleware(
State(rate_limit_state): State<RateLimitState>,
request: Request,
next: Next,
) -> Result<Response, RateLimitError> {
let path = request.uri().path();
if should_skip_auth(path) {
return Ok(next.run(request).await);
}
if is_dev_mode() {
return Ok(next.run(request).await);
}
let auth_ctx = request
.extensions()
.get::<AuthContext>()
.ok_or(RateLimitError::Unauthorized)?;
let result = rate_limit_state
.rate_limiter
.check_rate_limit(auth_ctx.tenant_id());
if !result.allowed {
return Err(RateLimitError::RateLimitExceeded {
retry_after: result.retry_after.unwrap_or_default().as_secs(),
limit: result.limit,
});
}
let mut response = next.run(request).await;
let headers = response.headers_mut();
headers.insert(
"X-RateLimit-Limit",
result.limit.to_string().parse().unwrap(),
);
headers.insert(
"X-RateLimit-Remaining",
result.remaining.to_string().parse().unwrap(),
);
Ok(response)
}
#[derive(Debug)]
pub enum RateLimitError {
RateLimitExceeded { retry_after: u64, limit: u32 },
Unauthorized,
}
impl IntoResponse for RateLimitError {
fn into_response(self) -> Response {
match self {
RateLimitError::RateLimitExceeded { retry_after, limit } => {
let mut response = (
StatusCode::TOO_MANY_REQUESTS,
format!("Rate limit exceeded. Limit: {limit} requests/min"),
)
.into_response();
if retry_after > 0 {
response
.headers_mut()
.insert("Retry-After", retry_after.to_string().parse().unwrap());
}
response
}
RateLimitError::Unauthorized => (
StatusCode::UNAUTHORIZED,
"Authentication required for rate limiting",
)
.into_response(),
}
}
}
#[macro_export]
macro_rules! require_permission {
($auth:expr, $perm:expr) => {
$auth.0.require_permission($perm).map_err(|_| {
(
axum::http::StatusCode::FORBIDDEN,
"Insufficient permissions",
)
})?
};
}
use crate::domain::{entities::Tenant, repositories::TenantRepository, value_objects::TenantId};
#[derive(Clone)]
pub struct TenantState<R: TenantRepository> {
pub tenant_repository: Arc<R>,
}
#[derive(Debug, Clone)]
pub struct TenantContext {
pub tenant: Tenant,
}
impl TenantContext {
pub fn tenant_id(&self) -> &TenantId {
self.tenant.id()
}
pub fn is_active(&self) -> bool {
self.tenant.is_active()
}
}
pub async fn tenant_isolation_middleware<R: TenantRepository + 'static>(
State(tenant_state): State<TenantState<R>>,
mut request: Request,
next: Next,
) -> Result<Response, TenantError> {
let auth_ctx = request
.extensions()
.get::<AuthContext>()
.ok_or(TenantError::Unauthorized)?
.clone();
let tenant_id =
TenantId::new(auth_ctx.tenant_id().to_string()).map_err(|_| TenantError::InvalidTenant)?;
let tenant = tenant_state
.tenant_repository
.find_by_id(&tenant_id)
.await
.map_err(|e| TenantError::RepositoryError(e.to_string()))?
.ok_or(TenantError::TenantNotFound)?;
if !tenant.is_active() {
return Err(TenantError::TenantInactive);
}
request.extensions_mut().insert(TenantContext { tenant });
Ok(next.run(request).await)
}
#[derive(Debug)]
pub enum TenantError {
Unauthorized,
InvalidTenant,
TenantNotFound,
TenantInactive,
RepositoryError(String),
}
impl IntoResponse for TenantError {
fn into_response(self) -> Response {
let (status, message) = match self {
TenantError::Unauthorized => (
StatusCode::UNAUTHORIZED,
"Authentication required for tenant access",
),
TenantError::InvalidTenant => (StatusCode::BAD_REQUEST, "Invalid tenant identifier"),
TenantError::TenantNotFound => (StatusCode::NOT_FOUND, "Tenant not found"),
TenantError::TenantInactive => (StatusCode::FORBIDDEN, "Tenant is inactive"),
TenantError::RepositoryError(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to validate tenant",
),
};
(status, message).into_response()
}
}
use uuid::Uuid;
#[derive(Debug, Clone)]
pub struct RequestId(pub String);
impl Default for RequestId {
fn default() -> Self {
Self::new()
}
}
impl RequestId {
pub fn new() -> Self {
Self(Uuid::new_v4().to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
pub async fn request_id_middleware(mut request: Request, next: Next) -> Response {
let request_id = request
.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.map_or_else(RequestId::new, |s| RequestId(s.to_string()));
request.extensions_mut().insert(request_id.clone());
let mut response = next.run(request).await;
response
.headers_mut()
.insert("x-request-id", request_id.0.parse().unwrap());
response
}
#[derive(Debug, Clone)]
pub struct SecurityConfig {
pub enable_hsts: bool,
pub hsts_max_age: u32,
pub enable_frame_options: bool,
pub frame_options: FrameOptions,
pub enable_content_type_options: bool,
pub enable_xss_protection: bool,
pub csp: Option<String>,
pub cors_origins: Vec<String>,
pub cors_methods: Vec<String>,
pub cors_headers: Vec<String>,
pub cors_max_age: u32,
}
#[derive(Debug, Clone)]
pub enum FrameOptions {
Deny,
SameOrigin,
AllowFrom(String),
}
impl Default for SecurityConfig {
fn default() -> Self {
Self {
enable_hsts: true,
hsts_max_age: 31_536_000, enable_frame_options: true,
frame_options: FrameOptions::Deny,
enable_content_type_options: true,
enable_xss_protection: true,
csp: Some("default-src 'self'".to_string()),
cors_origins: vec!["*".to_string()],
cors_methods: vec![
"GET".to_string(),
"POST".to_string(),
"PUT".to_string(),
"DELETE".to_string(),
],
cors_headers: vec!["Content-Type".to_string(), "Authorization".to_string()],
cors_max_age: 3600,
}
}
}
#[derive(Clone)]
pub struct SecurityState {
pub config: SecurityConfig,
}
pub async fn security_headers_middleware(
State(security_state): State<SecurityState>,
request: Request,
next: Next,
) -> Response {
let mut response = next.run(request).await;
let headers = response.headers_mut();
let config = &security_state.config;
if config.enable_hsts {
headers.insert(
"strict-transport-security",
format!("max-age={}", config.hsts_max_age).parse().unwrap(),
);
}
if config.enable_frame_options {
let value = match &config.frame_options {
FrameOptions::Deny => "DENY",
FrameOptions::SameOrigin => "SAMEORIGIN",
FrameOptions::AllowFrom(origin) => origin,
};
headers.insert("x-frame-options", value.parse().unwrap());
}
if config.enable_content_type_options {
headers.insert("x-content-type-options", "nosniff".parse().unwrap());
}
if config.enable_xss_protection {
headers.insert("x-xss-protection", "1; mode=block".parse().unwrap());
}
if let Some(csp) = &config.csp {
headers.insert("content-security-policy", csp.parse().unwrap());
}
headers.insert(
"access-control-allow-origin",
config.cors_origins.join(", ").parse().unwrap(),
);
headers.insert(
"access-control-allow-methods",
config.cors_methods.join(", ").parse().unwrap(),
);
headers.insert(
"access-control-allow-headers",
config.cors_headers.join(", ").parse().unwrap(),
);
headers.insert(
"access-control-max-age",
config.cors_max_age.to_string().parse().unwrap(),
);
response
}
use crate::infrastructure::security::IpFilter;
use std::net::SocketAddr;
#[derive(Clone)]
pub struct IpFilterState {
pub ip_filter: Arc<IpFilter>,
}
pub async fn ip_filter_middleware(
State(ip_filter_state): State<IpFilterState>,
request: Request,
next: Next,
) -> Result<Response, IpFilterError> {
let client_ip = request
.extensions()
.get::<axum::extract::ConnectInfo<SocketAddr>>()
.map(|connect_info| connect_info.0.ip())
.ok_or(IpFilterError::NoIpAddress)?;
let result = if let Some(tenant_ctx) = request.extensions().get::<TenantContext>() {
ip_filter_state
.ip_filter
.is_allowed_for_tenant(tenant_ctx.tenant_id(), &client_ip)
} else {
ip_filter_state.ip_filter.is_allowed(&client_ip)
};
if !result.allowed {
return Err(IpFilterError::Blocked {
reason: result.reason,
});
}
Ok(next.run(request).await)
}
#[derive(Debug)]
pub enum IpFilterError {
NoIpAddress,
Blocked { reason: String },
}
impl IntoResponse for IpFilterError {
fn into_response(self) -> Response {
match self {
IpFilterError::NoIpAddress => (
StatusCode::BAD_REQUEST,
"Unable to determine client IP address",
)
.into_response(),
IpFilterError::Blocked { reason } => {
(StatusCode::FORBIDDEN, format!("Access denied: {reason}")).into_response()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::infrastructure::security::auth::Role;
#[test]
fn test_extract_bearer_token() {
let mut headers = HeaderMap::new();
headers.insert("authorization", "Bearer test_token_123".parse().unwrap());
let token = extract_token(&headers).unwrap();
assert_eq!(token, "test_token_123");
}
#[test]
fn test_extract_lowercase_bearer() {
let mut headers = HeaderMap::new();
headers.insert("authorization", "bearer test_token_123".parse().unwrap());
let token = extract_token(&headers).unwrap();
assert_eq!(token, "test_token_123");
}
#[test]
fn test_extract_plain_token() {
let mut headers = HeaderMap::new();
headers.insert("authorization", "test_token_123".parse().unwrap());
let token = extract_token(&headers).unwrap();
assert_eq!(token, "test_token_123");
}
#[test]
fn test_missing_auth_header() {
let headers = HeaderMap::new();
assert!(extract_token(&headers).is_err());
}
#[test]
fn test_empty_auth_header() {
let mut headers = HeaderMap::new();
headers.insert("authorization", "".parse().unwrap());
assert!(extract_token(&headers).is_err());
}
#[test]
fn test_bearer_with_empty_token() {
let mut headers = HeaderMap::new();
headers.insert("authorization", "Bearer ".parse().unwrap());
assert!(extract_token(&headers).is_err());
}
#[test]
fn test_service_account_blocked_on_admin_paths() {
let claims = Claims::new(
"agent-key".to_string(),
"tenant1".to_string(),
Role::ServiceAccount,
chrono::Duration::hours(1),
);
let ctx = AuthContext { claims };
assert!(
ctx.require_permission(Permission::Admin).is_err(),
"ServiceAccount must not have Admin permission"
);
assert!(ctx.require_permission(Permission::Read).is_ok());
assert!(ctx.require_permission(Permission::Write).is_ok());
}
#[test]
fn test_admin_role_passes_admin_paths() {
let claims = Claims::new(
"admin-user".to_string(),
"tenant1".to_string(),
Role::Admin,
chrono::Duration::hours(1),
);
let ctx = AuthContext { claims };
assert!(
ctx.require_permission(Permission::Admin).is_ok(),
"Admin must have Admin permission"
);
}
#[test]
fn test_developer_blocked_on_admin_paths() {
let claims = Claims::new(
"dev-user".to_string(),
"tenant1".to_string(),
Role::Developer,
chrono::Duration::hours(1),
);
let ctx = AuthContext { claims };
assert!(
ctx.require_permission(Permission::Admin).is_err(),
"Developer must not have Admin permission"
);
}
#[test]
fn test_readonly_blocked_on_admin_paths() {
let claims = Claims::new(
"ro-user".to_string(),
"tenant1".to_string(),
Role::ReadOnly,
chrono::Duration::hours(1),
);
let ctx = AuthContext { claims };
assert!(ctx.require_permission(Permission::Admin).is_err());
assert!(ctx.require_permission(Permission::Read).is_ok());
assert!(ctx.require_permission(Permission::Write).is_err());
}
#[test]
fn test_is_admin_only_path_api_keys_create() {
assert!(is_admin_only_path("/api/v1/auth/api-keys", "POST"));
assert!(!is_admin_only_path("/api/v1/auth/api-keys", "GET"));
assert!(!is_admin_only_path("/api/v1/auth/api-keys", "DELETE"));
}
#[test]
fn test_is_admin_only_path_tenants() {
assert!(is_admin_only_path("/api/v1/tenants", "GET"));
assert!(is_admin_only_path("/api/v1/tenants", "POST"));
assert!(is_admin_only_path("/api/v1/tenants/some-id", "DELETE"));
assert!(is_admin_only_path("/api/v1/tenants/some-id/config", "PUT"));
}
#[test]
fn test_is_admin_only_path_normal_paths() {
assert!(!is_admin_only_path("/api/v1/events", "POST"));
assert!(!is_admin_only_path("/api/v1/events/query", "GET"));
assert!(!is_admin_only_path("/api/v1/auth/me", "GET"));
assert!(!is_admin_only_path("/api/v1/auth/login", "POST"));
assert!(!is_admin_only_path("/api/v1/schemas", "GET"));
}
#[test]
fn test_auth_context_permissions() {
let claims = Claims::new(
"user1".to_string(),
"tenant1".to_string(),
Role::Developer,
chrono::Duration::hours(1),
);
let ctx = AuthContext { claims };
assert!(ctx.require_permission(Permission::Read).is_ok());
assert!(ctx.require_permission(Permission::Write).is_ok());
assert!(ctx.require_permission(Permission::Admin).is_err());
}
#[test]
fn test_auth_context_admin_permissions() {
let claims = Claims::new(
"admin1".to_string(),
"tenant1".to_string(),
Role::Admin,
chrono::Duration::hours(1),
);
let ctx = AuthContext { claims };
assert!(ctx.require_permission(Permission::Read).is_ok());
assert!(ctx.require_permission(Permission::Write).is_ok());
assert!(ctx.require_permission(Permission::Admin).is_ok());
}
#[test]
fn test_auth_context_readonly_permissions() {
let claims = Claims::new(
"readonly1".to_string(),
"tenant1".to_string(),
Role::ReadOnly,
chrono::Duration::hours(1),
);
let ctx = AuthContext { claims };
assert!(ctx.require_permission(Permission::Read).is_ok());
assert!(ctx.require_permission(Permission::Write).is_err());
assert!(ctx.require_permission(Permission::Admin).is_err());
}
#[test]
fn test_auth_context_tenant_id() {
let claims = Claims::new(
"user1".to_string(),
"my-tenant".to_string(),
Role::Developer,
chrono::Duration::hours(1),
);
let ctx = AuthContext { claims };
assert_eq!(ctx.tenant_id(), "my-tenant");
}
#[test]
fn test_auth_context_user_id() {
let claims = Claims::new(
"my-user".to_string(),
"tenant1".to_string(),
Role::Developer,
chrono::Duration::hours(1),
);
let ctx = AuthContext { claims };
assert_eq!(ctx.user_id(), "my-user");
}
#[test]
fn test_request_id_new() {
let id1 = RequestId::new();
let id2 = RequestId::new();
assert_ne!(id1.as_str(), id2.as_str());
assert_eq!(id1.as_str().len(), 36);
}
#[test]
fn test_request_id_default() {
let id = RequestId::default();
assert_eq!(id.as_str().len(), 36);
}
#[test]
fn test_security_config_default() {
let config = SecurityConfig::default();
assert!(config.enable_hsts);
assert_eq!(config.hsts_max_age, 31536000);
assert!(config.enable_frame_options);
assert!(config.enable_content_type_options);
assert!(config.enable_xss_protection);
assert!(config.csp.is_some());
}
#[test]
fn test_frame_options_variants() {
let deny = FrameOptions::Deny;
let same_origin = FrameOptions::SameOrigin;
let allow_from = FrameOptions::AllowFrom("https://example.com".to_string());
assert!(format!("{deny:?}").contains("Deny"));
assert!(format!("{same_origin:?}").contains("SameOrigin"));
assert!(format!("{allow_from:?}").contains("AllowFrom"));
}
#[test]
fn test_auth_error_from_validation_error() {
let error = AllSourceError::ValidationError("test error".to_string());
let auth_error = AuthError::from(error);
assert!(format!("{auth_error:?}").contains("ValidationError"));
}
#[test]
fn test_rate_limit_error_display() {
let error = RateLimitError::RateLimitExceeded {
retry_after: 60,
limit: 100,
};
assert!(format!("{error:?}").contains("RateLimitExceeded"));
let unauth_error = RateLimitError::Unauthorized;
assert!(format!("{unauth_error:?}").contains("Unauthorized"));
}
#[test]
fn test_tenant_error_variants() {
let errors = vec![
TenantError::Unauthorized,
TenantError::InvalidTenant,
TenantError::TenantNotFound,
TenantError::TenantInactive,
TenantError::RepositoryError("test".to_string()),
];
for error in errors {
let _ = format!("{error:?}");
}
}
#[test]
fn test_ip_filter_error_variants() {
let errors = vec![
IpFilterError::NoIpAddress,
IpFilterError::Blocked {
reason: "blocked".to_string(),
},
];
for error in errors {
let _ = format!("{error:?}");
}
}
#[test]
fn test_security_state_clone() {
let config = SecurityConfig::default();
let state = SecurityState {
config: config.clone(),
};
let cloned = state.clone();
assert_eq!(cloned.config.hsts_max_age, config.hsts_max_age);
}
#[test]
fn test_auth_state_clone() {
let auth_manager = Arc::new(AuthManager::new("test-secret"));
let state = AuthState { auth_manager };
let cloned = state.clone();
assert!(Arc::ptr_eq(&state.auth_manager, &cloned.auth_manager));
}
#[test]
fn test_rate_limit_state_clone() {
use crate::infrastructure::security::rate_limit::RateLimitConfig;
let rate_limiter = Arc::new(RateLimiter::new(RateLimitConfig::free_tier()));
let state = RateLimitState { rate_limiter };
let cloned = state.clone();
assert!(Arc::ptr_eq(&state.rate_limiter, &cloned.rate_limiter));
}
#[test]
fn test_auth_skip_paths_contains_expected() {
assert!(should_skip_auth("/health"));
assert!(should_skip_auth("/metrics"));
assert!(should_skip_auth("/api/v1/auth/register"));
assert!(should_skip_auth("/api/v1/auth/login"));
assert!(should_skip_auth("/api/v1/demo/seed"));
assert!(should_skip_auth("/internal/promote"));
assert!(should_skip_auth("/internal/repoint"));
assert!(should_skip_auth("/internal/anything"));
assert!(!should_skip_auth("/api/v1/events"));
assert!(!should_skip_auth("/api/v1/auth/me"));
assert!(!should_skip_auth("/api/v1/tenants"));
}
#[test]
fn test_dev_mode_auth_context() {
let ctx = dev_mode_auth_context();
assert_eq!(ctx.tenant_id(), "dev-tenant");
assert_eq!(ctx.user_id(), "dev-user");
assert!(ctx.require_permission(Permission::Admin).is_ok());
assert!(ctx.require_permission(Permission::Read).is_ok());
assert!(ctx.require_permission(Permission::Write).is_ok());
}
#[test]
fn test_dev_mode_disabled_by_default() {
let env_value = std::env::var("ALLSOURCE_DEV_MODE").unwrap_or_default();
if env_value.is_empty() {
assert!(!is_dev_mode());
}
}
}