fix(identity): validate usernames and emails against every system they reach #172
Inga granskare
Etiketter
Inga etiketter
area/backups
area/ci
area/control-panel
area/identity
area/infra
area/observability
area/payments
area/security
area/storage
area/web
blocked
needs-info
needs-triage
ready-for-implementation
type
bug
type
chore
type
docs
type
epic
type
feature
type
task
wontfix
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Inget förfallodatum satt.
Beroenden
Inga beroenden satta
Referens
bitborg/bitborg-web!172
Läser in…
Hänvisa till i nytt ärende
Ingen beskrivning angiven.
Ta bort grenen "fix/identity-field-validation"
Borttagning av en gren är permanent. Även om den borttagna grenen kan fortsätta existera en kort tid innan den faktiskt tas bort, kan det INTE ångras i de flesta fall. Vill du fortsätta?
Closes #151, closes #152. Part of the identity-validation epic.
The portal was more permissive than the intersection of the two systems it projects into, so a
value it accepted could be refused downstream. The worst case was silent: sign-up succeeded
completely, and the first sign-in to the git host then failed with a 500 — no error shown, nothing
logged, no metric moved.
Adds
src/lib/identity-rules.tsas the single source of truth, read by sign-up, the account paneland the client-side
patternattributes. Every rule is transcribed from upstream at the pinnedversion and cited in the file and in its test, so an upgrade that changes a rule fails a test rather
than a user.
[a-z0-9._-]after; not UUID-shaped; notroot[-._]{2,}, no trailing separator; 33 reserved names;.atom .gpg .keys .png .rsssuffixesinput type=emailpatternThe duplicated ad-hoc email regex is gone from both modules.
Usernames are lower-cased visibly at the form, per the operator decision — the identity provider
does it silently anyway, so doing it visibly removes the divergence rather than hiding it, and keeps
the consent record and the provisioned account in agreement by construction.
Two deliberate refinements worth review:
patternattribute accepts either case. With scripting off the field cannot lower-case asyou type, and a strict pattern would block
Alicebehind an unexplained browser message when bothother layers would simply normalise it.
an existing account — the re-issue path — uses the identity provider's own looser rule, because
applying the intersection there would lock out any account created before this change that holds a
name the git host reserves. Those are precisely the people who need to recover.
#152 — the identity provider returns a clean
400versus409 conflicting_attributes, and thatdistinction was being discarded into one generic failure.
setPersonAttrsnow returnsinvalid/taken/errorwith the named attribute, and the account panel has matching copy inboth languages.
403/404stayerrordeliberately: a denied write means a missing delegation, andreporting that as bad input sends the user off correcting a correct address.
Verification
pnpm lint,pnpm check(0 errors),pnpm lang-check,pnpm test— 246 passing, up from 233.Needs an operator check before this is fully effective
production. It is three-valued and fails open: a refusal returns
null, logsaddress-in-use pre-check unavailable, and the authoritative post-write check still runs. So itcannot break anything, but it may be inert until the grant is confirmed. That log line is the
signal.
lookup at sign-up.
mismatch cannot recur, since the consent row is written from the same normalised value.
`setPersonAttrs` collapsed every non-OK response into `{ ok: false, reason: "error" }`, so "that address is malformed" and "that address already belongs to another account" both rendered as "something went wrong on our side" — pointing the user at us instead of at their input. Gitborg Auth draws the distinction cleanly and even names the conflicting attribute; we were throwing it away. It now returns a discriminated result — `invalid` / `taken` / `error`, plus the attribute the conflict named — classified on the status codes Gitborg Auth actually uses (409 for a uniqueness conflict, 400 for a refused value, everything else ours). A denied write stays "error" on purpose: a missing delegation on the service account must never be reported as bad input, or the user is sent off correcting an address that was already correct. The account panel gains matching copy in both languages, for the name and the address. Two follow-on fixes: Confirming an address consumes the single-use token BEFORE the write, which is right against replay but means a rejected value burns the link. The common rejection is now caught before a token is minted, by asking whether the address is already in use. That check is three-valued — the service account may not be allowed to search, and it must never be the reason a legitimate change is refused, so "could not tell" proceeds and the write stays the authority. When a write is rejected after the fact, the user is told plainly that the address cannot be used and the link is spent, rather than being left to retry into the same wall forever. Sign-up logged only a thrown network error, never a refusal, so an entire class of rejected input produced no log line and never raised the provisioning alert — the same blind spot the alert exists to close. Every non-OK response on that path is now logged, under the prefix the alert already matches. Closes #152.6ae1d66cafad9929e841