feat(cors): implement CORS configuration with From trait
Implement From<CorsSettings> for Cors trait to configure CORS middleware with production-ready security validation. - Move CorsSettings to backend/src/settings/cors.rs module - Validate wildcard + credentials constraint (browser security policy) - Configure allowed methods, headers, credentials, and max_age - Add structured logging for CORS configuration - Move tests from settings/mod.rs and startup.rs to cors module Ref: T014
This commit is contained in:
@@ -4,4 +4,4 @@ skip-clean = true
|
|||||||
target-dir = "coverage"
|
target-dir = "coverage"
|
||||||
output-dir = "coverage"
|
output-dir = "coverage"
|
||||||
fail-under = 60
|
fail-under = 60
|
||||||
exclude-files = ["target/*", "private/*"]
|
exclude-files = ["target/*", "private/*", "tests/*"]
|
||||||
|
|||||||
216
backend/src/settings/cors.rs
Normal file
216
backend/src/settings/cors.rs
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
use poem::{
|
||||||
|
http::{Method, header},
|
||||||
|
middleware::Cors,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// CORS (Cross-Origin Resource Sharing) configuration for the HTTP API.
|
||||||
|
///
|
||||||
|
/// Controls which origins can access the API from browsers. In development,
|
||||||
|
/// use permissive settings (`allowed_origins: ["*"]`). In production, use
|
||||||
|
/// restrictive settings with specific origins.
|
||||||
|
///
|
||||||
|
/// # Security Constraint
|
||||||
|
///
|
||||||
|
/// When `allow_credentials` is `true`, `allowed_origins` MUST NOT contain
|
||||||
|
/// wildcard `"*"`. This is enforced by browser security policy and will be
|
||||||
|
/// validated by the `build_cors()` function.
|
||||||
|
#[derive(Debug, serde::Deserialize, Clone)]
|
||||||
|
pub struct CorsSettings {
|
||||||
|
/// List of allowed origin URLs (e.g., `["https://sta.example.com"]`).
|
||||||
|
///
|
||||||
|
/// Use `["*"]` for development to allow all origins.
|
||||||
|
/// In production, specify exact origins to prevent unauthorized access.
|
||||||
|
#[serde(default)]
|
||||||
|
pub allowed_origins: Vec<String>,
|
||||||
|
/// Whether to allow credentials (cookies, authorization headers) in CORS requests.
|
||||||
|
///
|
||||||
|
/// Set to `true` in production when using Authelia authentication.
|
||||||
|
/// MUST be `false` when using wildcard `"*"` in `allowed_origins`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub allow_credentials: bool,
|
||||||
|
/// Duration in seconds that browsers can cache CORS preflight responses.
|
||||||
|
///
|
||||||
|
/// Typical value: `3600` (1 hour). Higher values reduce preflight requests
|
||||||
|
/// but delay policy changes from taking effect.
|
||||||
|
#[serde(default = "default_max_age_secs")]
|
||||||
|
pub max_age_secs: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CorsSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
allowed_origins: vec![],
|
||||||
|
allow_credentials: false,
|
||||||
|
max_age_secs: 3600,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Default value for CORS max age in seconds (1 hour).
|
||||||
|
const fn default_max_age_secs() -> i32 {
|
||||||
|
3600
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CorsSettings> for Cors {
|
||||||
|
fn from(val: CorsSettings) -> Self {
|
||||||
|
assert!(
|
||||||
|
!(val.allow_credentials && val.allowed_origins.contains(&"*".to_string())),
|
||||||
|
"CORS misconfiguration: wildcard origin not allowed with credentials=true"
|
||||||
|
);
|
||||||
|
let mut cors = Self::new();
|
||||||
|
for origin in &val.allowed_origins {
|
||||||
|
cors = cors.allow_origin(origin);
|
||||||
|
}
|
||||||
|
cors = cors.allow_methods(vec![
|
||||||
|
Method::GET,
|
||||||
|
Method::POST,
|
||||||
|
Method::PUT,
|
||||||
|
Method::PATCH,
|
||||||
|
Method::DELETE,
|
||||||
|
Method::OPTIONS,
|
||||||
|
]);
|
||||||
|
cors = cors.allow_headers(vec![header::CONTENT_TYPE, header::AUTHORIZATION]);
|
||||||
|
cors = cors
|
||||||
|
.allow_credentials(val.allow_credentials)
|
||||||
|
.max_age(val.max_age_secs);
|
||||||
|
tracing::info!(
|
||||||
|
target: "backend::settings::cors",
|
||||||
|
allowed_origins = ?val.allowed_origins,
|
||||||
|
allow_credentials = ?val.allow_credentials,
|
||||||
|
max_age_secs = ?val.max_age_secs,
|
||||||
|
"CORS middleware configured"
|
||||||
|
);
|
||||||
|
cors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
// T009: Tests for CorsSettings struct deserialization
|
||||||
|
#[test]
|
||||||
|
fn cors_settings_deserialize_from_yaml() {
|
||||||
|
let yaml = r#"
|
||||||
|
allowed_origins:
|
||||||
|
- "http://localhost:5173"
|
||||||
|
- "https://sta.example.com"
|
||||||
|
allow_credentials: true
|
||||||
|
max_age_secs: 7200
|
||||||
|
"#;
|
||||||
|
let settings: CorsSettings = serde_yaml::from_str(yaml).unwrap();
|
||||||
|
assert_eq!(settings.allowed_origins.len(), 2);
|
||||||
|
assert_eq!(settings.allowed_origins[0], "http://localhost:5173");
|
||||||
|
assert_eq!(settings.allowed_origins[1], "https://sta.example.com");
|
||||||
|
assert!(settings.allow_credentials);
|
||||||
|
assert_eq!(settings.max_age_secs, 7200);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_settings_default_has_empty_origins() {
|
||||||
|
let settings = CorsSettings::default();
|
||||||
|
assert!(
|
||||||
|
settings.allowed_origins.is_empty(),
|
||||||
|
"Default CorsSettings should have empty allowed_origins for restrictive fail-safe"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!settings.allow_credentials,
|
||||||
|
"Default CorsSettings should have credentials disabled"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
settings.max_age_secs, 3600,
|
||||||
|
"Default CorsSettings should have 1 hour max_age"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_settings_with_wildcard_deserializes() {
|
||||||
|
let yaml = r#"
|
||||||
|
allowed_origins:
|
||||||
|
- "*"
|
||||||
|
allow_credentials: false
|
||||||
|
max_age_secs: 3600
|
||||||
|
"#;
|
||||||
|
let settings: CorsSettings = serde_yaml::from_str(yaml).unwrap();
|
||||||
|
assert_eq!(settings.allowed_origins.len(), 1);
|
||||||
|
assert_eq!(settings.allowed_origins[0], "*");
|
||||||
|
assert!(!settings.allow_credentials);
|
||||||
|
assert_eq!(settings.max_age_secs, 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_settings_deserialize_with_defaults() {
|
||||||
|
// Test partial deserialization using serde defaults
|
||||||
|
let yaml = r#"
|
||||||
|
allowed_origins:
|
||||||
|
- "https://example.com"
|
||||||
|
"#;
|
||||||
|
let settings: CorsSettings = serde_yaml::from_str(yaml).unwrap();
|
||||||
|
assert_eq!(settings.allowed_origins.len(), 1);
|
||||||
|
assert_eq!(settings.allowed_origins[0], "https://example.com");
|
||||||
|
// These should use defaults
|
||||||
|
assert!(!settings.allow_credentials);
|
||||||
|
assert_eq!(settings.max_age_secs, 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
// T013: Tests for From<CorsSettings> for Cors trait implementation
|
||||||
|
#[test]
|
||||||
|
fn cors_conversion_with_wildcard_origin() {
|
||||||
|
let settings = CorsSettings {
|
||||||
|
allowed_origins: vec!["*".to_string()],
|
||||||
|
allow_credentials: false,
|
||||||
|
max_age_secs: 3600,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should successfully convert without panic
|
||||||
|
let _cors: Cors = settings.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_conversion_with_specific_origin() {
|
||||||
|
let settings = CorsSettings {
|
||||||
|
allowed_origins: vec!["https://sta.example.com".to_string()],
|
||||||
|
allow_credentials: true,
|
||||||
|
max_age_secs: 7200,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should successfully convert without panic
|
||||||
|
let _cors: Cors = settings.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_conversion_with_multiple_origins() {
|
||||||
|
let settings = CorsSettings {
|
||||||
|
allowed_origins: vec![
|
||||||
|
"http://localhost:5173".to_string(),
|
||||||
|
"https://sta.example.com".to_string(),
|
||||||
|
],
|
||||||
|
allow_credentials: false,
|
||||||
|
max_age_secs: 3600,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Should successfully convert without panic
|
||||||
|
let _cors: Cors = settings.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "CORS misconfiguration: wildcard origin not allowed with credentials=true")]
|
||||||
|
fn cors_conversion_panics_on_wildcard_with_credentials() {
|
||||||
|
let settings = CorsSettings {
|
||||||
|
allowed_origins: vec!["*".to_string()],
|
||||||
|
allow_credentials: true, // Invalid combination!
|
||||||
|
max_age_secs: 3600,
|
||||||
|
};
|
||||||
|
|
||||||
|
// This should panic due to browser security constraint violation
|
||||||
|
let _cors: Cors = settings.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cors_conversion_with_empty_origins() {
|
||||||
|
let settings = CorsSettings::default();
|
||||||
|
|
||||||
|
// Should successfully convert even with empty origins (restrictive CORS)
|
||||||
|
let _cors: Cors = settings.into();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
//! Settings include application details, Modbus connection parameters, relay configuration,
|
//! Settings include application details, Modbus connection parameters, relay configuration,
|
||||||
//! rate limiting, and environment settings.
|
//! rate limiting, and environment settings.
|
||||||
|
|
||||||
|
mod cors;
|
||||||
|
pub use cors::CorsSettings;
|
||||||
|
|
||||||
/// Application configuration settings.
|
/// Application configuration settings.
|
||||||
///
|
///
|
||||||
/// Loads configuration from YAML files and environment variables.
|
/// Loads configuration from YAML files and environment variables.
|
||||||
@@ -214,54 +217,6 @@ impl Default for RelaySettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default value for CORS max age in seconds (1 hour).
|
|
||||||
const fn default_max_age_secs() -> i32 {
|
|
||||||
3600
|
|
||||||
}
|
|
||||||
|
|
||||||
/// CORS (Cross-Origin Resource Sharing) configuration for the HTTP API.
|
|
||||||
///
|
|
||||||
/// Controls which origins can access the API from browsers. In development,
|
|
||||||
/// use permissive settings (`allowed_origins: ["*"]`). In production, use
|
|
||||||
/// restrictive settings with specific origins.
|
|
||||||
///
|
|
||||||
/// # Security Constraint
|
|
||||||
///
|
|
||||||
/// When `allow_credentials` is `true`, `allowed_origins` MUST NOT contain
|
|
||||||
/// wildcard `"*"`. This is enforced by browser security policy and will be
|
|
||||||
/// validated by the `build_cors()` function.
|
|
||||||
#[derive(Debug, serde::Deserialize, Clone)]
|
|
||||||
pub struct CorsSettings {
|
|
||||||
/// List of allowed origin URLs (e.g., `["https://sta.example.com"]`).
|
|
||||||
///
|
|
||||||
/// Use `["*"]` for development to allow all origins.
|
|
||||||
/// In production, specify exact origins to prevent unauthorized access.
|
|
||||||
#[serde(default)]
|
|
||||||
pub allowed_origins: Vec<String>,
|
|
||||||
/// Whether to allow credentials (cookies, authorization headers) in CORS requests.
|
|
||||||
///
|
|
||||||
/// Set to `true` in production when using Authelia authentication.
|
|
||||||
/// MUST be `false` when using wildcard `"*"` in `allowed_origins`.
|
|
||||||
#[serde(default)]
|
|
||||||
pub allow_credentials: bool,
|
|
||||||
/// Duration in seconds that browsers can cache CORS preflight responses.
|
|
||||||
///
|
|
||||||
/// Typical value: `3600` (1 hour). Higher values reduce preflight requests
|
|
||||||
/// but delay policy changes from taking effect.
|
|
||||||
#[serde(default = "default_max_age_secs")]
|
|
||||||
pub max_age_secs: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CorsSettings {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
allowed_origins: vec![],
|
|
||||||
allow_credentials: false,
|
|
||||||
max_age_secs: 3600,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -388,56 +343,7 @@ mod tests {
|
|||||||
assert_eq!(settings.per_seconds, 60); // default
|
assert_eq!(settings.per_seconds, 60); // default
|
||||||
}
|
}
|
||||||
|
|
||||||
// T009: Tests for CorsSettings struct (TDD - write tests first)
|
// T009: Integration test for CorsSettings within Settings struct
|
||||||
#[test]
|
|
||||||
fn cors_settings_deserialize_from_yaml() {
|
|
||||||
let yaml = r#"
|
|
||||||
allowed_origins:
|
|
||||||
- "http://localhost:5173"
|
|
||||||
- "https://sta.example.com"
|
|
||||||
allow_credentials: true
|
|
||||||
max_age_secs: 7200
|
|
||||||
"#;
|
|
||||||
let settings: CorsSettings = serde_yaml::from_str(yaml).unwrap();
|
|
||||||
assert_eq!(settings.allowed_origins.len(), 2);
|
|
||||||
assert_eq!(settings.allowed_origins[0], "http://localhost:5173");
|
|
||||||
assert_eq!(settings.allowed_origins[1], "https://sta.example.com");
|
|
||||||
assert!(settings.allow_credentials);
|
|
||||||
assert_eq!(settings.max_age_secs, 7200);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cors_settings_default_has_empty_origins() {
|
|
||||||
let settings = CorsSettings::default();
|
|
||||||
assert!(
|
|
||||||
settings.allowed_origins.is_empty(),
|
|
||||||
"Default CorsSettings should have empty allowed_origins for restrictive fail-safe"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!settings.allow_credentials,
|
|
||||||
"Default CorsSettings should have credentials disabled"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
settings.max_age_secs, 3600,
|
|
||||||
"Default CorsSettings should have 1 hour max_age"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cors_settings_with_wildcard_deserializes() {
|
|
||||||
let yaml = r#"
|
|
||||||
allowed_origins:
|
|
||||||
- "*"
|
|
||||||
allow_credentials: false
|
|
||||||
max_age_secs: 3600
|
|
||||||
"#;
|
|
||||||
let settings: CorsSettings = serde_yaml::from_str(yaml).unwrap();
|
|
||||||
assert_eq!(settings.allowed_origins.len(), 1);
|
|
||||||
assert_eq!(settings.allowed_origins[0], "*");
|
|
||||||
assert!(!settings.allow_credentials);
|
|
||||||
assert_eq!(settings.max_age_secs, 3600);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn settings_loads_cors_section_from_yaml() {
|
fn settings_loads_cors_section_from_yaml() {
|
||||||
// Create a temporary settings file with CORS configuration
|
// Create a temporary settings file with CORS configuration
|
||||||
@@ -483,19 +389,4 @@ relay:
|
|||||||
assert!(!settings.cors.allow_credentials);
|
assert!(!settings.cors.allow_credentials);
|
||||||
assert_eq!(settings.cors.max_age_secs, 3600);
|
assert_eq!(settings.cors.max_age_secs, 3600);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cors_settings_deserialize_with_defaults() {
|
|
||||||
// Test partial deserialization using serde defaults
|
|
||||||
let yaml = r#"
|
|
||||||
allowed_origins:
|
|
||||||
- "https://example.com"
|
|
||||||
"#;
|
|
||||||
let settings: CorsSettings = serde_yaml::from_str(yaml).unwrap();
|
|
||||||
assert_eq!(settings.allowed_origins.len(), 1);
|
|
||||||
assert_eq!(settings.allowed_origins[0], "https://example.com");
|
|
||||||
// These should use defaults
|
|
||||||
assert!(!settings.allow_credentials);
|
|
||||||
assert_eq!(settings.max_age_secs, 3600);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -225,131 +225,4 @@ mod tests {
|
|||||||
assert_eq!(app.host(), "127.0.0.1");
|
assert_eq!(app.host(), "127.0.0.1");
|
||||||
assert_eq!(app.port(), 8080);
|
assert_eq!(app.port(), 8080);
|
||||||
}
|
}
|
||||||
|
|
||||||
// T013: Tests for build_cors() function (TDD - write tests FIRST)
|
|
||||||
mod cors_tests {
|
|
||||||
use super::*;
|
|
||||||
use crate::settings::CorsSettings;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic(expected = "CORS misconfiguration")]
|
|
||||||
fn build_cors_with_credentials_and_wildcard_panics() {
|
|
||||||
// GIVEN a CORS configuration with wildcard origin AND credentials enabled
|
|
||||||
let settings = CorsSettings {
|
|
||||||
allowed_origins: vec!["*".to_string()],
|
|
||||||
allow_credentials: true,
|
|
||||||
max_age_secs: 3600,
|
|
||||||
};
|
|
||||||
|
|
||||||
// WHEN build_cors() is called
|
|
||||||
// THEN it should panic with a clear error message
|
|
||||||
let _cors = build_cors(&settings);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_cors_with_wildcard_origin_creates_permissive_cors() {
|
|
||||||
// GIVEN a CORS configuration with wildcard origin
|
|
||||||
let settings = CorsSettings {
|
|
||||||
allowed_origins: vec!["*".to_string()],
|
|
||||||
allow_credentials: false,
|
|
||||||
max_age_secs: 3600,
|
|
||||||
};
|
|
||||||
|
|
||||||
// WHEN build_cors() is called
|
|
||||||
let _cors = build_cors(&settings);
|
|
||||||
|
|
||||||
// THEN it should create a Cors middleware that allows any origin
|
|
||||||
// Note: We can't directly test Cors behavior without integration tests
|
|
||||||
// This test verifies that build_cors() completes without panicking
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_cors_with_specific_origin_creates_restrictive_cors() {
|
|
||||||
// GIVEN a CORS configuration with specific origins
|
|
||||||
let settings = CorsSettings {
|
|
||||||
allowed_origins: vec![
|
|
||||||
"https://sta.example.com".to_string(),
|
|
||||||
"http://localhost:5173".to_string(),
|
|
||||||
],
|
|
||||||
allow_credentials: true,
|
|
||||||
max_age_secs: 3600,
|
|
||||||
};
|
|
||||||
|
|
||||||
// WHEN build_cors() is called
|
|
||||||
let _cors = build_cors(&settings);
|
|
||||||
|
|
||||||
// THEN it should create a Cors middleware that only allows specified origins
|
|
||||||
// Note: We can't directly test Cors behavior without integration tests
|
|
||||||
// This test verifies that build_cors() completes without panicking
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_cors_sets_correct_methods() {
|
|
||||||
// GIVEN a CORS configuration
|
|
||||||
let settings = CorsSettings {
|
|
||||||
allowed_origins: vec!["https://example.com".to_string()],
|
|
||||||
allow_credentials: false,
|
|
||||||
max_age_secs: 3600,
|
|
||||||
};
|
|
||||||
|
|
||||||
// WHEN build_cors() is called
|
|
||||||
let _cors = build_cors(&settings);
|
|
||||||
|
|
||||||
// THEN it should configure the following methods:
|
|
||||||
// GET, POST, PUT, PATCH, DELETE, OPTIONS
|
|
||||||
// Note: Direct method verification requires integration tests
|
|
||||||
// This test ensures build_cors() completes without errors
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_cors_sets_correct_headers() {
|
|
||||||
// GIVEN a CORS configuration
|
|
||||||
let settings = CorsSettings {
|
|
||||||
allowed_origins: vec!["https://example.com".to_string()],
|
|
||||||
allow_credentials: false,
|
|
||||||
max_age_secs: 3600,
|
|
||||||
};
|
|
||||||
|
|
||||||
// WHEN build_cors() is called
|
|
||||||
let _cors = build_cors(&settings);
|
|
||||||
|
|
||||||
// THEN it should configure the following headers:
|
|
||||||
// content-type, authorization
|
|
||||||
// Note: Direct header verification requires integration tests
|
|
||||||
// This test ensures build_cors() completes without errors
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_cors_sets_max_age_from_settings() {
|
|
||||||
// GIVEN a CORS configuration with custom max_age
|
|
||||||
let settings = CorsSettings {
|
|
||||||
allowed_origins: vec!["https://example.com".to_string()],
|
|
||||||
allow_credentials: false,
|
|
||||||
max_age_secs: 7200, // 2 hours
|
|
||||||
};
|
|
||||||
|
|
||||||
// WHEN build_cors() is called
|
|
||||||
let _cors = build_cors(&settings);
|
|
||||||
|
|
||||||
// THEN it should configure max_age to 7200 seconds
|
|
||||||
// Note: Direct max_age verification requires integration tests
|
|
||||||
// This test ensures build_cors() completes without errors
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_cors_with_empty_origins() {
|
|
||||||
// GIVEN a CORS configuration with no allowed origins (restrictive fail-safe)
|
|
||||||
let settings = CorsSettings {
|
|
||||||
allowed_origins: vec![],
|
|
||||||
allow_credentials: false,
|
|
||||||
max_age_secs: 3600,
|
|
||||||
};
|
|
||||||
|
|
||||||
// WHEN build_cors() is called
|
|
||||||
let _cors = build_cors(&settings);
|
|
||||||
|
|
||||||
// THEN it should create a Cors middleware that denies all origins
|
|
||||||
// This test ensures build_cors() handles the fail-safe case
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@
|
|||||||
- **File**: backend/src/startup.rs (in tests module)
|
- **File**: backend/src/startup.rs (in tests module)
|
||||||
- **Complexity**: Medium | **Uncertainty**: Low
|
- **Complexity**: Medium | **Uncertainty**: Low
|
||||||
|
|
||||||
- [ ] **T014** [Setup] [TDD] Implement build_cors() free function in startup.rs
|
- [x] **T014** [Setup] [TDD] Implement build_cors() free function in startup.rs
|
||||||
- Function signature: `fn build_cors(settings: &CorsSettings) -> Cors`
|
- Function signature: `fn build_cors(settings: &CorsSettings) -> Cors`
|
||||||
- Validate: if `allow_credentials=true` AND `allowed_origins` contains "*", panic with clear error message
|
- Validate: if `allow_credentials=true` AND `allowed_origins` contains "*", panic with clear error message
|
||||||
- Iterate over `allowed_origins` and call `cors.allow_origin()` for each
|
- Iterate over `allowed_origins` and call `cors.allow_origin()` for each
|
||||||
|
|||||||
Reference in New Issue
Block a user