fix: seed builtin admin only on empty DB; recover page-load sessions with one refresh
Auth seeding (last-resort guard) - `_seed_builtin_admin()` in get_db() now skips when VACUUM_WALL_SEED_BUILTIN_ADMIN=0 or when the users table already contains any user — previously a fresh service start after a non-default bootstrap (e.g. --mgmt-user alice) seeded a hard-coded `admin` with an unrecoverable random password, shadowing the operator's account - bootstrap_auth.py sets VACUUM_WALL_SEED_BUILTIN_ADMIN=0: bootstrap creates the operator user itself on a fresh install, so exactly one account exists and no seeded admin can appear Frontend (session recovery) - on page load/restore the in-memory TTL timer is gone, so a valid 7-day refresh token could sit in sessionStorage while the access token is already expired server-side: the session `check` now attempts exactly one refresh (POST /api/auth/refresh with the stored refresh token) on 401 before treating the session as dead - extract shared `_doRefresh()` used by both the `check` 401 fallback and the `refresh` action (removes the duplicated rotation logic) Tests - update seeding tests to the new any-user-present check; add test_seed_skipped_when_users_exist, test_seed_skipped_via_env, test_bootstrap_flow_creates_exactly_one_user, and the auth-model JS test suite (tests/test-auth-model.js) Docs - AGENTS.md: document VACUUM_WALL_SEED_BUILTIN_ADMIN - architecture.md / hoover.md / security.md: describe the bootstrap check 401 → one-refresh fallback path
This commit is contained in:
@@ -288,17 +288,30 @@ def get_db() -> Database:
|
||||
|
||||
|
||||
def _seed_builtin_admin(db: Database) -> None:
|
||||
"""Create the builtin admin user if they don't exist.
|
||||
"""Create the builtin admin user as a last-resort fallback.
|
||||
|
||||
Seeding is skipped when:
|
||||
|
||||
- ``VACUUM_WALL_SEED_BUILTIN_ADMIN`` is set to ``0`` — bootstrap_auth.py
|
||||
sets this, since bootstrap creates the operator user itself and must
|
||||
not leave a hardcoded ``admin`` with an unrecoverable random password, or
|
||||
- the users table already contains users — accounts exist, so bootstrap
|
||||
(or a prior start) has run.
|
||||
|
||||
The seed therefore only fires on a completely empty database, i.e.
|
||||
bootstrap was genuinely skipped and a service starts first.
|
||||
|
||||
The builtin admin has full (rw) access to all subsystems and cannot
|
||||
be deleted or have permissions modified through the normal API.
|
||||
"""
|
||||
if os.environ.get("VACUUM_WALL_SEED_BUILTIN_ADMIN", "1") == "0":
|
||||
return
|
||||
|
||||
from lib.auth_users import ALL_SUBSYSTEMS, BUILTIN_ADMIN_USERNAME
|
||||
from lib.password import hash_password
|
||||
|
||||
# Check if admin already exists
|
||||
rows = db.query(Q_SELECT_USER_BY_NAME, (BUILTIN_ADMIN_USERNAME,))
|
||||
if rows:
|
||||
# Last-resort guard: only seed when no users exist at all.
|
||||
if db.query(Q_SELECT_ALL_USERS):
|
||||
return
|
||||
|
||||
# Generate a random password — this fallback should only fire if
|
||||
|
||||
Reference in New Issue
Block a user