* 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>
* fix(auth): let deployments close local self-registration
The OIDC provisioning policy (allowed_email_domains, require_verified_email,
auto_create_users) is enforced only in the SSO callback via
get_or_provision_oidc_user. POST /api/v1/auth/register creates a local account
without consulting any of it, and nothing can turn that path off, so a
deployment declaring an email-domain allowlist can still be joined by any
address through local registration.
Add auth.local.allow_registration (default true, so existing deployments are
unchanged) and gate /register on it before the account is created. Report the
flag from /setup-status so the login page stops offering a signup entry the
Gateway will reject.
/initialize is deliberately not gated: it is the bootstrap path, guarded by
admin_count == 0, and closing it would leave a fresh install unable to create
its first admin.
An unreadable config.yaml falls back to the pre-gate default (open) rather than
making these two endpoints a hard dependency on the file.
* docs(auth): align registration-gate fallback wording with the FileNotFoundError catch