Skip to main content

praxis_core/config/
insecure_options.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2024 Praxis Contributors
3
4//! Consolidated security override flags.
5//!
6//! All options default to `false` (secure by default). Each flag
7//! demotes one specific security check from an error to a warning.
8
9use serde::Deserialize;
10
11// -----------------------------------------------------------------------------
12// InsecureOptions
13// -----------------------------------------------------------------------------
14
15/// Consolidated security overrides for Praxis.
16///
17/// Every field defaults to `false`. Setting a flag to `true`
18/// demotes the corresponding security check from an error to a warning.
19///
20/// Only intended for use in development and testing.
21///
22/// ```
23/// use praxis_core::config::InsecureOptions;
24///
25/// let opts = InsecureOptions::default();
26/// assert!(!opts.allow_open_security_filters);
27/// assert!(!opts.allow_private_endpoints);
28/// assert!(!opts.allow_private_health_checks);
29/// assert!(!opts.allow_public_admin);
30/// assert!(!opts.allow_root);
31/// assert!(!opts.allow_tls_without_sni);
32/// assert!(!opts.allow_unbounded_body);
33/// assert!(!opts.csrf_log_only);
34/// assert!(!opts.skip_pipeline_validation);
35/// ```
36///
37/// ```
38/// use praxis_core::config::InsecureOptions;
39///
40/// let opts: InsecureOptions =
41///     serde_yaml::from_str("allow_root: true\nallow_public_admin: true\n").unwrap();
42/// assert!(opts.allow_root);
43/// assert!(opts.allow_public_admin);
44/// assert!(!opts.allow_unbounded_body);
45/// ```
46#[expect(clippy::struct_excessive_bools, reason = "security override flags")]
47#[derive(Debug, Clone, Default, Deserialize, serde::Serialize)]
48#[serde(default, deny_unknown_fields)]
49pub struct InsecureOptions {
50    /// Allow security-critical filters to use `failure_mode: open`,
51    /// demoting the validation error to a warning.
52    pub allow_open_security_filters: bool,
53
54    /// Allow cluster endpoints to resolve to loopback, link-local,
55    /// or cloud metadata addresses.
56    pub allow_private_endpoints: bool,
57
58    /// Allow health checks to loopback/metadata addresses.
59    pub allow_private_health_checks: bool,
60
61    /// Allow admin endpoint on `0.0.0.0` / `[::]`.
62    pub allow_public_admin: bool,
63
64    /// Allow running as root (UID 0).
65    pub allow_root: bool,
66
67    /// Allow TLS without SNI hostname verification.
68    pub allow_tls_without_sni: bool,
69
70    /// Allow startup without `body_limits.max_request_bytes` or
71    /// `body_limits.max_response_bytes` configured. Without this
72    /// flag, missing body limits are a hard validation error.
73    pub allow_unbounded_body: bool,
74
75    /// Run the CSRF filter in log-only mode: evaluate all rules
76    /// but log violations as warnings instead of rejecting requests.
77    pub csrf_log_only: bool,
78
79    /// Skip pipeline ordering validation. This blanket-disables all
80    /// structural safety checks including conditional security filters
81    /// and `failure_mode: open` detection.
82    pub skip_pipeline_validation: bool,
83}
84
85// -----------------------------------------------------------------------------
86// Tests
87// -----------------------------------------------------------------------------
88
89#[cfg(test)]
90#[expect(clippy::allow_attributes, reason = "blanket test suppressions")]
91#[allow(
92    clippy::unwrap_used,
93    clippy::expect_used,
94    clippy::indexing_slicing,
95    clippy::needless_raw_strings,
96    clippy::needless_raw_string_hashes,
97    reason = "tests use unwrap/expect/indexing/raw strings for brevity"
98)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn all_flags_default_to_false() {
104        let opts = InsecureOptions::default();
105        assert!(
106            !opts.allow_open_security_filters,
107            "allow_open_security_filters should default to false"
108        );
109        assert!(
110            !opts.allow_private_endpoints,
111            "allow_private_endpoints should default to false"
112        );
113        assert!(
114            !opts.allow_private_health_checks,
115            "allow_private_health_checks should default to false"
116        );
117        assert!(!opts.allow_public_admin, "allow_public_admin should default to false");
118        assert!(!opts.allow_root, "allow_root should default to false");
119        assert!(
120            !opts.allow_tls_without_sni,
121            "allow_tls_without_sni should default to false"
122        );
123        assert!(
124            !opts.allow_unbounded_body,
125            "allow_unbounded_body should default to false"
126        );
127        assert!(!opts.csrf_log_only, "csrf_log_only should default to false");
128        assert!(
129            !opts.skip_pipeline_validation,
130            "skip_pipeline_validation should default to false"
131        );
132    }
133
134    #[test]
135    fn deserializes_partial_overrides() {
136        let yaml = "allow_root: true\nskip_pipeline_validation: true\n";
137        let opts: InsecureOptions = serde_yaml::from_str(yaml).unwrap();
138        assert!(opts.allow_root, "allow_root should be true");
139        assert!(opts.skip_pipeline_validation, "skip_pipeline_validation should be true");
140        assert!(!opts.allow_public_admin, "allow_public_admin should still be false");
141    }
142
143    #[test]
144    fn deserializes_empty_to_defaults() {
145        let opts: InsecureOptions = serde_yaml::from_str("{}").unwrap();
146        assert!(!opts.allow_root, "empty YAML should produce defaults");
147    }
148}