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        Ok(request)
48    }
49
50    /// Optionally inspect/validate the response *after* the upstream call.
51    async fn post(
52        &self,
53        _request: ProxyRequest,
54        response: ProxyResponse,
55    ) -> Result<ProxyResponse, ProxyError> {
56        Ok(response)
57    }
58}
59
60/// Executes all registered providers, honouring bypass-routes.
61#[derive(Debug)]
62pub struct SecurityChain {
63    providers: Vec<Arc<dyn SecurityProvider>>,
64    bypass_routes: Vec<String>,
65}
66
67impl SecurityChain {
68    pub fn new(bypass_routes: Vec<String>) -> Self {
69        Self { providers: Vec::new(), bypass_routes }
70    }
71
72    /// Build from raw config list.
73    pub async fn from_configs(cfgs: Vec<ProviderConfig>) -> Result<Self, ProxyError> {
74        let mut chain = SecurityChain { providers: Vec::new(), bypass_routes: Vec::new() };
75
76        for c in cfgs {
77            match c {
78                ProviderConfig::Oidc { config } => {
79                    let p = OidcProvider::discover(config).await?;
80                    chain.add(Arc::new(p));
81                }
82            }
83        }
84
85        Ok(chain)
86    }
87
88    pub fn add(&mut self, p: Arc<dyn SecurityProvider>) { self.providers.push(p); }
89
90    fn is_bypassed(&self, path: &str) -> bool {
91        self.bypass_routes.iter().any(|p| path.starts_with(p))
92    }
93
94    pub async fn apply_pre(
95        &self,
96        mut req: ProxyRequest,
97    ) -> Result<ProxyRequest, ProxyError> {
98        if self.is_bypassed(&req.path) { return Ok(req); }
99        for p in &self.providers {
100            if p.stage().is_pre() {
101                req = p.pre(req).await
102                    .map_err(|e| ProxyError::SecurityError(format!("{}: {e}", p.name())))?;
103            }
104        }
105        Ok(req)
106    }
107
108    pub async fn apply_post(
109        &self,
110        req: ProxyRequest,
111        mut resp: ProxyResponse,
112    ) -> Result<ProxyResponse, ProxyError> {
113        if self.is_bypassed(&req.path) { return Ok(resp); }
114        for p in &self.providers {
115            if p.stage().is_post() {
116                resp = p.post(req.clone(), resp).await
117                    .map_err(|e| ProxyError::SecurityError(format!("{}: {e}", p.name())))?;
118            }
119        }
120        Ok(resp)
121    }
122}
123
124#[derive(Debug, Deserialize)]
125#[serde(tag = "type", rename_all = "lowercase")]
126pub enum ProviderConfig {
127    Oidc { config: OidcConfig },
128}