Skip to main content

alibi_core/utils/username/
policy.rs

1//! Configured username validation and normalization stages.
2
3use super::UsernameValidationError;
4use crate::{AuthError, AuthResult, utils::json::JsValue};
5use async_trait::async_trait;
6use std::{fmt, future::Future, sync::Arc};
7
8/// Awaited application validation of a username or display username.
9#[async_trait]
10pub trait UsernameValidator: Send + Sync {
11    async fn validate(&self, value: &str) -> AuthResult<bool>;
12}
13#[async_trait]
14impl<F, Fut> UsernameValidator for F
15where
16    F: Fn(String) -> Fut + Send + Sync,
17    Fut: Future<Output = AuthResult<bool>> + Send,
18{
19    async fn validate(&self, value: &str) -> AuthResult<bool> {
20        self(value.to_owned()).await
21    }
22}
23
24/// A synchronous normalization callback; errors propagate to its caller.
25pub trait UsernameNormalizer: Send + Sync {
26    fn normalize(&self, value: &str) -> AuthResult<String>;
27}
28impl<F> UsernameNormalizer for F
29where
30    F: Fn(&str) -> AuthResult<String> + Send + Sync,
31{
32    fn normalize(&self, value: &str) -> AuthResult<String> {
33        self(value)
34    }
35}
36
37#[derive(Clone, Default)]
38pub enum UsernameNormalization {
39    #[default]
40    Lowercase,
41    Preserve,
42    Custom(Arc<dyn UsernameNormalizer>),
43}
44impl fmt::Debug for UsernameNormalization {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        f.write_str(match self {
47            Self::Lowercase => "Lowercase",
48            Self::Preserve => "Preserve",
49            Self::Custom(_) => "Custom",
50        })
51    }
52}
53
54/// The explicit upstream validation-order setting. An absent setting is
55/// distinct: sign-in validates raw input, while lookup always normalizes it.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum UsernameValidationOrder {
58    PreNormalization,
59    PostNormalization,
60}
61
62#[derive(Clone)]
63pub struct UsernameConfig {
64    /// UTF-16 lengths; zero uses the upstream defaults of 3 and 30.
65    pub min_length: usize,
66    pub max_length: usize,
67    pub normalization: UsernameNormalization,
68    pub validator: Option<Arc<dyn UsernameValidator>>,
69    pub include_display_username: bool,
70    pub display_normalizer: Option<Arc<dyn UsernameNormalizer>>,
71    pub display_validator: Option<Arc<dyn UsernameValidator>>,
72    pub validation_order: Option<UsernameValidationOrder>,
73    pub display_validation_order: Option<UsernameValidationOrder>,
74    pub immutable_username: bool,
75    /// Admit usernames supplied by endpoint callers.
76    pub input: bool,
77}
78impl Default for UsernameConfig {
79    fn default() -> Self {
80        Self {
81            min_length: 3,
82            max_length: 30,
83            normalization: UsernameNormalization::Lowercase,
84            validator: None,
85            include_display_username: true,
86            display_normalizer: None,
87            display_validator: None,
88            validation_order: None,
89            display_validation_order: None,
90            immutable_username: false,
91            input: true,
92        }
93    }
94}
95impl fmt::Debug for UsernameConfig {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.debug_struct("UsernameConfig")
98            .field("min_length", &self.min_length)
99            .field("max_length", &self.max_length)
100            .field("normalization", &self.normalization)
101            .field("validator", &self.validator.as_ref().map(|_| "callback"))
102            .field("include_display_username", &self.include_display_username)
103            .field(
104                "display_normalizer",
105                &self.display_normalizer.as_ref().map(|_| "callback"),
106            )
107            .field(
108                "display_validator",
109                &self.display_validator.as_ref().map(|_| "callback"),
110            )
111            .field("validation_order", &self.validation_order)
112            .field("display_validation_order", &self.display_validation_order)
113            .field("immutable_username", &self.immutable_username)
114            .field("input", &self.input)
115            .finish()
116    }
117}
118impl UsernameConfig {
119    pub fn normalize(&self, value: &str) -> AuthResult<String> {
120        match &self.normalization {
121            UsernameNormalization::Lowercase => Ok(value.to_lowercase()),
122            UsernameNormalization::Preserve => Ok(value.to_owned()),
123            UsernameNormalization::Custom(normalizer) => {
124                normalizer.normalize(value).map_err(callback_error)
125            }
126        }
127    }
128    pub fn normalize_display(&self, value: &str) -> AuthResult<String> {
129        self.display_normalizer.as_ref().map_or_else(
130            || Ok(value.to_owned()),
131            |normalizer| normalizer.normalize(value).map_err(callback_error),
132        )
133    }
134    pub async fn value_error(&self, value: &str) -> AuthResult<Option<UsernameValidationError>> {
135        let length = value.encode_utf16().count();
136        let minimum = if self.min_length == 0 {
137            3
138        } else {
139            self.min_length
140        };
141        let maximum = if self.max_length == 0 {
142            30
143        } else {
144            self.max_length
145        };
146        if length < minimum {
147            return Ok(Some(UsernameValidationError::TooShort));
148        }
149        if length > maximum {
150            return Ok(Some(UsernameValidationError::TooLong));
151        }
152        let valid = if let Some(validator) = &self.validator {
153            validator.validate(value).await.map_err(callback_error)?
154        } else {
155            super::valid_default_characters(value)
156        };
157        Ok((!valid).then_some(UsernameValidationError::Invalid))
158    }
159    pub async fn validate_value(&self, value: &str, status: u16) -> AuthResult<()> {
160        if let Some(error) = self.value_error(value).await? {
161            return Err(error.auth_error(status));
162        }
163        Ok(())
164    }
165    pub async fn validate_hook_value(&self, value: &str) -> AuthResult<()> {
166        if self.validation_order == Some(UsernameValidationOrder::PostNormalization) {
167            self.validate_value(&self.normalize(value)?, 400).await
168        } else {
169            self.validate_value(value, 400).await
170        }
171    }
172    pub async fn validate_display(&self, value: &str) -> AuthResult<()> {
173        if !self.include_display_username {
174            return Ok(());
175        }
176        if let Some(validator) = &self.display_validator {
177            let normalized;
178            let input = if self.display_validation_order
179                == Some(UsernameValidationOrder::PostNormalization)
180            {
181                normalized = self.normalize_display(value)?;
182                &normalized
183            } else {
184                value
185            };
186            if !validator.validate(input).await.map_err(callback_error)? {
187                return Err(AuthError::Upstream {
188                    status: 400,
189                    code: "INVALID_DISPLAY_USERNAME",
190                    message: "Display username is invalid",
191                });
192            }
193        }
194        Ok(())
195    }
196    /// Schema input transforms run both during endpoint parsing and storage.
197    pub fn fields(&self) -> crate::field_policy::FieldConfigs {
198        use crate::field_policy::FieldConfig;
199        let mut fields = crate::field_policy::FieldConfigs::new();
200        let policy = self.clone();
201        let _ = fields.insert(
202            "username".into(),
203            FieldConfig::new(serde_json::json!({"type":"string"})).transform(move |value| {
204                match value {
205                    Some(JsValue::String(value)) => {
206                        Ok(Some(JsValue::String(policy.normalize(value)?)))
207                    }
208                    value => Ok(value.cloned()),
209                }
210            }),
211        );
212        if let Some(field) = fields.get_mut("username") {
213            field.input = self.input;
214        }
215        if self.include_display_username {
216            let policy = self.clone();
217            let _ = fields.insert(
218                "displayUsername".into(),
219                FieldConfig::new(serde_json::json!({"type":"string"})).transform(move |value| {
220                    match value {
221                        Some(JsValue::String(value)) => {
222                            Ok(Some(JsValue::String(policy.normalize_display(value)?)))
223                        }
224                        value => Ok(value.cloned()),
225                    }
226                }),
227            );
228        } else {
229            let mut hidden = FieldConfig::new(serde_json::json!({"type":"string"}));
230            hidden.input = false;
231            hidden.returned = false;
232            let _ = fields.insert("displayUsername".into(), hidden);
233        }
234        fields
235    }
236    /// Normalize the actual database-hook candidate before adapter transforms.
237    pub fn normalize_fields(
238        &self,
239        username: &mut Option<String>,
240        display: &mut Option<String>,
241        values: &mut crate::field_policy::FieldValues,
242        creation: bool,
243    ) -> AuthResult<()> {
244        let original_username = username.clone();
245        if let Some(value) = username.as_mut() {
246            if !value.is_empty() {
247                *value = self.normalize(value)?;
248            }
249            let _ = values.insert("username".into(), JsValue::String(value.clone()));
250        }
251        if self.include_display_username {
252            if creation
253                && username.as_ref().is_some_and(|value| !value.is_empty())
254                && display.as_ref().is_none_or(String::is_empty)
255            {
256                display.clone_from(&original_username);
257                if let Some(value) = &display {
258                    let _ = values.insert("displayUsername".into(), JsValue::String(value.clone()));
259                }
260            } else if let Some(value) = display.as_mut().filter(|value| !value.is_empty()) {
261                *value = self.normalize_display(value)?;
262                let _ = values.insert("displayUsername".into(), JsValue::String(value.clone()));
263            }
264        } else {
265            *display = None;
266        }
267        Ok(())
268    }
269}
270
271fn callback_error(error: AuthError) -> AuthError {
272    match error {
273        AuthError::Api { .. } | AuthError::Upstream { .. } | AuthError::CallbackFailure(_) => error,
274        error => AuthError::CallbackFailure(Box::new(error)),
275    }
276}