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:
+17
-10
@@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user