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 oidc;
11
12#[cfg(test)]
13mod tests;
14
15use async_trait::async_trait;
16use std::{fmt, sync::Arc};
17use serde::Deserialize;
18use crate::core::{ProxyError, ProxyRequest, ProxyResponse};
19use crate::security::oidc::{OidcConfig, OidcProvider};
20
21/// When in the request/response lifecycle should a provider run?
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum SecurityStage {
24    Pre,
25    Post,
26    Both,
27}
28
29impl SecurityStage {
30    pub const fn is_pre(self) -> bool { matches!(self, Self::Pre | Self::Both) }
31    pub const fn is_post(self) -> bool { matches!(self, Self::Post | Self::Both) }
32}
33
34/// A unit of security logic – e.g. BasicAuth, JWT, OIDC, mTLS …
35#[async_trait]
36pub trait SecurityProvider: fmt::Debug + Send + Sync {
37    /// Which phase(s) does this provider participate in?
38    fn stage(&self) -> SecurityStage;
39    /// Name shown in logs / error messages.
40    fn name(&self) -> &str;
41
42    /// Optionally mutate/validate the inbound request *before* routing.
43    async fn pre(
44        &self,
45        request: ProxyRequest,
46    ) -> Result<ProxyRequest, ProxyError> {
47        log::trace!("Security provider '{}' skipping pre-auth (default implementation)", self.name());
48        Ok(request)
49    }
50
51    /// Optionally inspect/validate the response *after* the upstream call.
52    async fn post(
53        &self,
54        _request: ProxyRequest,
55        response: ProxyResponse,
56    ) -> Result<ProxyResponse, ProxyError> {
57        log::trace!("Security provider '{}' skipping post-auth (default implementation)", self.name());
58        Ok(response)
59    }
60}
61
62/// Executes all registered providers, honouring bypass-routes.
63#[derive(Debug)]
64pub struct SecurityChain {
65    providers: Vec<Arc<dyn SecurityProvider>>,
66    bypass_routes: Vec<String>,
67}
68
69impl SecurityChain {
70    pub fn new(bypass_routes: Vec<String>) -> Self {
71        Self { providers: Vec::new(), bypass_routes }
72    }
73
74    /// Build from raw config list.
75    pub async fn from_configs(cfgs: Vec<ProviderConfig>) -> Result<Self, ProxyError> {
76        let mut chain = SecurityChain { providers: Vec::new(), bypass_routes: Vec::new() };
77
78        log::debug!("Building security chain from {} provider configs", cfgs.len());
79        
80        for c in cfgs {
81            match c {
82                ProviderConfig::Oidc { config } => {
83                    log::debug!("Initializing OIDC provider with issuer: {}", config.issuer_uri);
84                    match OidcProvider::discover(config).await {
85                        Ok(p) => {
86                            log::info!("Successfully initialized OIDC provider");
87                            chain.add(Arc::new(p));
88                        },
89                        Err(e) => {
90                            log::error!("Failed to initialize OIDC provider: {}", e);
91                            return Err(e);
92                        }
93                    }
94                }
95            }
96        }
97
98        Ok(chain)
99    }
100
101    pub fn add(&mut self, p: Arc<dyn SecurityProvider>) { self.providers.push(p); }
102
103    fn is_bypassed(&self, path: &str) -> bool {
104        let bypassed = self.bypass_routes.iter().any(|p| path.starts_with(p));
105        if bypassed {
106            log::debug!("Security bypass for path: {}", path);
107        }
108        bypassed
109    }
110
111    pub async fn apply_pre(
112        &self,
113        mut req: ProxyRequest,
114    ) -> Result<ProxyRequest, ProxyError> {
115        if self.is_bypassed(&req.path) { 
116            return Ok(req); 
117        }
118        
119        log::trace!("Applying security pre-auth chain with {} providers", self.providers.len());
120        
121        for p in &self.providers {
122            if p.stage().is_pre() {
123                log::trace!("Running pre-auth provider: {}", p.name());
124                match p.pre(req).await {
125                    Ok(new_req) => {
126                        req = new_req;
127                    },
128                    Err(e) => {
129                        let err = ProxyError::SecurityError(format!("{}: {}", p.name(), e));
130                        log::error!("Security pre-auth failed: {}", err);
131                        return Err(err);
132                    }
133                }
134            }
135        }
136        Ok(req)
137    }
138
139    pub async fn apply_post(
140        &self,
141        req: ProxyRequest,
142        mut resp: ProxyResponse,
143    ) -> Result<ProxyResponse, ProxyError> {
144        if self.is_bypassed(&req.path) { 
145            return Ok(resp); 
146        }
147        
148        log::trace!("Applying security post-auth chain with {} providers", self.providers.len());
149        
150        for p in &self.providers {
151            if p.stage().is_post() {
152                log::trace!("Running post-auth provider: {}", p.name());
153                match p.post(req.clone(), resp).await {
154                    Ok(new_resp) => {
155                        resp = new_resp;
156                    },
157                    Err(e) => {
158                        let err = ProxyError::SecurityError(format!("{}: {}", p.name(), e));
159                        log::error!("Security post-auth failed: {}", err);
160                        return Err(err);
161                    }
162                }
163            }
164        }
165        Ok(resp)
166    }
167}
168
169#[derive(Debug, Deserialize)]
170#[serde(tag = "type", rename_all = "lowercase")]
171pub enum ProviderConfig {
172    Oidc { config: OidcConfig },
173}