OIDC identity: usernames, not emails — import fails on email owner, emails must never be exposed, and the stray back-to-form button #370
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#370
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's wrong (three linked defects from the same root)
The import form's owner dropdown offers and uses the user's email address (
crueber@gmail.com) as the repo owner. Three consequences, all visible in the screenshot:bad target: invalid repo id "crueber@gmail.com/walhub"— the targetowner/nameparser (git.ParseRepoIdviaParseRequest,internal/repoimport/service.go) rejects@and.in the owner segment, so the import can never succeed for an OIDC user.crueber@gmail.comto any visitor. Emails must never be exposed to users other than the logged-in user. The principal name is the email today (see root cause), so every surface that rendersPrincipal.Nameleaks it: repo paths,/explore, issue authorship, the commit/author displays, notifications.Root cause (code evidence)
principalFromEmailreturnsauth.Principal{Name: email, …}(internal/server/auth.go:230-277— note :277,Name: email, lowercased but otherwise the full address). Every OIDC login, session mint, andwgt_token derives identity the same way (:200-228).you and your orgs only(web/src/pages/Import.jsx, thegetOwners()select in the current tree) — so for an OIDC user the dropdown contains their email, and that value flows intoownerof the import request.@), and the identity model has no username concept at all — there is nousers/<username>object, no display-name-vs-login separation for users. #234 added an owner profile (display name etc.) but the profile is keyed by the principal (the email), not by a username.What's needed
A. Usernames as the identity key (the systemic fix — this is the real ticket):
crueber,crueber2), stored on a user object (users/<username>/user.jsonmapping username ↔ email), and immutable thereafter.principalFromEmailresolves email → username and returnsPrincipal{Name: <username>}— the email never becomes a principal name, owner segment, or path component anywhere.Principal.Nameor owner strings (repo paths, explore listings, issue authorship, timeline events, notifications, invite subjects) — after the fix they all show usernames, and emails appear nowhere user-facing except the user's own settings.crueber@gmail.com/...) need a rename/mapping decision — at minimum a store-level alias so old keys resolve to the username namespace; enumerate affected keys in the PR.B. Import/create owner plumbing: with usernames in place, the owner dropdown options and the submitted
ownerare valid ID parts, and thebad targetfailure disappears. Until A lands, the form should at minimum filter out any option that isn't a valid ID part — but A is the fix, not the filter.C. Remove "back to form" from the inline-error state: it should only render when the user is NOT already on the form (i.e. on the outcome/progress view after a started import). One conditional fix in
Import.jsx.Acceptance criteria
@), assigned at first login, collision-uniquified, immutable, and stable across sessions.bad targeterror is gone).Fixed by PR #373 (#373): OIDC principals are usernames (derived at first login, collision-uniquified, immutable, email kept on Principal.Email for alias matching only). Owner dropdown holds usernames+orgs (username import admitted, email owner 400s), leak audit tests assert no cross-user email rendering (own email on own profile only), migration is a read-side alias with affected keys enumerated in docs/features/01 §Decisions, and back-to-form lives on the outcome view only.
Review: PR #373 (fix/issue-370) -- one real race found + fixed, rest verified
Scratch worktree
/tmp/pr373@4724e5b(includes my fix commit below). No browser (per instructions -- tests + reasoning; no UI behavior in this PR depends on a live render beyond the Import.jsx conditional, which is statically verified). Main worktree untouched (still clean).FOUND + FIXED: same-email concurrent first-login split identity (CAS race)
internal/identity/users.goResolveUsername: onPutCreate412 the loser didcontinueto the NEXT suffix without re-reading the winner. Two sessions racing first-login for one email claimed TWO usernames (race+race2, twouser.jsonfor one email) -- the loser session then lives under the wrong username permanently (alias points at the winner, but the loser session +user.jsonpersist). Proven with a barrier probe: 2/50 splits pre-fix. The file's own Concurrency section already documented "read the winner (same email -> adopt...)" -- the code just didn't do it.Fix pushed to
origin/fix/issue-370(4724e5b): on 412, re-GET the candidate in the same pass -- same email -> adopt + repair alias; foreign email -> next suffix; store error on re-read -> fail closed. Plus committed regression testTestResolveUsernameConcurrentFirstLogin(50 barrier iterations, shared memory store). Post-fix: 5/5-raceruns clean, fullidentity+server/authsuites green.Verified OK (adversarial pass)
server/auth/username.go): no@ever possible -- local-part only, every non-[a-z0-9._-]byte folded to-, andusernameForre-validates viaValidUsernamewith pure-base fallback. Multi-@, unicode, leading dots, 64-char truncation + suffix room,../userfallback,anonymous/anon->-usersuffix all sane.emailOf), username re-resolves per request; pre-change sessions (wire=email) resolve to the username post-change. Distinct-email collision (crueber@gmail.comvscrueber@other.com->crueber/crueber2) + repeat stability probed green.Principal.Nameis now the username by construction; deny messages, synthesized access, invite issuer (p.Name), org-hook titles, new-state rosters all username-only --TestEmailLeakAudit+auth370surface tests pin this.GET /users/{u}fillsEmailonly onmatchPrincipalself-match. No.Emailrenders to non-owners found inidentity/server.ValidPrincipalaccepting both spellings does NOT reintroduce email owners -- owner gate isCheckCreateOwner(self-match on username; email-as-owner falls to deny) +git.ParseRepoId(no@).isUserNamespaceexcludes synthetics/orgs (exact-keyorg.jsonprobe)/unclaimed (user.jsonprobe); errors fail closed. Writeless-self pin holds.Resolve/isOrgOwnerFor/inTeamFor/MemberOrgsForvia purematchPrincipal(no extra round trips, law 6 holds);CheckPushforwards{Name, Email}with flags stripped.allowedOwners+isValidOwnerPartfilter emails client-side; server 400s bad targets regardless. Back-to-form moved into outcome view only; inline-error copy verified gone.-raceclean;go build,gofmt,go vetclean; node 721/723 (2smoke.test.jsfailures pre-existing on main -- missingconcepts/push.gifasset);TestUIAssetConceptsfails identically on main. No new deps.Residuals (not blocking -- pre-existing data shapes, follow-ups)
serveCollaborators, team/org-owner expansion,GetAccess, rosters, stored issue/PR authors) -- criterion 3 holds for new state only. Follow-up: rewrite or read-side masking.ProfileKey(email)vsProfileKey(username)-- pre-migration display names stay under the email key; post-migration reads the username key. Consider lazy profile migration on first login.samePrincipal(doc.CreatedBy, p.Name)(api/placeholder.go:445,summary.go:248) -- legacy email-CreatedByunborn placeholders 409 for the same user post-migration; also missing from the 01 Decisions affected-keys enumeration.leak_test.gostep 4 (collaborators to other user) is a no-op pin (_ = w) -- make it a real assertion when (1) is addressed.MERGE RECOMMENDATION: ready to merge
The one structural defect (CAS split) is fixed on the branch with a regression test; all gates hold; residuals are documented pre-existing-data limitations, not regressions.
Fixed by PR #373 (review caught + fixed a concurrent-first-login split-brain; derivation, leak audit, migration, import, back-to-form verified), merged. Closing.