Validate admin config and registration input, and log config changes
fee_address was the dangerous one (B-05). PUT /admin/config assigned whatever it
was given, and a well-formed address from another chain (bc1...) parses fine as a
witness program — so every round's 30% commission would be signed and broadcast
to a script nobody holds the key for. A malformed one instead wedged the payout
with an unhandled EmbitError. It now has to pass is_valid_plm_address, the same
check user withdrawals already had. Numeric fields got bounds too:
fee_rate_sat_vb=0 produces transactions no node relays, which stalls bets,
payouts and withdrawals alike, and round_duration_seconds=0 expires a round the
instant it opens.
Config changes are audit-logged (B-10). /pause and /resume were logged but a
config edit wasn't, so the most sensitive setting in the system could be changed
without leaving any trace — contradicting CLAUDE.md, which says audit_log records
what changed. The entry carries a before/after diff per field, computed before
assignment, and no-op updates write nothing. `paused` was removed from
_CONFIG_FIELDS so the maintenance switch has exactly one audited path; it stays
in the response model.
Admin token comparison is constant-time (B-14), with the empty-token check kept
*ahead* of it: compare_digest("", "") returns True, so the obvious ordering would
have opened the panel on any instance without an ADMIN_TOKEN.
Registration input (B-12). It accepted an empty username and a one-character
password while /users/me/change-password demanded 8 — an odd place to be lenient
on a custodial system holding real funds. MIN_PASSWORD_LENGTH moved to
auth/security.py so both share it, and the username is constrained to 3-32 chars
of [A-Za-z0-9_.-]. The IntegrityError handler also distinguishes a username
collision (answers username_taken) from a derivation-index one (retries): a
concurrent duplicate username used to be retried five times and then reported as
derivation_index_conflict, which told the user the wrong thing.
verify_password (B-13) catches VerificationError and InvalidHashError, not just
VerifyMismatchError, so an unparseable stored hash reads as "wrong password"
instead of a 500 — logged as an error, since that one is a data problem.
guida-admin.md gains a table of the audit events worth watching, including
payout_failed, which needs manual intervention.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
daf66fd6bc
commit
85dce221c5
8 files changed
+229
-30
No files matched your search
@@ -11,3 +11,21 @@ def test_jwt_roundtrip(monkeypatch):
|
||||
monkeypatch.setattr(security.settings, "jwt_secret", "test-secret")
|
||||
token = security.create_access_token(user_id=42)
|
||||
assert security.decode_access_token(token) == 42
|
||||
|
||||
|
||||
def test_verify_password_returns_false_for_an_unparseable_hash():
|
||||
"""B-13: only VerifyMismatchError was caught, so a corrupted stored hash raised
|
||||
InvalidHashError and became an unhandled 500 on the login endpoint instead of a
|
||||
plain "wrong credentials" 401."""
|
||||
from app.auth.security import verify_password
|
||||
|
||||
assert verify_password("whatever", "not-an-argon2-hash") is False
|
||||
assert verify_password("whatever", "") is False
|
||||
|
||||
|
||||
def test_verify_password_still_rejects_a_wrong_password():
|
||||
from app.auth.security import hash_password, verify_password
|
||||
|
||||
stored = hash_password("correct-horse-battery")
|
||||
assert verify_password("correct-horse-battery", stored) is True
|
||||
assert verify_password("wrong", stored) is False
|
||||
Reference in new issue
Block a user