Files
vacuum-wall/scripts/bootstrap_auth.py
mteehan 183904faad 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
2026-08-18 00:00:09 +00:00

96 lines
3.6 KiB
Python

"""Bootstrap auth: initialize DB and seed admin user at install time.
Idempotent — safe to run on every install (and re-install):
- Writes config/auth/config.json only if it does not exist (existing
JWT/WebAuthn settings are preserved).
- Creates the admin user if missing; if the user already exists, updates
the admin password to the provided value (docs/deployment.md: "On
re-run, updates the admin password if already present").
- Suppresses the last-resort builtin admin seed (VACUUM_WALL_SEED_BUILTIN_ADMIN=0):
bootstrap is the operator user's creator on a fresh install, so exactly
one account exists and no hardcoded admin with an unrecoverable random
password is left behind.
Usage:
python scripts/bootstrap_auth.py --project-dir /path/to/project \
--username admin \
--password secret \
--domain wall.example.com
"""
import argparse
import json
import os
import sys
from pathlib import Path
# Ensure project lib is importable
def main() -> None:
parser = argparse.ArgumentParser(description="Bootstrap Vacuum Wall auth")
parser.add_argument("--project-dir", required=True, help="Project root directory")
parser.add_argument("--username", required=True, help="Admin username")
parser.add_argument("--password", required=True, help="Admin password")
parser.add_argument("--domain", required=True, help="Management domain (rp_id)")
args = parser.parse_args()
project_dir = Path(args.project_dir).resolve()
sys.path.insert(0, str(project_dir))
# Set DB path before importing lib modules
db_path = str(project_dir / "data" / "auth.db")
os.environ["VACUUM_WALL_DB_BACKEND"] = "sqlite"
os.environ["VACUUM_WALL_DB_PATH"] = db_path
# Suppress the last-resort builtin admin seed in get_db(): bootstrap
# creates the operator user itself, so no seeded admin may shadow it.
os.environ["VACUUM_WALL_SEED_BUILTIN_ADMIN"] = "0"
from lib.auth_users import (
ALL_SUBSYSTEMS,
create_user,
find_user,
reset_password,
)
# Write config — only if missing, so re-runs never clobber existing
# JWT/WebAuthn settings (e.g. a customized rp_id/origin).
config_dir = project_dir / "config" / "auth"
config_dir.mkdir(parents=True, exist_ok=True)
config_path = config_dir / "config.json"
if not config_path.exists():
config = {
"jwt": {
"access_token_ttl": 300,
"refresh_token_ttl": 604800,
"algorithm": "HS256",
},
"webauthn": {
"rp_name": "Vacuum Wall",
"rp_id": args.domain,
"origin": f"https://{args.domain}",
},
}
with open(config_path, "w") as f:
json.dump(config, f, indent=2)
f.write("\n")
print(f"Wrote auth config: {config_path}")
else:
print(f"Auth config already present, leaving unchanged: {config_path}")
# Initialize DB and create the admin user, or sync the password on re-run
if find_user(args.username) is not None:
reset_password(args.username, args.password)
print(f"Updated existing user: {args.username} (password synced)")
else:
permissions = {sub: "rw" for sub in ALL_SUBSYSTEMS}
user = create_user(args.username, args.password, permissions)
print(f"Created admin user: {user['username']} (id={user['id']})")
print(f"Permissions: {len(permissions)} subsystems, all 'rw'")
if __name__ == "__main__":
main()