feat(username): normalize username into lowercase
- normalize username into lowercase (this is already ASCII only) - permit users to login with their username with insensitive case - if a disabled account is reactivated and got a collision, it will normalize it too - server will stop on collision (ex: 2 entries with `Alice` and `alice`) in a such case admin can run: ``` oxicloud migrate lowercase-usernames --dry-run ``` then ``` oxicloud migrate lowercase-usernames ```
This commit is contained in:
@@ -94,6 +94,19 @@ pub async fn basic_auth_middleware(
|
||||
let (raw_username, password) =
|
||||
parse_basic_auth(auth_header).ok_or(NextcloudAuthError::Unauthorized)?;
|
||||
|
||||
// Canonicalise the whole Basic-Auth username to lowercase.
|
||||
//
|
||||
// Usernames are canonical (lowercase) in the DB post-migration
|
||||
// (`docs/plan/username-lowercase.md`), and NC / DAVX5 clients that
|
||||
// cached URLs from before the migration keep sending `Alice:pass`
|
||||
// — the server continues to accept that indefinitely by
|
||||
// lowercasing here. Safe for the multi-drive `user~drive_uuid`
|
||||
// composite because UUID hex `[0-9a-f-]` lowercases to itself.
|
||||
//
|
||||
// ASCII-only by `validate_username`'s charset check, so
|
||||
// `to_ascii_lowercase` is deterministic and locale-safe.
|
||||
let raw_username = raw_username.to_ascii_lowercase();
|
||||
|
||||
// ── Multi-drive composite-username parse ────────────────────────
|
||||
// POC wire shape: `{username}~{drive_marker}` may appear in the
|
||||
// Basic Auth header. `~` was chosen because it needs no URL
|
||||
@@ -319,7 +332,13 @@ pub fn parse_basic_auth(header_value: &str) -> Option<(String, String)> {
|
||||
let decoded = String::from_utf8(decoded).ok()?;
|
||||
let (user, pass) = decoded.split_once(':')?;
|
||||
|
||||
Some((user.to_string(), pass.to_string()))
|
||||
// Canonicalise the username to lowercase here too, so any caller
|
||||
// that reaches for `parse_basic_auth` directly (bypassing the
|
||||
// middleware wrapper) also sees the canonical form. Redundant with
|
||||
// the middleware's explicit `to_ascii_lowercase` on `raw_username`
|
||||
// — belt-and-braces to keep the invariant local to the parser too.
|
||||
// See `docs/plan/username-lowercase.md § 4. NextCloud DAV surface`.
|
||||
Some((user.to_ascii_lowercase(), pass.to_string()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -107,7 +107,21 @@ fn extract_url_user(path: &str) -> Option<std::borrow::Cow<'_, str>> {
|
||||
// common path allocates nothing; only a percent-encoded username owns. The
|
||||
// old `.into_owned()` forced a `String` on EVERY path-scoped NC DAV request
|
||||
// (benches/ROUND19.md §M7). The caller compares by slice.
|
||||
urlencoding::decode(user_seg).ok()
|
||||
//
|
||||
// Lowercase before returning so cached client URLs like
|
||||
// `/dav/files/Alice/...` compare equal to the canonical
|
||||
// (lowercase) `session.raw_username`. See
|
||||
// `docs/plan/username-lowercase.md § 4. NextCloud DAV surface`.
|
||||
//
|
||||
// The lowercase transform always allocates (`to_ascii_lowercase`
|
||||
// on a `str` returns `String`). Trades the "Cow::Borrowed common
|
||||
// path" of the ROUND19 optimisation for correctness of the case-
|
||||
// insensitive comparison at line 157 — a `&str` compare with a
|
||||
// borrowed segment against a lowercase `session.raw_username`
|
||||
// would silently mismatch for `Alice`. The alloc is one small
|
||||
// String per NC DAV request; the correctness win is worth it.
|
||||
let decoded = urlencoding::decode(user_seg).ok()?;
|
||||
Some(std::borrow::Cow::Owned(decoded.to_ascii_lowercase()))
|
||||
}
|
||||
|
||||
/// Axum extractor: the shared handle to the request's [`NcSession`].
|
||||
|
||||
Reference in New Issue
Block a user