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 std::collections::HashMap;
16use std::future::Future;
17use std::pin::Pin;
18use async_trait::async_trait;
19use std::{fmt, sync::Arc};
20use once_cell::sync::Lazy;
21use serde::Deserialize;
22use std::sync::RwLock as StdRwLock;
23use crate::core::{ProxyError, ProxyRequest, ProxyResponse};
24use crate::{debug_fmt, error_fmt, info_fmt, trace_fmt};
25use crate::security::oidc::{OidcConfig, OidcProvider};
26
27/// When in the request/response lifecycle should a provider run?
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum SecurityStage {
30    Pre,
31    Post,
32    Both,
33}
34
35impl SecurityStage {
36    pub const fn is_pre(self) -> bool { matches!(self, Self::Pre | Self::Both) }
37    pub const fn is_post(self) -> bool { matches!(self, Self::Post | Self::Both) }
38}
39
40/// A unit of security logic – e.g. BasicAuth, JWT, OIDC, mTLS …
41#[async_trait]
42pub trait SecurityProvider: fmt::Debug + Send + Sync {
43    /// Which phase(s) does this provider participate in?
44    fn stage(&self) -> SecurityStage;
45    /// Name shown in logs / error messages.
46    fn name(&self) -> &str;
47
48    /// Optionally mutate/validate the inbound request *before* routing.
49    async fn pre(
50        &self,
51        request: ProxyRequest,
52    ) -> Result<ProxyRequest, ProxyError> {
53        trace_fmt!("SecurityChain", "Security provider '{}' skipping pre-auth (default implementation)", self.name());
54        Ok(request)
55    }
56
57    /// Optionally inspect/validate the response *after* the upstream call.
58    async fn post(
59        &self,
60        _request: ProxyRequest,
61        response: ProxyResponse,
62    ) -> Result<ProxyResponse, ProxyError> {
63        trace_fmt!("SecurityChain", "Security provider '{}' skipping post-auth (default implementation)", self.name());
64        Ok(response)
65    }
66}
67
68/// Executes all registered providers.
69#[derive(Debug)]
70pub struct SecurityChain {
71    providers: Vec<Arc<dyn SecurityProvider>>,
72}
73
74impl SecurityChain {
75    pub fn new() -> Self {
76        Self { providers: Vec::new() }
77    }
78
79    /// Build from raw config list.
80    pub async fn from_configs(cfgs: Vec<ProviderConfig>) -> Result<Self, ProxyError> {
81        let mut chain = SecurityChain::new();
82
83        debug_fmt!("SecurityChain", "Building security chain from {} provider configs", cfgs.len());
84
85        for c in cfgs {
86            let provider = SecurityProviderFactory::create_provider(&c.type_, c.config).await?;
87            chain.add(provider);
88        }
89
90        Ok(chain)
91    }
92
93    pub fn add(&mut self, p: Arc<dyn SecurityProvider>) { self.providers.push(p); }
94
95    pub async fn apply_pre(
96        &self,
97        mut req: ProxyRequest,
98    ) -> Result<ProxyRequest, ProxyError> {
99        trace_fmt!("SecurityChain", "Applying security pre-auth chain with {} providers", self.providers.len());
100
101        for p in &self.providers {
102            if p.stage().is_pre() {
103                trace_fmt!("SecurityChain", "Running pre-auth provider: {}", p.name());
104                match p.pre(req).await {
105                    Ok(new_req) => {
106                        req = new_req;
107                    },
108                    Err(e) => {
109                        let err = ProxyError::SecurityError(format!("{}: {}", p.name(), e));
110                        error_fmt!("SecurityChain", "Security pre-auth failed: {}", err);
111                        return Err(err);
112                    }
113                }
114            }
115        }
116        Ok(req)
117    }
118
119    pub async fn apply_post(
120        &self,
121        req: ProxyRequest,
122        mut resp: ProxyResponse,
123    ) -> Result<ProxyResponse, ProxyError> {
124        trace_fmt!("SecurityChain", "Applying security post-auth chain with {} providers", self.providers.len());
125
126        for p in &self.providers {
127            if p.stage().is_post() {
128                trace_fmt!("SecurityChain", "Running post-auth provider: {}", p.name());
129                match p.post(req.clone(), resp).await {
130                    Ok(new_resp) => {
131                        resp = new_resp;
132                    },
133                    Err(e) => {
134                        let err = ProxyError::SecurityError(format!("{}: {}", p.name(), e));
135                        error_fmt!("SecurityChain", "Security post-auth failed: {}", err);
136                        return Err(err);
137                    }
138                }
139            }
140        }
141        Ok(resp)
142    }
143}
144
145#[derive(Debug, Deserialize)]
146pub struct ProviderConfig {
147    #[serde(rename = "type")]
148    pub type_: String,
149    pub config: serde_json::Value,
150}
151
152
153/// Constructor signature every dynamic security provider must implement.
154/// Because providers may need to perform async operations (like OIDC discovery),
155/// the constructor returns a pinned, boxed future.
156pub type SecurityProviderConstructor =
157fn(serde_json::Value) -> Pin<Box<dyn Future<Output = Result<Arc<dyn SecurityProvider>, ProxyError>> + Send>>;
158
159
160/// Global registry for security providers.
161static SECURITY_PROVIDER_REGISTRY: Lazy<StdRwLock<HashMap<String, SecurityProviderConstructor>>> =
162    Lazy::new(|| StdRwLock::new(HashMap::new()));
163
164/// Register a security provider under a unique name.
165pub fn register_security_provider(name: &str, ctor: SecurityProviderConstructor) {
166    SECURITY_PROVIDER_REGISTRY
167        .write()
168        .expect("SECURITY_PROVIDER_REGISTRY poisoned")
169        .insert(name.to_string(), ctor);
170}
171
172/// Internal helper to get a registered security provider constructor.
173fn get_registered_security_provider(name: &str) -> Option<SecurityProviderConstructor> {
174    SECURITY_PROVIDER_REGISTRY
175        .read()
176        .expect("SECURITY_PROVIDER_REGISTRY poisoned")
177        .get(name)
178        .copied()
179}
180
181/// Factory for creating security providers based on configuration.
182#[derive(Debug)]
183pub struct SecurityProviderFactory;
184
185impl SecurityProviderFactory {
186    /// Create a security provider based on its type and configuration.
187    pub async fn create_provider(
188        provider_type: &str,
189        config: serde_json::Value,
190    ) -> Result<Arc<dyn SecurityProvider>, ProxyError> {
191        debug_fmt!("SecurityProviderFactory", "Creating security provider of type '{}'", provider_type);
192        if let Some(ctor) = get_registered_security_provider(provider_type) {
193            return ctor(config).await;
194        }
195
196        match provider_type {
197            "oidc" => {
198                let oidc_config: OidcConfig = serde_json::from_value(config)
199                    .map_err(|e| ProxyError::SecurityError(format!("Invalid OIDC provider config: {}", e)))?;
200                let provider = OidcProvider::discover(oidc_config).await?;
201                Ok(Arc::new(provider))
202            }
203            _ => Err(ProxyError::SecurityError(format!("Unknown security provider type: {}", provider_type))),
204        }
205    }
206}