fix: harden auth and fix frontend issues
- Add builtin admin user with full access, immutable permissions (lib/db.py, lib/auth_users.py, webui/static/pages/users.js) - Fix passkeys TypeError on string throws (webui/static/pages/passkeys.js) - Add zero-permission warning in create user modal (webui/static/pages/users.js) - Restore readonly on proxy paths textarea (webui/static/pages/proxy.js) - Mask credential ownership errors to prevent enumeration (lib/webauthn.py, tests/test_auth.py)
This commit is contained in:
@@ -31,6 +31,9 @@ from lib.password import hash_password, needs_rehash, verify_password
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Builtin admin — hardcoded, full access, cannot be modified/deleted
|
||||
BUILTIN_ADMIN_USERNAME = "admin"
|
||||
|
||||
# All subsystem names for default permission assignment
|
||||
ALL_SUBSYSTEMS = [
|
||||
"firewall",
|
||||
@@ -211,7 +214,13 @@ def update_permissions(username: str, permissions: dict[str, str]) -> None:
|
||||
Args:
|
||||
username: The username.
|
||||
permissions: Dict mapping subsystem names to permission levels.
|
||||
|
||||
Raises:
|
||||
ValueError: If attempting to modify builtin admin.
|
||||
"""
|
||||
if username == BUILTIN_ADMIN_USERNAME:
|
||||
raise ValueError("Cannot modify permissions for builtin admin")
|
||||
|
||||
user = find_user(username)
|
||||
if user is None:
|
||||
raise ValueError(f"User {username!r} not found")
|
||||
|
||||
@@ -18,6 +18,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import secrets
|
||||
from abc import ABC, abstractmethod
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar
|
||||
@@ -271,11 +272,40 @@ def get_db() -> Database:
|
||||
_db_instance = SQLiteBackend(path)
|
||||
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
||||
_db_instance.init_tables()
|
||||
_seed_builtin_admin(_db_instance)
|
||||
else:
|
||||
raise ValueError(f"Unknown database backend: {backend!r}")
|
||||
return _db_instance
|
||||
|
||||
|
||||
def _seed_builtin_admin(db: Database) -> None:
|
||||
"""Create the builtin admin user if they don't exist.
|
||||
|
||||
The builtin admin has full (rw) access to all subsystems and cannot
|
||||
be deleted or have permissions modified through the normal API.
|
||||
"""
|
||||
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:
|
||||
return
|
||||
|
||||
# Create with a placeholder password that should be changed
|
||||
placeholder_hash = hash_password("CHANGEME")
|
||||
jwt_secret = secrets.token_urlsafe(32)
|
||||
|
||||
with db.in_transaction() as tx:
|
||||
tx.run_one(
|
||||
Q_INSERT_USER, (BUILTIN_ADMIN_USERNAME, placeholder_hash, jwt_secret)
|
||||
)
|
||||
for subsystem in ALL_SUBSYSTEMS:
|
||||
tx.run(Q_UPSERT_PERMISSION, (BUILTIN_ADMIN_USERNAME, subsystem, "rw"))
|
||||
|
||||
logger.info("Builtin admin user created with full access")
|
||||
|
||||
|
||||
def reset_db_for_test() -> None:
|
||||
"""Reset the singleton — only for tests."""
|
||||
global _db_instance
|
||||
|
||||
+1
-1
@@ -337,7 +337,7 @@ def remove_credential(username: str, credential_id: str) -> bool:
|
||||
if not rows:
|
||||
raise ValueError(f"Credential {credential_id!r} not found")
|
||||
if rows[0]["username"] != username:
|
||||
raise ValueError("Credential does not belong to this user")
|
||||
raise ValueError(f"Credential {credential_id!r} not found")
|
||||
|
||||
db.run(Q_DELETE_WEBAUTHN, (credential_id,))
|
||||
logger.info("WebAuthn credential removed: %s, %s", username, credential_id)
|
||||
|
||||
Reference in New Issue
Block a user