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) => normalizer
124                .normalize(value)
125                .map_err(AuthError::into_callback_failure),
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| {
132                normalizer
133                    .normalize(value)
134                    .map_err(AuthError::into_callback_failure)
135            },
136        )
137    }
138    pub async fn value_error(&self, value: &str) -> AuthResult<Option<UsernameValidationError>> {
139        let length = value.encode_utf16().count();
140        let minimum = if self.min_length == 0 {
141            3
142        } else {
143            self.min_length
144        };
145        let maximum = if self.max_length == 0 {
146            30
147        } else {
148            self.max_length
149        };
150        if length < minimum {
151            return Ok(Some(UsernameValidationError::TooShort));
152        }
153        if length > maximum {
154            return Ok(Some(UsernameValidationError::TooLong));
155        }
156        let valid = if let Some(validator) = &self.validator {
157            validator
158                .validate(value)
159                .await
160                .map_err(AuthError::into_callback_failure)?
161        } else {
162            super::valid_default_characters(value)
163        };
164        Ok((!valid).then_some(UsernameValidationError::Invalid))
165    }
166    pub async fn validate_value(&self, value: &str, status: u16) -> AuthResult<()> {
167        if let Some(error) = self.value_error(value).await? {
168            return Err(error.auth_error(status));
169        }
170        Ok(())
171    }
172    pub async fn validate_hook_value(&self, value: &str) -> AuthResult<()> {
173        if self.validation_order == Some(UsernameValidationOrder::PostNormalization) {
174            self.validate_value(&self.normalize(value)?, 400).await
175        } else {
176            self.validate_value(value, 400).await
177        }
178    }
179    /// Normalize an untyped passwordless create-hook value without changing
180    /// the string-only contract of configured native callbacks.
181    pub fn normalize_raw_hook_value(&self, value: &JsValue) -> AuthResult<JsValue> {
182        if let JsValue::String(value) = value {
183            return self.normalize(value).map(JsValue::String);
184        }
185        if matches!(self.normalization, UsernameNormalization::Preserve) {
186            return Ok(value.clone());
187        }
188        Err(AuthError::CallbackFailure(Box::new(AuthError::internal(
189            "Username normalization requires a string",
190        ))))
191    }
192    /// The dependency reads raw .length before its default regular expression.
193    /// Numeric and boolean candidates have no length; its regular expression
194    /// applies String coercion independently from later normalization.
195    #[expect(
196        clippy::as_conversions,
197        clippy::cast_precision_loss,
198        reason = "Raw JavaScript length comparisons use IEEE754 numbers"
199    )]
200    pub async fn validate_raw_hook_value(&self, value: &JsValue) -> AuthResult<()> {
201        if let JsValue::String(value) = value {
202            return self.validate_hook_value(value).await;
203        }
204        let normalized;
205        let value = if self.validation_order == Some(UsernameValidationOrder::PostNormalization) {
206            normalized = self.normalize_raw_hook_value(value)?;
207            &normalized
208        } else {
209            value
210        };
211        let raw_length = match value {
212            JsValue::String(value) => Some(value.encode_utf16().count() as f64),
213            JsValue::Array(value) => Some(value.len() as f64),
214            JsValue::Object(value) => value
215                .get("length")
216                .map(|length| -> AuthResult<f64> {
217                    Ok(match length {
218                        JsValue::Null => 0.0,
219                        JsValue::Bool(value) => {
220                            if *value {
221                                1.0
222                            } else {
223                                0.0
224                            }
225                        }
226                        JsValue::Number(value) => *value,
227                        value => {
228                            let text = value.coerce_string().map_err(|message| {
229                                AuthError::CallbackFailure(Box::new(AuthError::internal(message)))
230                            })?;
231                            crate::utils::javascript::string_to_number(&text).unwrap_or(f64::NAN)
232                        }
233                    })
234                })
235                .transpose()?,
236            _ => None,
237        };
238        let minimum = if self.min_length == 0 {
239            3
240        } else {
241            self.min_length
242        };
243        let maximum = if self.max_length == 0 {
244            30
245        } else {
246            self.max_length
247        };
248        if raw_length.is_some_and(|length| length < minimum as f64) {
249            return Err(UsernameValidationError::TooShort.auth_error(400));
250        }
251        if raw_length.is_some_and(|length| length > maximum as f64) {
252            return Err(UsernameValidationError::TooLong.auth_error(400));
253        }
254        if self.validator.is_some() {
255            return Err(AuthError::CallbackFailure(Box::new(AuthError::internal(
256                "Configured username validation requires a string",
257            ))));
258        }
259        let text = value.coerce_string().map_err(|message| {
260            AuthError::CallbackFailure(Box::new(AuthError::internal(message)))
261        })?;
262        if text.is_empty() || !super::valid_default_characters(&text) {
263            return Err(UsernameValidationError::Invalid.auth_error(400));
264        }
265        Ok(())
266    }
267    pub async fn validate_display(&self, value: &str) -> AuthResult<()> {
268        if !self.include_display_username {
269            return Ok(());
270        }
271        if let Some(validator) = &self.display_validator {
272            let normalized;
273            let input = if self.display_validation_order
274                == Some(UsernameValidationOrder::PostNormalization)
275            {
276                normalized = self.normalize_display(value)?;
277                &normalized
278            } else {
279                value
280            };
281            if !validator
282                .validate(input)
283                .await
284                .map_err(AuthError::into_callback_failure)?
285            {
286                return Err(AuthError::Upstream {
287                    status: 400,
288                    code: "INVALID_DISPLAY_USERNAME",
289                    message: "Display username is invalid",
290                });
291            }
292        }
293        Ok(())
294    }
295    /// Schema input transforms run both during endpoint parsing and storage.
296    pub fn fields(&self) -> crate::field_policy::FieldConfigs {
297        use crate::field_policy::FieldConfig;
298        let mut fields = crate::field_policy::FieldConfigs::new();
299        let policy = self.clone();
300        _ = fields.insert(
301            "username".into(),
302            FieldConfig::new(serde_json::json!({"type":"string"})).transform(move |value| {
303                match value {
304                    Some(JsValue::String(value)) => {
305                        Ok(Some(JsValue::String(policy.normalize(value)?)))
306                    }
307                    value => Ok(value.cloned()),
308                }
309            }),
310        );
311        if let Some(field) = fields.get_mut("username") {
312            field.input = self.input;
313        }
314        if self.include_display_username {
315            let policy = self.clone();
316            _ = fields.insert(
317                "displayUsername".into(),
318                FieldConfig::new(serde_json::json!({"type":"string"})).transform(move |value| {
319                    match value {
320                        Some(JsValue::String(value)) => {
321                            Ok(Some(JsValue::String(policy.normalize_display(value)?)))
322                        }
323                        value => Ok(value.cloned()),
324                    }
325                }),
326            );
327        } else {
328            let mut hidden = FieldConfig::new(serde_json::json!({"type":"string"}));
329            hidden.input = false;
330            hidden.returned = false;
331            _ = fields.insert("displayUsername".into(), hidden);
332        }
333        fields
334    }
335    /// Normalize the actual database-hook candidate before adapter transforms.
336    pub fn normalize_fields(
337        &self,
338        username: &mut Option<String>,
339        display: &mut Option<String>,
340        values: &mut crate::field_policy::FieldValues,
341        creation: bool,
342    ) -> AuthResult<()> {
343        if username.is_none()
344            && let Some(raw) = values.get("username").cloned()
345            && raw.is_truthy()
346        {
347            let normalized = self.normalize_raw_hook_value(&raw)?;
348            _ = values.insert("username".into(), normalized);
349            if creation
350                && self.include_display_username
351                && values
352                    .get("displayUsername")
353                    .is_none_or(|value| !value.is_truthy())
354            {
355                _ = values.insert("displayUsername".into(), raw);
356            }
357        }
358        if display.is_none()
359            && self.include_display_username
360            && let Some(raw) = values
361                .get("displayUsername")
362                .filter(|raw| raw.is_truthy())
363                .cloned()
364            && self.display_normalizer.is_some()
365        {
366            let raw = raw.as_str().ok_or_else(|| {
367                AuthError::CallbackFailure(Box::new(AuthError::internal(
368                    "Configured display normalization requires a string",
369                )))
370            })?;
371            _ = values.insert(
372                "displayUsername".into(),
373                JsValue::String(self.normalize_display(raw)?),
374            );
375        }
376        let original_username = username.clone();
377        if let Some(value) = username.as_mut() {
378            if !value.is_empty() {
379                *value = self.normalize(value)?;
380            }
381            _ = values.insert("username".into(), JsValue::String(value.clone()));
382        }
383        if self.include_display_username {
384            if creation
385                && username.as_ref().is_some_and(|value| !value.is_empty())
386                && display.as_ref().is_none_or(String::is_empty)
387            {
388                display.clone_from(&original_username);
389                if let Some(value) = &display {
390                    _ = values.insert("displayUsername".into(), JsValue::String(value.clone()));
391                }
392            } else if let Some(value) = display.as_mut().filter(|value| !value.is_empty()) {
393                *value = self.normalize_display(value)?;
394                _ = values.insert("displayUsername".into(), JsValue::String(value.clone()));
395            }
396        } else {
397            *display = None;
398        }
399        Ok(())
400    }
401}