* fix(auth): claim the first admin atomically instead of counting first
POST /api/v1/auth/initialize read the admin count in one session and
created the admin in another, with no lock between them. Two requests
arriving together with different emails both saw an empty system and
both became admins: reproduced over HTTP, two 201s and two admin rows,
after which setup-status reports an initialized system.
The endpoint is public and unthrottled, and during the pre-setup window
anyone who can reach the gateway can claim admin with a single request
anyway — that part is the first-boot design. What the race removes is
the only signal: without it exactly one request wins and the loser gets
409, so an operator who loses sees "already initialized" and looks into
it. With it, both succeed and the operator's setup looks ordinary.
The check and the insert now share one transaction in the repository,
with writers serialized first — SQLite takes the write lock up front
(BEGIN IMMEDIATE) and Postgres a transaction-scoped advisory lock, the
idiom the project and channel repositories already use. The same
repository serves both backends, and multi-replica deployments make the
race genuinely parallel rather than an event-loop interleaving.
create_first_admin returns None when the claim is lost, which the route
reports as 409; uniqueness conflicts still raise ValueError and stay a
400. The count in the handler is now only a fast path that avoids
hashing a password for an already-initialized system.
AUTH_DESIGN.md said concurrent initialization was backstopped by a
database unique constraint. No such constraint exists — email
uniqueness only catches the same address submitted twice — so it now
describes the claim that does the work.
* docs(changelog): note the atomic first-admin claim (#5776)
* fix(auth): refuse a dialect the first-admin claim cannot serialize
Review follow-up. The claim's serialization handled sqlite and
postgresql and fell through silently for anything else, which would
leave create_first_admin as the plain check-then-act it exists to
prevent — the one place the abstract method's atomicity contract is
enforced.
An unknown dialect now raises instead. The engine builds only these two
today, so this guards a future backend: it fails loudly at first boot
rather than quietly reopening the race.
* feat(auth): make login rate-limit parameters configurable, fixes#5108
Add auth.local.max_login_attempts (default 5) and auth.local.lockout_seconds
(default 300) so operators can tune the per-IP login throttle: raise the
ceiling for shared-egress-IP offices behind proxies/NAT, or tighten it for
stricter posture. Policy is live-read per call (matching the
_local_registration_enabled precedent), so a config reload applies without a
Gateway restart; raising the threshold mid-lockout immediately unblocks
affected IPs.
Review feedback addressed (willem-bd):
- Only FileNotFoundError falls back to the hardcoded defaults; a malformed
config propagates, mirroring _local_registration_enabled, so an operator
who tightened the policy never silently gets the more permissive defaults.
- _check_rate_limit looks up the record before resolving the policy, so a
clean IP pays zero config reads (get_app_config re-hashes config.yaml per
call and login_local is an unauthenticated async endpoint).
Bumps config_version to 39 in config.example.yaml and the Helm chart
(values.yaml + README example) so the chart drift check stays green.
* fix(auth): reject max_login_attempts=1 and honor live lockout_seconds for active lockouts
* fix(auth): close live-policy state gaps in login throttle (resurrection, count reset, broken-config verification)
* fix(auth): commit evaluated lockout duration on decreases too, preventing raise-resurrection
* test(auth): pin broken-config fail-closed sequence through the login route
* fix(auth): sweep expired locks by stored sentence and keep policy reads off the event loop
* fix(auth): re-read throttle record after the policy-resolution yield point
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
binding, Internal Auth) and group IM bindings with direct HTTP under a
shared platform-trust model. Explain Internal Auth trust boundaries:
DeerFlow validates only the platform token, does not manage end-user
accounts in users, and relies on the channel to maintain owner identity.
Cross-link AUTH_DESIGN, API.md, and docs README
* docs: document auth design and user isolation
* docs: align auth docs with current storage and reset behavior
---------
Co-authored-by: greatmengqi <chenmengqi.0376@bytedance.com>