Skip to main content

securitydept_basic_auth_context/config/
validator.rs

1use securitydept_creds::BasicAuthCred;
2use securitydept_utils::redirect::RedirectTargetConfig;
3use serde::{Deserialize, Serialize};
4
5use super::BasicAuthContextConfigSource;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct BasicAuthContextConfigValidationError {
9    pub field_path: String,
10    pub code: String,
11    pub message: String,
12}
13
14impl BasicAuthContextConfigValidationError {
15    pub fn new(
16        field_path: impl Into<String>,
17        code: impl Into<String>,
18        message: impl Into<String>,
19    ) -> Self {
20        Self {
21            field_path: field_path.into(),
22            code: code.into(),
23            message: message.into(),
24        }
25    }
26}
27
28impl std::fmt::Display for BasicAuthContextConfigValidationError {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        write!(
31            f,
32            "basic_auth_context config validation failed for {} ({}): {}",
33            self.field_path, self.code, self.message
34        )
35    }
36}
37
38impl std::error::Error for BasicAuthContextConfigValidationError {}
39
40pub trait BasicAuthContextConfigValidator<Creds>
41where
42    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
43{
44    fn validate_basic_auth_context_config<S>(
45        &self,
46        config: &S,
47    ) -> Result<(), BasicAuthContextConfigValidationError>
48    where
49        S: BasicAuthContextConfigSource<Creds> + ?Sized;
50}
51
52#[derive(Debug, Clone, Copy, Default)]
53pub struct NoopBasicAuthContextConfigValidator;
54
55impl<Creds> BasicAuthContextConfigValidator<Creds> for NoopBasicAuthContextConfigValidator
56where
57    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
58{
59    fn validate_basic_auth_context_config<S>(
60        &self,
61        _config: &S,
62    ) -> Result<(), BasicAuthContextConfigValidationError>
63    where
64        S: BasicAuthContextConfigSource<Creds> + ?Sized,
65    {
66        Ok(())
67    }
68}
69
70impl<Creds, V> BasicAuthContextConfigValidator<Creds> for &V
71where
72    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
73    V: BasicAuthContextConfigValidator<Creds> + ?Sized,
74{
75    fn validate_basic_auth_context_config<S>(
76        &self,
77        config: &S,
78    ) -> Result<(), BasicAuthContextConfigValidationError>
79    where
80        S: BasicAuthContextConfigSource<Creds> + ?Sized,
81    {
82        (*self).validate_basic_auth_context_config(config)
83    }
84}
85
86impl<Creds, V> BasicAuthContextConfigValidator<Creds> for [V]
87where
88    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
89    V: BasicAuthContextConfigValidator<Creds>,
90{
91    fn validate_basic_auth_context_config<S>(
92        &self,
93        config: &S,
94    ) -> Result<(), BasicAuthContextConfigValidationError>
95    where
96        S: BasicAuthContextConfigSource<Creds> + ?Sized,
97    {
98        for validator in self {
99            validator.validate_basic_auth_context_config(config)?;
100        }
101
102        Ok(())
103    }
104}
105
106impl<Creds, V, const N: usize> BasicAuthContextConfigValidator<Creds> for [V; N]
107where
108    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
109    V: BasicAuthContextConfigValidator<Creds>,
110{
111    fn validate_basic_auth_context_config<S>(
112        &self,
113        config: &S,
114    ) -> Result<(), BasicAuthContextConfigValidationError>
115    where
116        S: BasicAuthContextConfigSource<Creds> + ?Sized,
117    {
118        self.as_slice().validate_basic_auth_context_config(config)
119    }
120}
121
122impl<Creds, V> BasicAuthContextConfigValidator<Creds> for Vec<V>
123where
124    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
125    V: BasicAuthContextConfigValidator<Creds>,
126{
127    fn validate_basic_auth_context_config<S>(
128        &self,
129        config: &S,
130    ) -> Result<(), BasicAuthContextConfigValidationError>
131    where
132        S: BasicAuthContextConfigSource<Creds> + ?Sized,
133    {
134        self.as_slice().validate_basic_auth_context_config(config)
135    }
136}
137
138impl<Creds, A, B> BasicAuthContextConfigValidator<Creds> for (A, B)
139where
140    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
141    A: BasicAuthContextConfigValidator<Creds>,
142    B: BasicAuthContextConfigValidator<Creds>,
143{
144    fn validate_basic_auth_context_config<S>(
145        &self,
146        config: &S,
147    ) -> Result<(), BasicAuthContextConfigValidationError>
148    where
149        S: BasicAuthContextConfigSource<Creds> + ?Sized,
150    {
151        self.0.validate_basic_auth_context_config(config)?;
152        self.1.validate_basic_auth_context_config(config)
153    }
154}
155
156impl<Creds, A, B, C> BasicAuthContextConfigValidator<Creds> for (A, B, C)
157where
158    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
159    A: BasicAuthContextConfigValidator<Creds>,
160    B: BasicAuthContextConfigValidator<Creds>,
161    C: BasicAuthContextConfigValidator<Creds>,
162{
163    fn validate_basic_auth_context_config<S>(
164        &self,
165        config: &S,
166    ) -> Result<(), BasicAuthContextConfigValidationError>
167    where
168        S: BasicAuthContextConfigSource<Creds> + ?Sized,
169    {
170        self.0.validate_basic_auth_context_config(config)?;
171        self.1.validate_basic_auth_context_config(config)?;
172        self.2.validate_basic_auth_context_config(config)
173    }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct BasicAuthContextFixedSingleZonePathValidator {
178    zone_prefix: String,
179    login_subpath: String,
180    logout_subpath: String,
181}
182
183impl BasicAuthContextFixedSingleZonePathValidator {
184    pub fn new(
185        zone_prefix: impl Into<String>,
186        login_subpath: impl Into<String>,
187        logout_subpath: impl Into<String>,
188    ) -> Self {
189        Self {
190            zone_prefix: zone_prefix.into(),
191            login_subpath: login_subpath.into(),
192            logout_subpath: logout_subpath.into(),
193        }
194    }
195}
196
197impl<Creds> BasicAuthContextConfigValidator<Creds> for BasicAuthContextFixedSingleZonePathValidator
198where
199    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
200{
201    fn validate_basic_auth_context_config<S>(
202        &self,
203        config: &S,
204    ) -> Result<(), BasicAuthContextConfigValidationError>
205    where
206        S: BasicAuthContextConfigSource<Creds> + ?Sized,
207    {
208        if config.zones_config().len() != 1 {
209            return Err(BasicAuthContextConfigValidationError::new(
210                "zones",
211                "fixed_single_zone_required",
212                "expected exactly one basic-auth zone for the fixed single-zone host policy",
213            ));
214        }
215
216        let zone = &config.zones_config()[0];
217        if zone.zone_prefix != self.zone_prefix {
218            return Err(BasicAuthContextConfigValidationError::new(
219                "zones[0].zone_prefix",
220                "fixed_zone_path_conflict",
221                format!(
222                    "basic-auth zone_prefix is fixed by the host to {}",
223                    self.zone_prefix
224                ),
225            ));
226        }
227        if zone.login_subpath != self.login_subpath {
228            return Err(BasicAuthContextConfigValidationError::new(
229                "zones[0].login_subpath",
230                "fixed_zone_path_conflict",
231                format!(
232                    "basic-auth login_subpath is fixed by the host to {}",
233                    self.login_subpath
234                ),
235            ));
236        }
237        if zone.logout_subpath != self.logout_subpath {
238            return Err(BasicAuthContextConfigValidationError::new(
239                "zones[0].logout_subpath",
240                "fixed_zone_path_conflict",
241                format!(
242                    "basic-auth logout_subpath is fixed by the host to {}",
243                    self.logout_subpath
244                ),
245            ));
246        }
247
248        Ok(())
249    }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub struct BasicAuthContextFixedPostAuthRedirectValidator {
254    post_auth_redirect: RedirectTargetConfig,
255}
256
257impl BasicAuthContextFixedPostAuthRedirectValidator {
258    pub fn new(post_auth_redirect: RedirectTargetConfig) -> Self {
259        Self { post_auth_redirect }
260    }
261}
262
263impl<Creds> BasicAuthContextConfigValidator<Creds>
264    for BasicAuthContextFixedPostAuthRedirectValidator
265where
266    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
267{
268    fn validate_basic_auth_context_config<S>(
269        &self,
270        config: &S,
271    ) -> Result<(), BasicAuthContextConfigValidationError>
272    where
273        S: BasicAuthContextConfigSource<Creds> + ?Sized,
274    {
275        if config.post_auth_redirect_config() != &self.post_auth_redirect {
276            return Err(BasicAuthContextConfigValidationError::new(
277                "post_auth_redirect",
278                "fixed_post_auth_redirect_conflict",
279                "basic-auth post_auth_redirect is fixed by the host and cannot be overridden",
280            ));
281        }
282
283        Ok(())
284    }
285}
286
287#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
288pub struct BasicAuthContextRejectZonePostAuthRedirectOverrideValidator;
289
290impl<Creds> BasicAuthContextConfigValidator<Creds>
291    for BasicAuthContextRejectZonePostAuthRedirectOverrideValidator
292where
293    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
294{
295    fn validate_basic_auth_context_config<S>(
296        &self,
297        config: &S,
298    ) -> Result<(), BasicAuthContextConfigValidationError>
299    where
300        S: BasicAuthContextConfigSource<Creds> + ?Sized,
301    {
302        if let Some((index, _)) = config
303            .zones_config()
304            .iter()
305            .enumerate()
306            .find(|(_, zone)| zone.post_auth_redirect.is_some())
307        {
308            return Err(BasicAuthContextConfigValidationError::new(
309                format!("zones[{index}].post_auth_redirect"),
310                "zone_post_auth_redirect_override_forbidden",
311                "zone-level basic-auth post_auth_redirect overrides are forbidden by the host",
312            ));
313        }
314
315        Ok(())
316    }
317}