fix: auth review fixes — token revocation, WS auth, seeding, and hardening
Refresh/logout and token robustness - drop the post-rotation refresh_tokens row delete in auth_refresh so logout blacklists the current (rotated) refresh token; remove the dead _clear_refresh_token_after_rotation helper and clear_active_refresh_token - reject non-object JWT payloads in _extract_unverified_sub so crafted Authorization headers return 401 instead of crashing with 500 SQLite user store - make builtin-admin seeding idempotent: on a concurrent first start the losing seeder re-checks, finds the winner, and returns instead of raising IntegrityError - per-thread sqlite connections + busy_timeout so Flask worker threads don't hit cross-thread ProgrammingError / SQLITE_BUSY - add LogsDirectory + /var/log/vacuum-wall to ReadWritePaths in both systemd units so the fallback admin password actually lands on disk Frontend - skip apiFetch 401-recovery for public auth endpoints so a failed login no longer logs out a valid session - add /passkeys to the nav (passkey registration was unreachable); remove the dead checkWebAuthnCapable export - drop the CSP-blocked inline WS-URL script and the __WS_URL_PLACEHOLDER__ plumbing; the WS URL is derived from location Daemon / WS - parse Sec-WebSocket-Protocol manually (web.Request.get_subprotocols does not exist in aiohttp 3.13); X-Auth-Token is a custom-nginx fallback only — docstring and security docs corrected Install / system - bootstrap_auth.py is now idempotent: preserves existing auth config and syncs the admin password on re-runs (new reset_password helper) - WebUI server block renders auth_basic off (the UI is JWT-protected) - install.sh chown/chmod skips .git to avoid git dubious-ownership breakage - tolerate unreadable /etc/wireguard during system import Contracts / docs - create_user returns 409 on duplicate username per docs/api.md - correct docs/api.md response shapes, docs/security.md blacklist cleanup wording + one-refresh-per-user caveat, stale WS-URL references, and the .htpasswd description Tests: +7 regression tests (rotation/logout revocation, crafted-token 401, concurrent seeding); placeholder-substitution tests replaced with serve-as-is SPA root tests.
This commit is contained in:
+43
-28
@@ -1,7 +1,12 @@
|
||||
"""Bootstrap auth: initialize DB and seed admin user at install time.
|
||||
|
||||
Run once during installation. Writes config/auth/config.json with a
|
||||
generated JWT secret and creates the admin user in SQLite.
|
||||
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").
|
||||
|
||||
Usage:
|
||||
python scripts/bootstrap_auth.py --project-dir /path/to/project \
|
||||
@@ -35,38 +40,48 @@ def main() -> None:
|
||||
os.environ["VACUUM_WALL_DB_BACKEND"] = "sqlite"
|
||||
os.environ["VACUUM_WALL_DB_PATH"] = db_path
|
||||
|
||||
from lib.auth_users import ALL_SUBSYSTEMS, create_user
|
||||
from lib.auth_users import (
|
||||
ALL_SUBSYSTEMS,
|
||||
create_user,
|
||||
find_user,
|
||||
reset_password,
|
||||
)
|
||||
|
||||
# Write config
|
||||
# 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"
|
||||
|
||||
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}",
|
||||
},
|
||||
}
|
||||
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}")
|
||||
|
||||
with open(config_path, "w") as f:
|
||||
json.dump(config, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
print(f"Wrote auth config: {config_path}")
|
||||
|
||||
# Initialize DB and create admin user
|
||||
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'")
|
||||
# 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__":
|
||||
|
||||
+30
-15
@@ -212,15 +212,26 @@ mkdir -p "${PROJECT_DIR}/config"/{dnsmasq,nginx,wireguard,firewall}
|
||||
mkdir -p "${PROJECT_DIR}/data"/{nginx/sites-enabled,dnsmasq,firewall,wireguard,acme}
|
||||
mkdir -p /etc/wireguard
|
||||
mkdir -p /etc/dnsmasq
|
||||
# Set ownership: daemon owns project dir in prod, repo owner keeps ownership in dev
|
||||
# Set ownership: daemon owns project dir in prod, repo owner keeps ownership in dev.
|
||||
# The top-level .git (directory or worktree pointer file) is left untouched so
|
||||
# the repo owner's git isn't tripped by git's dubious-ownership check.
|
||||
if [[ "$_cli_is_dev" == true ]]; then
|
||||
_dev_owner="$USER_NAME"
|
||||
else
|
||||
_dev_owner="$USER_DAEMON_NAME"
|
||||
fi
|
||||
chown -R "$_dev_owner:$USER_GROUP" "$PROJECT_DIR"
|
||||
chmod -R g+rwX "$PROJECT_DIR"
|
||||
find "$PROJECT_DIR" -type d -exec chmod g+s '{}' +
|
||||
(
|
||||
shopt -s dotglob nullglob
|
||||
for _entry in "$PROJECT_DIR"/*; do
|
||||
[[ "$(basename "$_entry")" == ".git" ]] && continue
|
||||
chown -R "$_dev_owner:$USER_GROUP" "$_entry"
|
||||
chmod -R g+rwX "$_entry"
|
||||
find "$_entry" -type d -exec chmod g+s '{}' +
|
||||
done
|
||||
# Top dir: ownership + shared-group access (never .git)
|
||||
chown "$_dev_owner:$USER_GROUP" "$PROJECT_DIR"
|
||||
chmod g+rwX,g+s "$PROJECT_DIR"
|
||||
)
|
||||
|
||||
# --- 4. Template rendering function ---
|
||||
# Renders Jinja2 templates by injecting env vars as template context.
|
||||
@@ -344,21 +355,26 @@ else
|
||||
echo ""
|
||||
echo " Setting up initial management configuration..."
|
||||
|
||||
# Bootstrap auth: generate config + seed admin user
|
||||
# Bootstrap auth: generate config + seed admin user. bootstrap_auth.py
|
||||
# is idempotent — on re-run it preserves the existing config and
|
||||
# updates the admin password to MGMT_PASS (docs/deployment.md).
|
||||
if [[ ! -f "${PROJECT_DIR}/config/auth/config.json" ]]; then
|
||||
echo ""
|
||||
echo " Bootstrapping auth (creating admin user: $MGMT_USER)..."
|
||||
|
||||
"${PROJECT_DIR}/.venv/bin/python3" "${PROJECT_DIR}/scripts/bootstrap_auth.py" \
|
||||
--project-dir "$PROJECT_DIR" \
|
||||
--username "$MGMT_USER" \
|
||||
--password "$MGMT_PASS" \
|
||||
--domain "$DOMAIN"
|
||||
|
||||
chown -R "$USER_DAEMON_NAME:$USER_GROUP" "${PROJECT_DIR}/config/auth"
|
||||
chown -R "$USER_DAEMON_NAME:$USER_GROUP" "${PROJECT_DIR}/data/auth.db" 2>/dev/null || true
|
||||
else
|
||||
echo ""
|
||||
echo " Syncing admin password for existing user: $MGMT_USER..."
|
||||
fi
|
||||
|
||||
"${PROJECT_DIR}/.venv/bin/python3" "${PROJECT_DIR}/scripts/bootstrap_auth.py" \
|
||||
--project-dir "$PROJECT_DIR" \
|
||||
--username "$MGMT_USER" \
|
||||
--password "$MGMT_PASS" \
|
||||
--domain "$DOMAIN"
|
||||
|
||||
chown -R "$USER_DAEMON_NAME:$USER_GROUP" "${PROJECT_DIR}/config/auth"
|
||||
chown -R "$USER_DAEMON_NAME:$USER_GROUP" "${PROJECT_DIR}/data/auth.db" 2>/dev/null || true
|
||||
|
||||
# Helper: POST JSON to daemon API over Unix socket
|
||||
_daemon_post() {
|
||||
local endpoint="$1"
|
||||
@@ -381,7 +397,6 @@ else
|
||||
_daemon_post "/acme/self-signed" "{\"domain\":\"$DOMAIN\"}" "Self-signed certificate"
|
||||
|
||||
# 1C. Management proxy domain (no auth — JWT auth is handled by Flask)
|
||||
local mgmt_json
|
||||
mgmt_json="$(jq -n \
|
||||
--arg domain "$DOMAIN" \
|
||||
'{
|
||||
|
||||
Reference in New Issue
Block a user