docs(plan): case-insensitive usernames
Records the design for issue #691 (make usernames case-insensitive): silently lowercase on ingest, explicit `oxicloud migrate lowercase-usernames [--dry-run]`, refuse-to-boot until DB is fully lowercase. Includes the chunked-upload directory rename step and the OIDC JIT lowercase fix surfaced during the design sweep. Design deferred pieces (display_name split, WebDAV URL redesign) are listed under "Not in scope" so the boundary is explicit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,524 @@
|
||||
# Plan — Case-insensitive usernames (lowercase-on-ingest)
|
||||
|
||||
## Context
|
||||
|
||||
Feature ask: [issue #691](https://github.com/AtalayaLabs/OxiCloud/issues/691).
|
||||
Usernames are currently case-sensitive, so `Alice`, `alice`, and `ALICE`
|
||||
refer to three different accounts. Users hit this as a login friction —
|
||||
they type their name with different capitalization on different clients
|
||||
and get "invalid credentials" instead of a successful login.
|
||||
|
||||
## Why this is simpler than it looks in this codebase specifically
|
||||
|
||||
- `validate_username` in `src/domain/entities/user.rs:884` already
|
||||
restricts usernames to ASCII-only `[a-zA-Z0-9._-]{2,64}` with no `@`.
|
||||
The Unicode case-folding minefield (Turkish dotted-I, German ß, Greek
|
||||
final sigma, NFC vs NFD) does not apply — ASCII case-folding is
|
||||
trivial (`to_ascii_lowercase`), deterministic, and locale-independent.
|
||||
- OIDC identity binds via `(iss, sub)` in
|
||||
`get_user_by_federation_subject` at
|
||||
`src/infrastructure/repositories/pg/user_pg_repository.rs:1257-1303` —
|
||||
case-sensitivity of the local username is orthogonal to OIDC identity
|
||||
matching. No OIDC breakage risk.
|
||||
- Password verification runs through Argon2's `verify_password`
|
||||
(constant-time by construction). Not affected.
|
||||
- `@`-forbidden rule in usernames is the disjoint namespace with email
|
||||
lookup (`dispatch_login` at `auth_application_service.rs:1018`).
|
||||
Case-insensitive usernames align semantics with email addresses
|
||||
(already case-insensitive in practice), so any future
|
||||
`groupname@domain` composition stays consistent.
|
||||
- NextCloud URL `/remote.php/dav/files/{user}/…` uses `{user}` as an
|
||||
informational / consistency-check marker, not a security boundary —
|
||||
the chroot ACL is the real authz. Handling case in the URL segment is
|
||||
a small local change (documented in `session.rs:33-34`).
|
||||
|
||||
## Design decisions
|
||||
|
||||
1. **Silently lowercase on ingest** (registration, admin-create, OIDC
|
||||
provisioning, rename). Never reject uppercase input from clients —
|
||||
accept liberally, store strictly (Postel's Law).
|
||||
2. **Explicit migration** (`oxicloud migrate lowercase-usernames
|
||||
[--dry-run]`). The server never mutates `auth.users` at boot. Ops
|
||||
MUST run the migration explicitly. Follows the
|
||||
[[feedback_no_silent_auto_repair]] rule: consistency tenants are
|
||||
discovery-only by default; mutation is opt-in.
|
||||
3. **Refuse-to-boot** if any active-user username is not already
|
||||
lowercase. Boot error message shows the exact CLI command to run.
|
||||
Boot performs a read-only verification only.
|
||||
4. **Collision tiebreak** on migration: `(last_login_at DESC NULLS
|
||||
LAST, created_at ASC)`. Winner keeps the canonical lowercased name.
|
||||
Losers get `-2`, `-3`, … suffix (increment until free), matching the
|
||||
pattern in `oxicloud migrate nfc-filenames`.
|
||||
5. **Active accounts only** in the boot check + migration. Soft-deleted
|
||||
/ disabled rows are skipped (they don't block usable logins). NULL
|
||||
usernames (OPAQUE-migrated accounts) are skipped in every layer —
|
||||
the boot verifier, the migration UPDATE, the CLI report. The
|
||||
`WHERE username <> LOWER(username)` predicate is already NULL-safe
|
||||
by SQL semantics (NULL comparisons yield NULL, filtered out); state
|
||||
it explicitly so a reviewer isn't left wondering.
|
||||
6. **Rename-only** on collision resolution — sessions are not
|
||||
invalidated. Sessions key on `user_id` so they survive the rename.
|
||||
7. **Un-soft-delete of a mixed-case account uses the SAME suffix
|
||||
scheme.** If `Alice` is soft-deleted (skipped by migration) and
|
||||
later un-soft-deleted while `alice` already exists, the un-soft-
|
||||
delete path re-normalizes via `set_username` and, on collision,
|
||||
assigns `alice-2` / `alice-3` / … — the same helper the migration
|
||||
CLI calls. Both callers reach for a shared
|
||||
`find_free_username_suffix(pool, base) -> String` in
|
||||
`src/common/username_migration.rs` so migration + un-soft-delete
|
||||
agree by construction. Without this, an un-soft-delete of the
|
||||
only pre-migration mixed-case survivor would refuse-to-boot on
|
||||
the next restart.
|
||||
|
||||
## Not in scope
|
||||
|
||||
- Unicode case-folding (usernames are ASCII-only by validation).
|
||||
- `display_name` split (usernames were already just identifiers;
|
||||
free-form display is a separate future feature if a user asks for
|
||||
it — deferred pending real demand signal).
|
||||
- OIDC provisioning behaviour change beyond the ingest-normalize point.
|
||||
- Case-insensitivity for emails (already achieved in practice; not
|
||||
touched).
|
||||
- Any change to `validate_username`'s character-class rules.
|
||||
- Any change to the WebDAV URL shape `/dav/files/{user}/…` (client
|
||||
compat; drop deferred separately per [[project_nc_multidrive_poc]]).
|
||||
- Group names (`SubjectGroup`). Lowercase by convention today; no
|
||||
runtime enforcement, no migration. If group-name case-insensitivity
|
||||
becomes a real ask, it lands as a sibling plan doc with the same
|
||||
shape.
|
||||
|
||||
## Deliverables
|
||||
|
||||
### 1. Ingest normalization
|
||||
|
||||
Change `validate_username` to return the canonical form instead of
|
||||
`()`:
|
||||
|
||||
```rust
|
||||
// src/domain/entities/user.rs — new signature
|
||||
fn validate_username(username: &str) -> UserResult<String> {
|
||||
let normalized = username.trim().to_ascii_lowercase();
|
||||
// ... existing length + charset + boundary checks apply to `normalized` ...
|
||||
Ok(normalized)
|
||||
}
|
||||
```
|
||||
|
||||
Every caller that today does `Self::validate_username(u)?;` becomes
|
||||
`let u = Self::validate_username(&u)?;` — the returned canonical form
|
||||
is what gets stored. Because the return type changes from `Result<()>`
|
||||
to `Result<String>`, any caller that ignores the result now becomes a
|
||||
compile error — the type system forces every write path through the
|
||||
normalizer.
|
||||
|
||||
Write sites all funnel through `User::new` (`src/domain/entities/user.rs:313`)
|
||||
or `User::set_username` (`:811`), so the signature change catches the
|
||||
entity-write path automatically. Callers to touch:
|
||||
|
||||
- Application services calling `User::new`:
|
||||
- `auth_application_service.rs:840` — `register()` public signup
|
||||
- `auth_application_service.rs:944` — `setup_create_admin()`
|
||||
first-boot admin
|
||||
- `auth_application_service.rs:3520`, `:3532` — `admin_create_user()`
|
||||
external + internal branches
|
||||
- `auth_application_service.rs:4692` — OIDC JIT provisioning
|
||||
- `magic_link_invite_service.rs:233` — magic-link external invite
|
||||
|
||||
- User-driven rename (calls `User::set_username`):
|
||||
- `auth_application_service.rs:2756-2801` — `update_profile()`
|
||||
|
||||
- Repository-write compile-error catches:
|
||||
- `src/infrastructure/repositories/pg/user_pg_repository.rs:281`
|
||||
(`create_user` INSERT) and `:740` (`update_user` UPDATE) — these
|
||||
bind `user_clone.username()`, which is now guaranteed lowercase by
|
||||
the entity constructor.
|
||||
|
||||
### 2. OIDC JIT derivation
|
||||
|
||||
`auth_application_service.rs:4649-4690` derives a local username from
|
||||
the OIDC `preferred_username` / `name` / `sub` claims, filters to
|
||||
`[a-zA-Z0-9._-]`, and truncates. **It does not currently lowercase.**
|
||||
Add `to_ascii_lowercase()` on the derived string before passing to
|
||||
`User::new`. This is beyond what the entity signature change catches —
|
||||
explicit fix required.
|
||||
|
||||
### 3. Lookup normalization
|
||||
|
||||
Repository `find_by_username`-style methods internally lowercase the
|
||||
input before the SQL query, so callers don't have to remember. One-line
|
||||
change per method:
|
||||
|
||||
- `src/infrastructure/repositories/pg/user_pg_repository.rs:487`
|
||||
(`get_user_by_username`) — add `let username = username.trim().
|
||||
to_ascii_lowercase();` before the `.bind(&username)` at line 488.
|
||||
- `src/infrastructure/repositories/pg/user_pg_repository.rs:1043`
|
||||
(`search_users`) — `ILIKE` is already case-insensitive by
|
||||
construction; verify nothing regresses.
|
||||
- `src/infrastructure/repositories/pg/user_pg_repository.rs:1504`
|
||||
(`search_usernames`) — same as above.
|
||||
- `src/application/services/storage_usage_service.rs:145-149`
|
||||
(`update_user_storage_usage_by_username`) — raw SQL bind; normalize
|
||||
before `.bind()`.
|
||||
- `src/cli/opaque.rs:125`, `:202` — `opaque reset` CLI identifier
|
||||
dispatch on `@`; lowercase the username branch input.
|
||||
|
||||
Post-migration, the DB is fully lowercase so `WHERE username = 'alice'`
|
||||
matches. Pre-migration users are blocked from booting by the boot-time
|
||||
check, so the mixed-case-DB-during-transition state cannot serve
|
||||
traffic.
|
||||
|
||||
### 4. NextCloud DAV surface
|
||||
|
||||
Two coordinated changes on the NC surface:
|
||||
|
||||
- `src/interfaces/nextcloud/basic_auth_middleware.rs:94-134` — decoded
|
||||
`raw_username` from the Basic Auth header, lowercase the whole
|
||||
string. Safe for the `user~drive_uuid` multi-drive format because
|
||||
UUID hex is `[0-9a-f-]` which lowercases to itself.
|
||||
- `src/interfaces/nextcloud/basic_auth_middleware.rs:307-323`
|
||||
(`parse_basic_auth` helper) — lowercase the username portion before
|
||||
returning.
|
||||
- `src/interfaces/nextcloud/session.rs:90-111`
|
||||
(`extract_url_user`) — lowercase the returned `Cow<'_, str>` value
|
||||
from URL decode. The cross-check comparison at `session.rs:157-161`
|
||||
(`url_user != session.raw_username`) then compares normalized vs
|
||||
normalized — no change needed at the comparison site itself.
|
||||
|
||||
Downstream `session.raw_username` consumers (WebDAV / OCS href
|
||||
builders, MOVE Destination parsers, avatar / trashbin handlers) all
|
||||
pass through and emit lowercase automatically — no per-site change
|
||||
needed.
|
||||
|
||||
**Client compatibility:** NC / DAVX5 clients that cached URLs like
|
||||
`/remote.php/dav/files/Alice/…` continue to work through the migration
|
||||
because the server accepts uppercase URL segments **indefinitely**
|
||||
(the Basic Auth middleware + `extract_url_user` both lowercase on
|
||||
decode). No forced client upgrade or reconfiguration. PROPFIND
|
||||
response bodies emit lowercase hrefs (from canonical
|
||||
`session.raw_username`), which well-behaved clients update on next
|
||||
sync.
|
||||
|
||||
Expected per-client behavior on first PROPFIND after upgrade:
|
||||
|
||||
- **Nextcloud desktop** — prompts a one-time re-sync notification
|
||||
when it notices the account URL case changed. Files re-verify
|
||||
via ETag, so no re-upload; the re-sync completes in
|
||||
seconds-to-minutes depending on file count. Users click through
|
||||
the reconnect dialog.
|
||||
- **DAVX5** (calendars, contacts) — silent update of the internal
|
||||
`principal-URL`; user sees no dialog.
|
||||
- **NC mobile app** — silent refresh of the account tile.
|
||||
- **Older / misbehaving clients** — may create a duplicate account
|
||||
profile (rare, cosmetic, not destructive).
|
||||
|
||||
**Zero data risk in every path.** The chroot ACL keys on
|
||||
`user_id`, not username, so files, calendars, contacts, and
|
||||
grants all follow the user across the rename. The blast radius
|
||||
is a one-time UX notification, not lost bytes.
|
||||
|
||||
**Power-user pre-emption** (worth documenting in CHANGELOG): ops
|
||||
who want to avoid the re-sync prompt entirely can, before
|
||||
upgrading, log into each NC desktop client and manually update
|
||||
the account URL from `.../USERNAME` to lowercase. Cheap
|
||||
prophylactic for organizations rolling out to non-technical
|
||||
users.
|
||||
|
||||
### 5. Chunked-upload directory rename
|
||||
|
||||
`src/infrastructure/services/nextcloud_chunked_upload_service.rs:99-103`
|
||||
uses `user.username` as an on-disk directory name AND as an in-memory
|
||||
cache key. Post-migration, `user.username` becomes lowercase; any
|
||||
in-flight upload for `Alice` at migration time strands the on-disk
|
||||
`base_dir/Alice/upload_xxx/` directory and orphans its cache entry.
|
||||
|
||||
The migration command must ALSO walk `base_dir/*/` and rename any
|
||||
mixed-case subdirectory to its lowercase form. Collision handling
|
||||
(both `Alice/` and `alice/` present) → merge contents; else simple
|
||||
rename. In practice this is likely a no-op — chunked-upload state is
|
||||
ephemeral, and simultaneous mixed-case uploads by the same user are
|
||||
rare.
|
||||
|
||||
### 6. Boot-time verification
|
||||
|
||||
New module `src/common/username_migration.rs` exposing:
|
||||
|
||||
```rust
|
||||
pub async fn verify_all_usernames_lowercase(pool: &PgPool) -> Result<(), String>
|
||||
```
|
||||
|
||||
Runs after `sqlx::migrate!()` completes, before `AppState` is
|
||||
assembled. Query:
|
||||
|
||||
```sql
|
||||
SELECT id, username, created_at, last_login_at
|
||||
FROM auth.users
|
||||
WHERE username <> LOWER(username)
|
||||
-- NULL usernames (OPAQUE-migrated accounts) are already filtered
|
||||
-- out by SQL semantics: NULL <> anything yields NULL, which
|
||||
-- WHERE excludes. Explicit for the reviewer's benefit.
|
||||
-- add is_deleted / disabled filter if such a flag exists
|
||||
ORDER BY LOWER(username),
|
||||
(last_login_at IS NULL),
|
||||
last_login_at DESC NULLS LAST,
|
||||
created_at ASC
|
||||
LIMIT 200; -- soft cap on error-message size
|
||||
```
|
||||
|
||||
If empty → boot proceeds. If non-empty → format the FATAL error and
|
||||
return `Err(String)`. `main.rs` propagates via `?` to a non-zero
|
||||
process exit.
|
||||
|
||||
Boot only READS `auth.users`; never WRITES. This is the "explicit
|
||||
migration required" enforcement layer.
|
||||
|
||||
**Error message format** (self-sufficient — no docs required at 3 AM):
|
||||
|
||||
```
|
||||
FATAL: cannot start — <N> user account(s) have non-lowercase usernames.
|
||||
|
||||
Before this version can boot, run the migration:
|
||||
|
||||
oxicloud migrate lowercase-usernames --dry-run # preview
|
||||
oxicloud migrate lowercase-usernames # apply
|
||||
|
||||
Affected accounts (up to 20 shown; full list via the dry-run):
|
||||
|
||||
Alice (id: a1b2c3d4-... last_login: 2026-08-01)
|
||||
BOB (id: 9abc0000-... last_login: never)
|
||||
...
|
||||
|
||||
The migration handles case-collisions (Alice + alice → alice keeps
|
||||
the name based on most recent login; the other gets alice-2 suffix).
|
||||
Sessions and grants survive the rename (they key on user_id).
|
||||
```
|
||||
|
||||
### 7. Migration CLI
|
||||
|
||||
New action under `oxicloud migrate`:
|
||||
|
||||
```rust
|
||||
// src/cli/migrate.rs — extend the Action enum
|
||||
Action::LowercaseUsernames { dry_run: bool }
|
||||
```
|
||||
|
||||
Following the shape of `run_nfc_filenames`:
|
||||
|
||||
- Load all active users (skip soft-deleted / disabled AND rows
|
||||
where `username IS NULL` — OPAQUE-migrated accounts have no
|
||||
username string to normalize)
|
||||
- Group by `LOWER(username)`
|
||||
- For each group:
|
||||
- Single-member group with mixed-case name → UPDATE to lowercase
|
||||
- Multi-member group (collision) → apply tiebreak
|
||||
`(last_login_at DESC NULLS LAST, created_at ASC)`, winner UPDATEs
|
||||
to lowercase, losers UPDATE to `<lowercase>-2`, `-3`, … (increment
|
||||
until free)
|
||||
- Per-row `println!` log:
|
||||
`NORMALIZE user=<uuid> '<before>' ({}B) → '<after>' ({}B)`
|
||||
- Summary at end: scanned / already-lowercase / normalized /
|
||||
collision-resolved / renamed-to-suffix
|
||||
- `--dry-run` guards all UPDATEs
|
||||
|
||||
After the DB pass, run the chunked-upload directory rename step (see
|
||||
Deliverable 5).
|
||||
|
||||
Suffix search reuses the pattern from
|
||||
`find_free_folder_duplicate_name` in the existing NFC migration —
|
||||
increment-until-free loop, starting at `-2`, probing until an
|
||||
unused suffix is found. Robust against pre-existing rows like
|
||||
`alice-2` already being taken (the probe just steps past them
|
||||
to `-3`, `-4`, …).
|
||||
|
||||
Extracted into a shared public helper in
|
||||
`src/common/username_migration.rs`:
|
||||
|
||||
```rust
|
||||
pub async fn find_free_username_suffix(pool: &PgPool, base: &str) -> Result<String, sqlx::Error>
|
||||
```
|
||||
|
||||
Both the migration CLI AND the un-soft-delete API (Design decision
|
||||
7) call this helper — same collision-resolution behavior by
|
||||
construction, no drift risk between the two paths.
|
||||
|
||||
Bounded at 10,000 as a safety cap. The probability of reaching
|
||||
that in a real deployment is negligible — it would require ~10 K
|
||||
distinct accounts all originally cased differently but sharing
|
||||
the same lowercase form (a normal collision is 2-3 accounts, not
|
||||
10 K). If the cap ever fires, something is very wrong with the
|
||||
account universe and the migration ABORTs with a loud error
|
||||
rather than silently truncating — the loud abort IS the
|
||||
detection mechanism.
|
||||
|
||||
### 8. Test seed audit
|
||||
|
||||
Sweep-verified: existing test seeds all produce lowercase or NULL
|
||||
usernames. Worth one more grep pass to ensure no test fixture INSERTs
|
||||
`INSERT INTO auth.users … 'AliceTest'` — if any exist, lowercase them
|
||||
in the same commit to avoid CI refuse-to-boot regressions.
|
||||
|
||||
Files verified (all safe):
|
||||
- `src/infrastructure/repositories/pg/user_pg_repository.rs:1786`
|
||||
- `src/infrastructure/repositories/pg/opaque_pg_repository.rs:339`
|
||||
(NULL)
|
||||
- `src/application/services/auth_application_service.rs:4982` (NULL)
|
||||
- `src/application/services/subject_group_service.rs:796` (NULL)
|
||||
- `src/bin/load-seed.rs:414`, `:446` (`load_user_XXXX` — lowercase)
|
||||
- `src/mount_it_support.rs:61` (`make_user(name)` — verify callers)
|
||||
- `tests/common/init-test-schema.sh:40` (`ci-admin` — lowercase)
|
||||
|
||||
### 9. Cosmetic side-effects (worth noting in CHANGELOG, non-blocking)
|
||||
|
||||
- `src/interfaces/nextcloud/avatar_handler.rs:283` — `pick_color`
|
||||
derives a deterministic tile color from username bytes. Users whose
|
||||
canonical username had uppercase letters will get a different
|
||||
fallback-avatar tile color after the migration. One-time cosmetic
|
||||
change.
|
||||
- **NC desktop may perform a one-time re-sync** — see Deliverable 4.
|
||||
|
||||
### 10. Documentation
|
||||
|
||||
- `CHANGELOG.md` — user-visible note:
|
||||
- Migration required; server refuses to boot until it's been run.
|
||||
- Exact CLI command shown in the refusal message.
|
||||
- Nextcloud desktop clients will prompt for a one-time re-sync
|
||||
on first PROPFIND after upgrade. Files are ETag-verified, not
|
||||
re-uploaded. DAVX5 and NC mobile handle the URL case change
|
||||
silently. **No forced client upgrade or reconfiguration** —
|
||||
server accepts uppercase URL segments indefinitely.
|
||||
- Optional pre-emption for non-technical users: ops can manually
|
||||
update the account URL to lowercase in each NC desktop client
|
||||
before upgrading, avoiding the re-sync prompt entirely.
|
||||
- Usernames become lowercase in ALL UI display surfaces (share
|
||||
dialogs, activity feeds, admin panels, PROPFIND response
|
||||
bodies, notification bell). Login identity unchanged from the
|
||||
user's POV (they can still type any case at the login form).
|
||||
- Avatar fallback color may change for users with previously-
|
||||
uppercase usernames.
|
||||
- Preamble noting `display_name` is a possible follow-up if
|
||||
users miss capitalization for display — deferred pending
|
||||
demand signal, no compat cost to adding later.
|
||||
- `docs/config/env.md` — note the boot-time check + migration command.
|
||||
- `docs/install/binary.md` — upgrade-from-case-sensitive section.
|
||||
|
||||
### 11. Test coverage
|
||||
|
||||
- **Unit**: `validate_username("Alice")` returns `Ok("alice")`;
|
||||
`validate_username("alice-")` returns `Err(...)` unchanged;
|
||||
`validate_username(" Alice ")` returns `Ok("alice")`.
|
||||
- **Unit**: `verify_all_usernames_lowercase` with mocked pool — empty
|
||||
result → Ok; non-empty → Err with formatted message.
|
||||
- **Hurl** (`tests/api/lowercase_usernames.hurl`, new): register a
|
||||
user with `MixedCase`, assert DB stores `mixedcase`; log in with
|
||||
`MIXEDCASE` and `mixedcase` — both succeed; rename to `NewName`,
|
||||
assert `newname` stored; NC Basic Auth accepts `MixedCase:pass`,
|
||||
`MIXEDCASE:pass`, `mixedcase:pass`.
|
||||
- **Manual** (against dev DB, not CI):
|
||||
- Induce a collision via `INSERT INTO auth.users … 'Alice'` on top
|
||||
of `alice`; boot server → verify refusal message + exact CLI shown
|
||||
- `oxicloud migrate lowercase-usernames --dry-run` → verify report
|
||||
- `oxicloud migrate lowercase-usernames` → verify apply
|
||||
- Boot again → succeeds
|
||||
- `curl -u ALICE:pass https://oxicloud/remote.php/dav/files/ALICE/…`
|
||||
→ succeeds (accepts uppercase input, resolves to lowercase user)
|
||||
|
||||
## Cache-and-consistency observations (informational)
|
||||
|
||||
- `src/infrastructure/services/login_lockout_service.rs:33,68-89` —
|
||||
already lowercases the key at line 58. No code change; comment
|
||||
becomes factual not incidental.
|
||||
- `src/application/services/app_password_service.rs:89,317-323` —
|
||||
BLAKE3-keyed cache using the raw wire username. Post-normalization,
|
||||
both sides normalize consistently → cache stays coherent. 300 s TTL
|
||||
self-heals any transitional window.
|
||||
- `NC_CHROOT_CACHE` in `basic_auth_middleware.rs:34-40` — keyed on
|
||||
`Uuid`, not username. Unaffected.
|
||||
|
||||
## Critical files
|
||||
|
||||
Full enumeration in the Deliverables sections above. Grouped summary:
|
||||
|
||||
**Ingest normalizer:**
|
||||
- `src/domain/entities/user.rs` (signature change + callers)
|
||||
|
||||
**Application services (write callers):**
|
||||
- `src/application/services/auth_application_service.rs`
|
||||
- `src/application/services/magic_link_invite_service.rs`
|
||||
|
||||
**Repositories (lookup normalization):**
|
||||
- `src/infrastructure/repositories/pg/user_pg_repository.rs`
|
||||
- `src/application/services/storage_usage_service.rs`
|
||||
- `src/cli/opaque.rs`
|
||||
|
||||
**NextCloud DAV surface:**
|
||||
- `src/interfaces/nextcloud/basic_auth_middleware.rs`
|
||||
- `src/interfaces/nextcloud/session.rs`
|
||||
- `src/infrastructure/services/nextcloud_chunked_upload_service.rs`
|
||||
|
||||
**New files:**
|
||||
- `src/common/username_migration.rs`
|
||||
- `tests/api/lowercase_usernames.hurl`
|
||||
|
||||
**Main entry:**
|
||||
- `src/main.rs` (call verifier after `sqlx::migrate!()`)
|
||||
|
||||
**Migration CLI:**
|
||||
- `src/cli/migrate.rs`
|
||||
|
||||
**Docs:**
|
||||
- `CHANGELOG.md`
|
||||
- `docs/config/env.md`
|
||||
- `docs/install/binary.md`
|
||||
|
||||
## Delivery order
|
||||
|
||||
1. Change `validate_username` signature to return `Result<String>` —
|
||||
one file.
|
||||
2. Fix OIDC JIT derivation
|
||||
(`auth_application_service.rs:4649-4690`) to lowercase before
|
||||
passing to `User::new` — explicit change beyond the entity
|
||||
normalizer's compile-time catches.
|
||||
3. Iterate on compile errors — the return-type change catches every
|
||||
downstream write-site.
|
||||
4. Update repository lookup methods (`user_pg_repository.rs`,
|
||||
`storage_usage_service.rs`, `cli/opaque.rs`) to internally
|
||||
lowercase input before `.bind()`.
|
||||
5. Update NC `basic_auth_middleware.rs` (lowercase `raw_username` at
|
||||
decode) + `session.rs::extract_url_user` (lowercase return).
|
||||
6. Add the boot-time verification helper (`src/common/username_migration.rs`)
|
||||
+ wire into `main.rs`.
|
||||
7. Extend `oxicloud migrate` with `lowercase-usernames [--dry-run]` —
|
||||
DB pass + chunked-upload directory rename.
|
||||
8. Test seed audit (grep pass).
|
||||
9. Add hurl coverage.
|
||||
10. CHANGELOG entry + admin docs update.
|
||||
11. Manual smoke test against dev DB.
|
||||
12. PR to canonical.
|
||||
|
||||
## Total scope estimate
|
||||
|
||||
~6-8 hours of careful work. Larger than the initial estimate because
|
||||
of these sweep-surfaced additions:
|
||||
|
||||
- OIDC JIT explicit fix (small).
|
||||
- Chunked-upload directory rename step in the migration (~30 min).
|
||||
- Test-seed audit (~15 min).
|
||||
- More lookup callsites than initially thought.
|
||||
|
||||
The shape is uniform (`to_ascii_lowercase()` at every touchpoint) and
|
||||
the compiler catches missed entity-write sites via the
|
||||
`Result<String>` signature change. The parts NOT caught by the
|
||||
compiler (OIDC JIT, lookup normalizers, NC URL segment, chunked-upload
|
||||
directory) are the ones needing careful review — enumerated above.
|
||||
|
||||
## References
|
||||
|
||||
- Issue: [#691](https://github.com/AtalayaLabs/OxiCloud/issues/691)
|
||||
- Related feature restrictions today:
|
||||
- `validate_username` at
|
||||
`src/domain/entities/user.rs:884-916`
|
||||
- `@`-disjoint dispatch at
|
||||
`src/application/services/auth_application_service.rs:1018`
|
||||
- Related project docs:
|
||||
- `docs/plan/auth-simplification.md` — the broader auth surface this
|
||||
fits within
|
||||
- Prior similar migration:
|
||||
`oxicloud migrate nfc-filenames` in `src/cli/migrate.rs`
|
||||
Reference in New Issue
Block a user