alibi_core/utils/username/
policy.rs1use super::UsernameValidationError;
4use crate::{AuthError, AuthResult, utils::json::JsValue};
5use async_trait::async_trait;
6use std::{fmt, future::Future, sync::Arc};
7
8#[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
24pub 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub enum UsernameValidationOrder {
58 PreNormalization,
59 PostNormalization,
60}
61
62#[derive(Clone)]
63pub struct UsernameConfig {
64 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 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 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 #[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 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 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}