foxy/security/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Security subsystem – runs before/after the main filter pipeline.
6//!
7//! Initially ships with *zero* providers; downstream crates add their own by
8//! implementing [`SecurityProvider`] and registering them on [`ProxyCore`].
9
10pub mod basic;
11pub mod oidc;
12
13#[cfg(test)]
14#[path = "../../tests/unit/security/tests.rs"]
15mod tests;
16
17use crate::core::{ProxyError, ProxyRequest, ProxyResponse};
18use crate::security::basic::{BasicAuthConfig, BasicAuthProvider};
19use crate::security::oidc::{OidcConfig, OidcProvider};
20use crate::{debug_fmt, error_fmt, trace_fmt};
21use async_trait::async_trait;
22use once_cell::sync::Lazy;
23use serde::Deserialize;
24use std::collections::HashMap;
25use std::future::Future;
26use std::pin::Pin;
27use std::sync::RwLock as StdRwLock;
28use std::{fmt, sync::Arc};
29
30/// When in the request/response lifecycle should a provider run?
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum SecurityStage {
33    Pre,
34    Post,
35    Both,
36}
37
38impl SecurityStage {
39    pub const fn is_pre(self) -> bool {
40        matches!(self, Self::Pre | Self::Both)
41    }
42    pub const fn is_post(self) -> bool {
43        matches!(self, Self::Post | Self::Both)
44    }
45}
46
47/// A unit of security logic – e.g. BasicAuth, JWT, OIDC, mTLS …
48#[async_trait]
49pub trait SecurityProvider: fmt::Debug + Send + Sync {
50    /// Which phase(s) does this provider participate in?
51    fn stage(&self) -> SecurityStage;
52    /// Name shown in logs / error messages.
53    fn name(&self) -> &str;
54
55    /// Optionally mutate/validate the inbound request *before* routing.
56    async fn pre(&self, request: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
57        trace_fmt!(
58            "SecurityChain",
59            "Security provider '{}' skipping pre-auth (default implementation)",
60            self.name()
61        );
62        Ok(request)
63    }
64
65    /// Optionally inspect/validate the response *after* the upstream call.
66    async fn post(
67        &self,
68        _request: ProxyRequest,
69        response: ProxyResponse,
70    ) -> Result<ProxyResponse, ProxyError> {
71        trace_fmt!(
72            "SecurityChain",
73            "Security provider '{}' skipping post-auth (default implementation)",
74            self.name()
75        );
76        Ok(response)
77    }
78}
79
80/// Executes all registered providers.
81#[derive(Debug)]
82pub struct SecurityChain {
83    providers: Vec<Arc<dyn SecurityProvider>>,
84}
85
86impl Default for SecurityChain {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92impl SecurityChain {
93    pub fn new() -> Self {
94        Self {
95            providers: Vec::new(),
96        }
97    }
98
99    /// Build from raw config list.
100    pub async fn from_configs(cfgs: Vec<ProviderConfig>) -> Result<Self, ProxyError> {
101        let mut chain = SecurityChain::new();
102
103        debug_fmt!(
104            "SecurityChain",
105            "Building security chain from {} provider configs",
106            cfgs.len()
107        );
108
109        for c in cfgs {
110            let provider = SecurityProviderFactory::create_provider(&c.type_, c.config).await?;
111            chain.add(provider);
112        }
113
114        Ok(chain)
115    }
116
117    pub fn add(&mut self, p: Arc<dyn SecurityProvider>) {
118        self.providers.push(p);
119    }
120
121    pub async fn apply_pre(&self, mut req: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
122        trace_fmt!(
123            "SecurityChain",
124            "Applying security pre-auth chain with {} providers",
125            self.providers.len()
126        );
127
128        for p in &self.providers {
129            if p.stage().is_pre() {
130                trace_fmt!("SecurityChain", "Running pre-auth provider: {}", p.name());
131                match p.pre(req).await {
132                    Ok(new_req) => {
133                        req = new_req;
134                    }
135                    Err(e) => {
136                        let err = ProxyError::SecurityError(format!("{}: {}", p.name(), e));
137                        error_fmt!("SecurityChain", "Security pre-auth failed: {}", err);
138                        return Err(err);
139                    }
140                }
141            }
142        }
143        Ok(req)
144    }
145
146    pub async fn apply_post(
147        &self,
148        req: ProxyRequest,
149        mut resp: ProxyResponse,
150    ) -> Result<ProxyResponse, ProxyError> {
151        trace_fmt!(
152            "SecurityChain",
153            "Applying security post-auth chain with {} providers",
154            self.providers.len()
155        );
156
157        for p in &self.providers {
158            if p.stage().is_post() {
159                trace_fmt!("SecurityChain", "Running post-auth provider: {}", p.name());
160                match p.post(req.clone(), resp).await {
161                    Ok(new_resp) => {
162                        resp = new_resp;
163                    }
164                    Err(e) => {
165                        let err = ProxyError::SecurityError(format!("{}: {}", p.name(), e));
166                        error_fmt!("SecurityChain", "Security post-auth failed: {}", err);
167                        return Err(err);
168                    }
169                }
170            }
171        }
172        Ok(resp)
173    }
174}
175
176#[derive(Debug, Deserialize)]
177pub struct ProviderConfig {
178    #[serde(rename = "type")]
179    pub type_: String,
180    pub config: serde_json::Value,
181}
182
183/// Constructor signature every dynamic security provider must implement.
184/// Because providers may need to perform async operations (like OIDC discovery),
185/// the constructor returns a pinned, boxed future.
186pub type SecurityProviderConstructor =
187    fn(
188        serde_json::Value,
189    ) -> Pin<Box<dyn Future<Output = Result<Arc<dyn SecurityProvider>, ProxyError>> + Send>>;
190
191/// Global registry for security providers.
192static SECURITY_PROVIDER_REGISTRY: Lazy<StdRwLock<HashMap<String, SecurityProviderConstructor>>> =
193    Lazy::new(|| StdRwLock::new(HashMap::new()));
194
195/// Register a security provider under a unique name.
196pub fn register_security_provider(name: &str, ctor: SecurityProviderConstructor) {
197    SECURITY_PROVIDER_REGISTRY
198        .write()
199        .expect("SECURITY_PROVIDER_REGISTRY poisoned")
200        .insert(name.to_string(), ctor);
201}
202
203/// Internal helper to get a registered security provider constructor.
204fn get_registered_security_provider(name: &str) -> Option<SecurityProviderConstructor> {
205    SECURITY_PROVIDER_REGISTRY
206        .read()
207        .expect("SECURITY_PROVIDER_REGISTRY poisoned")
208        .get(name)
209        .copied()
210}
211
212/// Factory for creating security providers based on configuration.
213#[derive(Debug)]
214pub struct SecurityProviderFactory;
215
216impl SecurityProviderFactory {
217    /// Create a security provider based on its type and configuration.
218    pub async fn create_provider(
219        provider_type: &str,
220        config: serde_json::Value,
221    ) -> Result<Arc<dyn SecurityProvider>, ProxyError> {
222        debug_fmt!(
223            "SecurityProviderFactory",
224            "Creating security provider of type '{}'",
225            provider_type
226        );
227        if let Some(ctor) = get_registered_security_provider(provider_type) {
228            return ctor(config).await;
229        }
230
231        match provider_type {
232            "oidc" => {
233                let oidc_config: OidcConfig = serde_json::from_value(config).map_err(|e| {
234                    ProxyError::SecurityError(format!("Invalid OIDC provider config: {e}"))
235                })?;
236                let provider = OidcProvider::discover(oidc_config).await?;
237                Ok(Arc::new(provider))
238            }
239            "basic" => {
240                let basic_auth_config: BasicAuthConfig =
241                    serde_json::from_value(config).map_err(|e| {
242                        ProxyError::SecurityError(format!(
243                            "Invalid Basic Auth provider config: {e}"
244                        ))
245                    })?;
246                let provider = BasicAuthProvider::new(basic_auth_config)?;
247                Ok(Arc::new(provider))
248            }
249            _ => Err(ProxyError::SecurityError(format!(
250                "Unknown security provider type: {provider_type}"
251            ))),
252        }
253    }
254}