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:
2026-07-28 18:52:03 +00:00
parent a82578f342
commit 8ae60ab8cf
7 changed files with 78 additions and 22 deletions
+17 -10
View File
@@ -189,7 +189,8 @@ class TestDBLayer:
db.run(Q_INSERT_USER, ("aaa", "$argon2id$hash", "test-secret"))
db.run(Q_INSERT_USER, ("bbb", "$argon2id$hash", "test-secret"))
rows = db.query(Q_SELECT_ALL_USERS, ())
assert len(rows) == 2
# admin (builtin seed) + aaa + bbb, ordered by username
assert len(rows) == 3
assert rows[0]["username"] == "aaa"
@@ -200,7 +201,6 @@ class TestJWT:
def setup_method(self) -> None:
reset_db_for_test()
get_db()
create_user("admin", "secretpass", {"firewall": "rw"})
def test_generate_tokens(self):
tokens = generate_tokens("admin", {"firewall": "rw"})
@@ -347,7 +347,8 @@ class TestUserManagement:
create_user("listuser1", "pass1", {"firewall": "rw"})
create_user("listuser2", "pass2", {"network": "rw"})
users = list_users()
assert len(users) == 2
# admin (builtin) + listuser1 + listuser2
assert len(users) == 3
usernames = {u["username"] for u in users}
assert "listuser1" in usernames
@@ -368,13 +369,13 @@ class TestLoginFlow:
def test_full_login_flow(self):
"""Create user → verify password → generate tokens → validate tokens."""
user = create_user("admin", "secretpass", {"firewall": "rw", "auth": "rw"})
assert verify_user_password("admin", "secretpass") is not None
user = create_user("testadmin", "secretpass", {"firewall": "rw", "auth": "rw"})
assert verify_user_password("testadmin", "secretpass") is not None
tokens = generate_tokens("admin", user["permissions"])
tokens = generate_tokens("testadmin", user["permissions"])
payload = validate_token(tokens["access_token"], "access")
assert payload is not None
assert payload["sub"] == "admin"
assert payload["sub"] == "testadmin"
assert payload["permissions"]["firewall"] == "rw"
def test_logout_flow(self):
@@ -695,11 +696,16 @@ class TestWebAuthnCredentials:
remove_credential("testuser", "nonexistent")
def test_remove_credential_wrong_user(self) -> None:
"""Remove fails for another user's credential with same error as not-found.
Both cases return 404 — no distinction leaked to prevent credential
enumeration. Internal ownership check still prevents cross-user deletion.
"""
_insert_cred("otheruser", cred_id=_FAKE_CRED_ID)
from lib.webauthn import remove_credential
with pytest.raises(ValueError, match="does not belong"):
with pytest.raises(ValueError, match="not found"):
remove_credential("testuser", _FAKE_CRED_ID)
@@ -844,7 +850,7 @@ class TestMultiUserAdmin:
"""
from daemon.handlers.auth import auth_delete_user
create_user("admin", "password123", {"auth": "rw"})
create_user("tester", "password123", {"auth": "rw"})
create_user("target", "password123", {"firewall": "read"})
# Handler can delete other users
@@ -927,7 +933,8 @@ class TestMultiUserAdmin:
create_user("charlie", "pass3", {})
users = list_users()
assert len(users) == 3
# admin (builtin) + alice + bob + charlie
assert len(users) == 4
by_name = {u["username"]: u for u in users}