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:
Edouard Vanbelle
2026-09-13 18:46:42 +02:00
parent 0b9e8bfe23
commit a95a6b106c
15 changed files with 1454 additions and 81 deletions
+15 -1
View File
@@ -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`].