Skip to main content

photon_backend/
broker_security.rs

1//! Broker transport security policy for adapter connect paths.
2
3use crate::{PhotonError, Result};
4
5/// Environment opt-in for plaintext broker endpoints (development/CI only).
6pub const ALLOW_INSECURE_BROKER_ENV: &str = "PHOTON_ALLOW_INSECURE_BROKER";
7
8/// How Photon may connect to an external broker.
9///
10/// Production hosts should use [`Self::RequireTls`]. Plaintext endpoints require an explicit
11/// [`Self::AllowInsecurePlaintext`] opt-in (builder method or [`ALLOW_INSECURE_BROKER_ENV`]).
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum BrokerTransportSecurity {
14    /// Reject plaintext broker URLs/endpoints; prefer TLS (`tls://`, SDK TLS options).
15    #[default]
16    RequireTls,
17    /// Development/CI only: allow `nats://` and other plaintext broker endpoints.
18    AllowInsecurePlaintext,
19}
20
21impl BrokerTransportSecurity {
22    /// Load from [`ALLOW_INSECURE_BROKER_ENV`] (`1`/`true` → insecure; otherwise require TLS).
23    #[must_use]
24    pub fn from_env() -> Self {
25        match std::env::var(ALLOW_INSECURE_BROKER_ENV).as_deref() {
26            Ok("1" | "true" | "TRUE" | "yes" | "YES") => Self::AllowInsecurePlaintext,
27            _ => Self::RequireTls,
28        }
29    }
30
31    /// Returns true when plaintext broker endpoints are permitted.
32    #[must_use]
33    pub const fn allows_plaintext(self) -> bool {
34        matches!(self, Self::AllowInsecurePlaintext)
35    }
36
37    /// Fail closed when `endpoint` looks like plaintext and insecure opt-in is absent.
38    ///
39    /// Recognizes common plaintext schemes: `nats://`, `kafka://`, `ftp://`, bare `host:port`
40    /// without a `tls` / `ssl` / `https` marker. `tls://`, `nats+tls://`, and URLs containing
41    /// `ssl`/`tls` as the scheme are treated as TLS-oriented.
42    ///
43    /// # Errors
44    ///
45    /// Returns [`PhotonError::Internal`] when plaintext would be used without opt-in.
46    pub fn check_endpoint(self, endpoint: &str) -> Result<()> {
47        if self.allows_plaintext() || endpoint_looks_tls(endpoint) {
48            return Ok(());
49        }
50        Err(PhotonError::Internal(format!(
51            "plaintext broker endpoint rejected under BrokerTransportSecurity::RequireTls \
52             (endpoint looks non-TLS). Opt in explicitly with .allow_insecure_plaintext() \
53             or {ALLOW_INSECURE_BROKER_ENV}=1 for development/CI only"
54        )))
55    }
56}
57
58fn endpoint_looks_tls(endpoint: &str) -> bool {
59    let trimmed = endpoint.trim();
60    let lower = trimmed.to_ascii_lowercase();
61    if lower.starts_with("tls://")
62        || lower.starts_with("ssl://")
63        || lower.starts_with("https://")
64        || lower.starts_with("nats+tls://")
65        || lower.starts_with("rediss://")
66    {
67        return true;
68    }
69    // Multi-URL lists: every entry must look TLS-capable when requiring TLS.
70    if trimmed.contains(',') {
71        return trimmed
72            .split(',')
73            .map(str::trim)
74            .filter(|s| !s.is_empty())
75            .all(endpoint_looks_tls);
76    }
77    false
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn require_tls_rejects_nats_plaintext() {
86        let err = BrokerTransportSecurity::RequireTls
87            .check_endpoint("nats://127.0.0.1:4222")
88            .expect_err("plaintext");
89        assert!(err.to_string().contains("plaintext"));
90    }
91
92    #[test]
93    fn require_tls_accepts_tls_scheme() {
94        BrokerTransportSecurity::RequireTls
95            .check_endpoint("tls://broker.example:4222")
96            .expect("tls ok");
97    }
98
99    #[test]
100    fn insecure_allows_nats_plaintext() {
101        BrokerTransportSecurity::AllowInsecurePlaintext
102            .check_endpoint("nats://127.0.0.1:4222")
103            .expect("insecure ok");
104    }
105}