Skip to main content

securitydept_basic_auth_context/config/
mod.rs

1use securitydept_creds::{BasicAuthCred, BasicAuthCredsConfig};
2use securitydept_realip::{RealIpAccessConfig, RealIpAccessManager};
3use securitydept_utils::redirect::{RedirectTargetConfig, UriRelativeRedirectTargetResolver};
4use serde::{Deserialize, Serialize};
5use snafu::Snafu;
6use typed_builder::TypedBuilder;
7
8use crate::{BasicAuthContextError, BasicAuthContextResult};
9
10pub mod validator;
11
12pub use validator::{
13    BasicAuthContextConfigValidationError, BasicAuthContextConfigValidator,
14    BasicAuthContextFixedPostAuthRedirectValidator, BasicAuthContextFixedSingleZonePathValidator,
15    BasicAuthContextRejectZonePostAuthRedirectOverrideValidator,
16    NoopBasicAuthContextConfigValidator,
17};
18
19#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
20pub struct BasicAuthZoneConfig {
21    #[builder(default = default_zone_prefix())]
22    #[serde(default = "default_zone_prefix")]
23    pub zone_prefix: String,
24    #[builder(default = default_login_subpath())]
25    #[serde(default = "default_login_subpath")]
26    pub login_subpath: String,
27    #[builder(default = default_logout_subpath())]
28    #[serde(default = "default_logout_subpath")]
29    pub logout_subpath: String,
30    #[builder(default, setter(strip_option))]
31    #[serde(default)]
32    pub realm: Option<String>,
33    #[serde(default)]
34    #[builder(default, setter(strip_option))]
35    pub post_auth_redirect: Option<RedirectTargetConfig>,
36}
37
38impl Default for BasicAuthZoneConfig {
39    fn default() -> Self {
40        Self {
41            zone_prefix: default_zone_prefix(),
42            login_subpath: default_login_subpath(),
43            logout_subpath: default_logout_subpath(),
44            realm: None,
45            post_auth_redirect: None,
46        }
47    }
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, TypedBuilder)]
51pub struct BasicAuthContextConfig<Creds>
52where
53    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
54{
55    #[serde(
56        flatten,
57        bound = "Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>",
58        default = "BasicAuthCredsConfig::default"
59    )]
60    #[builder(default = BasicAuthCredsConfig::default())]
61    pub creds: BasicAuthCredsConfig<Creds>,
62    #[serde(default)]
63    #[builder(default, setter(strip_option))]
64    pub real_ip_access: Option<RealIpAccessConfig>,
65    #[serde(default)]
66    #[builder(default = Vec::new())]
67    pub zones: Vec<BasicAuthZoneConfig>,
68    #[serde(default)]
69    #[builder(default, setter(strip_option))]
70    pub realm: Option<String>,
71    #[serde(default = "default_post_auth_redirect")]
72    #[builder(default = default_post_auth_redirect())]
73    pub post_auth_redirect: RedirectTargetConfig,
74}
75
76impl<Creds> Default for BasicAuthContextConfig<Creds>
77where
78    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
79{
80    fn default() -> Self {
81        Self {
82            creds: BasicAuthCredsConfig::default(),
83            post_auth_redirect: default_post_auth_redirect(),
84            real_ip_access: None,
85            zones: Vec::new(),
86            realm: None,
87        }
88    }
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct ResolvedBasicAuthZoneConfig {
93    pub zone_prefix: String,
94    pub login_subpath: String,
95    pub logout_subpath: String,
96    pub realm: String,
97    pub post_auth_redirect: RedirectTargetConfig,
98}
99
100#[derive(Debug, Clone)]
101pub struct ResolvedBasicAuthContextConfig<Creds>
102where
103    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
104{
105    pub creds: BasicAuthCredsConfig<Creds>,
106    pub real_ip_access: Option<RealIpAccessConfig>,
107    pub zones: Vec<ResolvedBasicAuthZoneConfig>,
108    pub realm: String,
109    pub post_auth_redirect: RedirectTargetConfig,
110}
111
112#[derive(Debug, Snafu)]
113pub enum BasicAuthContextConfigBuildError {
114    #[snafu(transparent)]
115    Validation {
116        source: BasicAuthContextConfigValidationError,
117    },
118    #[snafu(transparent)]
119    Context { source: BasicAuthContextError },
120}
121
122pub trait BasicAuthContextConfigSource<Creds>
123where
124    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
125{
126    fn creds_config(&self) -> &BasicAuthCredsConfig<Creds>;
127    fn real_ip_access_config(&self) -> Option<&RealIpAccessConfig>;
128    fn zones_config(&self) -> &[BasicAuthZoneConfig];
129    fn realm_config(&self) -> Option<&str>;
130    fn post_auth_redirect_config(&self) -> &RedirectTargetConfig;
131
132    fn resolve_creds_config(&self) -> BasicAuthCredsConfig<Creds>
133    where
134        Creds: Clone,
135    {
136        self.creds_config().clone()
137    }
138
139    fn resolve_real_ip_access_config(&self) -> BasicAuthContextResult<Option<RealIpAccessConfig>> {
140        let config = self.real_ip_access_config().cloned();
141        config
142            .clone()
143            .map(RealIpAccessManager::from_config)
144            .transpose()
145            .map_err(|source| BasicAuthContextError::RealIp { source })?;
146        Ok(config)
147    }
148
149    fn resolve_realm_config(&self) -> String {
150        self.realm_config()
151            .map(ToOwned::to_owned)
152            .unwrap_or_else(default_realm)
153    }
154
155    fn resolve_post_auth_redirect_config(&self) -> BasicAuthContextResult<RedirectTargetConfig> {
156        let config = self.post_auth_redirect_config().clone();
157        UriRelativeRedirectTargetResolver::from_config(config.clone())
158            .map_err(|source| BasicAuthContextError::RedirectTarget { source })?;
159        Ok(config)
160    }
161
162    fn resolve_zones_config(
163        &self,
164        default_realm: &str,
165        default_post_auth_redirect: &RedirectTargetConfig,
166    ) -> BasicAuthContextResult<Vec<ResolvedBasicAuthZoneConfig>> {
167        self.zones_config()
168            .iter()
169            .cloned()
170            .map(|zone| {
171                resolve_basic_auth_zone_config(zone, default_realm, default_post_auth_redirect)
172            })
173            .collect()
174    }
175
176    fn resolve_all(
177        &self,
178    ) -> Result<ResolvedBasicAuthContextConfig<Creds>, BasicAuthContextConfigBuildError>
179    where
180        Creds: Clone,
181    {
182        let validator = NoopBasicAuthContextConfigValidator;
183        self.resolve_all_with_validator(&validator)
184    }
185
186    fn resolve_all_with_validator<V>(
187        &self,
188        validator: &V,
189    ) -> Result<ResolvedBasicAuthContextConfig<Creds>, BasicAuthContextConfigBuildError>
190    where
191        Creds: Clone,
192        V: BasicAuthContextConfigValidator<Creds>,
193    {
194        validator
195            .validate_basic_auth_context_config(self)
196            .map_err(|source| BasicAuthContextConfigBuildError::Validation { source })?;
197
198        let realm = self.resolve_realm_config();
199        let post_auth_redirect = self
200            .resolve_post_auth_redirect_config()
201            .map_err(|source| BasicAuthContextConfigBuildError::Context { source })?;
202
203        Ok(ResolvedBasicAuthContextConfig {
204            creds: self.resolve_creds_config(),
205            real_ip_access: self
206                .resolve_real_ip_access_config()
207                .map_err(|source| BasicAuthContextConfigBuildError::Context { source })?,
208            zones: self
209                .resolve_zones_config(&realm, &post_auth_redirect)
210                .map_err(|source| BasicAuthContextConfigBuildError::Context { source })?,
211            realm,
212            post_auth_redirect,
213        })
214    }
215}
216
217impl<Creds> BasicAuthContextConfigSource<Creds> for BasicAuthContextConfig<Creds>
218where
219    Creds: BasicAuthCred + Serialize + for<'a> Deserialize<'a>,
220{
221    fn creds_config(&self) -> &BasicAuthCredsConfig<Creds> {
222        &self.creds
223    }
224
225    fn real_ip_access_config(&self) -> Option<&RealIpAccessConfig> {
226        self.real_ip_access.as_ref()
227    }
228
229    fn zones_config(&self) -> &[BasicAuthZoneConfig] {
230        &self.zones
231    }
232
233    fn realm_config(&self) -> Option<&str> {
234        self.realm.as_deref()
235    }
236
237    fn post_auth_redirect_config(&self) -> &RedirectTargetConfig {
238        &self.post_auth_redirect
239    }
240}
241
242pub(crate) fn default_zone_prefix() -> String {
243    "/basic".to_string()
244}
245
246pub(crate) fn default_login_subpath() -> String {
247    "/login".to_string()
248}
249
250pub(crate) fn default_logout_subpath() -> String {
251    "/logout".to_string()
252}
253
254pub(crate) fn default_post_auth_redirect() -> RedirectTargetConfig {
255    RedirectTargetConfig::strict_default("/")
256}
257
258pub(crate) fn default_realm() -> String {
259    "securitydept".to_string()
260}
261
262pub(crate) fn resolve_basic_auth_zone_config(
263    zone: BasicAuthZoneConfig,
264    default_realm: &str,
265    default_post_auth_redirect: &RedirectTargetConfig,
266) -> BasicAuthContextResult<ResolvedBasicAuthZoneConfig> {
267    let post_auth_redirect = zone
268        .post_auth_redirect
269        .unwrap_or_else(|| default_post_auth_redirect.clone());
270    UriRelativeRedirectTargetResolver::from_config(post_auth_redirect.clone())
271        .map_err(|source| BasicAuthContextError::RedirectTarget { source })?;
272
273    Ok(ResolvedBasicAuthZoneConfig {
274        zone_prefix: zone.zone_prefix,
275        login_subpath: zone.login_subpath,
276        logout_subpath: zone.logout_subpath,
277        realm: zone.realm.unwrap_or_else(|| default_realm.to_string()),
278        post_auth_redirect,
279    })
280}