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