foxy/security/
basic.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//! Basic authentication provider.
6
7use async_trait::async_trait;
8use base64::{engine::general_purpose, Engine as _};
9use globset::{Glob, GlobSet, GlobSetBuilder};
10use serde::Deserialize;
11use crate::{
12    core::{ProxyError, ProxyRequest},
13    debug_fmt, error_fmt, security::{SecurityProvider, SecurityStage}, trace_fmt, warn_fmt,
14};
15
16const BASIC: &str = "basic ";
17
18#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
19pub struct RouteRuleConfig {
20    pub methods: Vec<String>,
21    pub path: String,
22}
23
24#[derive(Debug)]
25struct RouteRule {
26    methods: Vec<String>,
27    paths: GlobSet,
28}
29
30impl RouteRule {
31    fn matches(&self, method: &str, path: &str) -> bool {
32        let method_match = self.methods.iter().any(|m| m == "*" || m == method);
33        let path_match = self.paths.is_match(path);
34        
35        trace_fmt!("BasicAuthProvider", "Basic Auth bypass rule check: method={} path={} -> method_match={} path_match={}", 
36            method, path, method_match, path_match);
37            
38        method_match && path_match
39    }
40}
41
42/// Configuration for the Basic Auth provider.
43#[derive(Debug, Clone, Deserialize, serde::Serialize)]
44pub struct BasicAuthConfig {
45    /// List of valid username:password pairs.
46    pub credentials: Vec<String>,
47    /// Routes to bypass authentication for.
48    #[serde(default)]
49    pub bypass: Vec<RouteRuleConfig>,
50}
51
52/// Basic authentication security provider.
53#[derive(Debug)]
54pub struct BasicAuthProvider {
55    valid_credentials: Vec<(String, String)>,
56    rules: Vec<RouteRule>,
57}
58
59impl BasicAuthProvider {
60    pub fn new(cfg: BasicAuthConfig) -> Result<Self, ProxyError> {
61        let mut valid_credentials = Vec::new();
62        for cred_pair in cfg.credentials {
63            let parts: Vec<&str> = cred_pair.splitn(2, ':').collect();
64            if parts.len() == 2 {
65                valid_credentials.push((parts[0].to_string(), parts[1].to_string()));
66            } else {
67                let err = ProxyError::SecurityError(format!("Invalid credential format: {cred_pair}"));
68                error_fmt!("BasicAuthProvider", "{}", err);
69                return Err(err);
70            }
71        }
72
73        let mut rules = Vec::with_capacity(cfg.bypass.len());
74        for raw in cfg.bypass {
75            let mut builder = GlobSetBuilder::new();
76            match Glob::new(&raw.path) {
77                Ok(glob) => {
78                    builder.add(glob);
79                    rules.push(RouteRule {
80                        methods: raw.methods.iter().map(|m| m.to_ascii_uppercase()).collect(),
81                        paths: match builder.build() {
82                            Ok(set) => set,
83                            Err(e) => {
84                                let err = ProxyError::SecurityError(
85                                    format!("Failed to build glob set for path {}: {}", raw.path, e)
86                                );
87                                error_fmt!("BasicAuthProvider", "{}", err);
88                                return Err(err);
89                            }
90                        },
91                    });
92                    debug_fmt!("BasicAuthProvider", "Added Basic Auth bypass rule: methods={:?}, path={}", raw.methods, raw.path);
93                },
94                Err(e) => {
95                    let err = ProxyError::SecurityError(
96                        format!("Invalid glob pattern in bypass rule: {e}")
97                    );
98                    error_fmt!("BasicAuthProvider", "{}", err);
99                    return Err(err);
100                }
101            }
102        }
103
104        Ok(Self {
105            valid_credentials,
106            rules,
107        })
108    }
109
110    #[inline]
111    fn is_bypassed(&self, method: &str, path: &str) -> bool {
112        let bypassed = self.rules.iter().any(|r| r.matches(method, path));
113        if bypassed {
114            debug_fmt!("BasicAuthProvider", "Basic Auth bypass for {} {}", method, path);
115        }
116        bypassed
117    }
118}
119
120#[async_trait]
121impl SecurityProvider for BasicAuthProvider {
122    fn name(&self) -> &str { "Basic" }
123
124    fn stage(&self) -> SecurityStage { SecurityStage::Pre }
125
126    async fn pre(&self, req: ProxyRequest) -> Result<ProxyRequest, ProxyError> {
127        // 0) Bypass?
128        if self.is_bypassed(&req.method.to_string(), &req.path) {
129            debug_fmt!("BasicAuthProvider", "Basic Auth bypass for {} {}", req.method, req.path);
130            return Ok(req);
131        }
132
133        debug_fmt!("BasicAuthProvider", "Basic Auth validating request: {} {}", req.method, req.path);
134
135        // 1) Extract Authorization header
136        let auth_header = match req.headers.get("authorization") {
137            Some(h) => match h.to_str() {
138                Ok(s) => s,
139                Err(e) => {
140                    let err = ProxyError::SecurityError(
141                        format!("Invalid authorization header: {e}")
142                    );
143                    warn_fmt!("BasicAuthProvider", "{}", err);
144                    return Err(err);
145                }
146            },
147            None => {
148                let err = ProxyError::SecurityError("Missing authorization header".to_string());
149                warn_fmt!("BasicAuthProvider", "{}", err);
150                return Err(err);
151            }
152        };
153
154        if !auth_header.to_lowercase().starts_with(BASIC) {
155            let err = ProxyError::SecurityError(
156                format!("Invalid authorization scheme: expected 'Basic', got '{}'", 
157                    auth_header.split_whitespace().next().unwrap_or(""))
158            );
159            warn_fmt!("BasicAuthProvider", "{}", err);
160            return Err(err);
161        }
162
163        let encoded_credentials = &auth_header[BASIC.len()..];
164        if encoded_credentials.is_empty() {
165            let err = ProxyError::SecurityError("Empty basic auth credentials".to_string());
166            warn_fmt!("BasicAuthProvider", "{}", err);
167            return Err(err);
168        }
169
170        // 2) Decode credentials
171        let decoded_credentials = match general_purpose::STANDARD.decode(encoded_credentials) {
172            Ok(bytes) => match String::from_utf8(bytes) {
173                Ok(s) => s,
174                Err(e) => {
175                    let err = ProxyError::SecurityError(format!("Invalid UTF-8 in credentials: {e}"));
176                    warn_fmt!("BasicAuthProvider", "{}", err);
177                    return Err(err);
178                }
179            },
180            Err(e) => {
181                let err = ProxyError::SecurityError(format!("Failed to base64 decode credentials: {e}"));
182                warn_fmt!("BasicAuthProvider", "{}", err);
183                return Err(err);
184            }
185        };
186
187        let parts: Vec<&str> = decoded_credentials.splitn(2, ':').collect();
188        if parts.len() != 2 {
189            let err = ProxyError::SecurityError("Invalid basic auth credential format".to_string());
190            warn_fmt!("BasicAuthProvider", "{}", err);
191            return Err(err);
192        }
193        let username = parts[0];
194        let password = parts[1];
195
196        // 3) Validate credentials
197        if self.valid_credentials.iter().any(|(u, p)| u == username && p == password) {
198            debug_fmt!("BasicAuthProvider", "Basic Auth validation successful for user: {}", username);
199            Ok(req)
200        } else {
201            let err = ProxyError::SecurityError("Invalid basic auth credentials".to_string());
202            warn_fmt!("BasicAuthProvider", "{}", err);
203            Err(err)
204        }
205    }
206}