Throttle the change-password route and make epoch bumps atomic
Two session-management bugs from the audit: - /api/password recorded failed attempts against the login budget but never checked it, so a borrowed session could guess the current password without limit while still locking the owner out of /api/login. Verified: thirteen consecutive wrong guesses all returned 403 and none returned 429. It now spends from the same budget it was topping up. - bump_epoch read the epoch and wrote it back without the write lock, so concurrent revocations lost increments and sessions that should have been cut off survived. Verified: twenty concurrent bumps advanced the counter from 2 to 6, and twenty concurrent "sign out other devices" calls left four sessions authenticated. It takes BEGIN IMMEDIATE now; set_password hashes before locking, so scrypt doesn't serialise unrelated writes. Removing either fix makes its test fail with exactly that symptom. README corrections: deployment is rsync, not git pull — gitea on .8 cannot serve a clone to .8 itself, which the deploy section now documents — and the concurrency check count was understated. Checks go from 182 to 197. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,22 +45,24 @@ Then open http://127.0.0.1:8123.
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
.venv/bin/python -m tests.test_api # 166 checks, in-process
|
||||
.venv/bin/python -m tests.test_concurrency # 15 checks, against a real uvicorn
|
||||
.venv/bin/python -m tests.test_api # 172 checks, in-process
|
||||
.venv/bin/python -m tests.test_concurrency # 25 checks, against a real uvicorn
|
||||
```
|
||||
|
||||
`test_api` exercises the API end to end against a throwaway database — the auth
|
||||
gate and session-token signing, nested categories and locations, search across
|
||||
every indexed field, filter rollups, stock adjustment and history, patch
|
||||
semantics including explicit nulls, taxonomy cycle rejection, security headers
|
||||
and asset versioning, login throttling, and the full password-management flow
|
||||
including scrypt hashing, session invalidation and the recovery CLI.
|
||||
and asset versioning, login throttling on both the login and change-password
|
||||
routes, and the full password-management flow including scrypt hashing, session
|
||||
invalidation and the recovery CLI.
|
||||
|
||||
`test_concurrency` needs a real server process, because a lost update only shows
|
||||
up when two requests genuinely overlap inside SQLite. It fires overlapping
|
||||
adjustments, patches and creates at one part and asserts the stock log always
|
||||
sums to the stored quantity, and races taxonomy renames against reads to check
|
||||
the search index never describes a name the tree no longer has.
|
||||
sums to the stored quantity; races taxonomy renames against reads to check the
|
||||
search index never describes a name the tree no longer has; and races session
|
||||
revocations to check no epoch increment is lost.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -100,15 +102,31 @@ Lives at `/home/jay/srv/parts/` on `.8` and follows the same conventions as the
|
||||
other services there — `build: .`, external `caddy_web` network, named volume
|
||||
for `/data`.
|
||||
|
||||
`~/srv/parts` is a checkout of this repository, so deploying is a pull:
|
||||
`~/srv/parts` is a checkout of this repository — `git log` there tells you
|
||||
exactly what is deployed — but **deploys go over rsync, not `git pull`**:
|
||||
|
||||
```sh
|
||||
ssh jay@192.168.50.8
|
||||
cd ~/srv/parts && git pull && docker compose up -d --build
|
||||
rsync -az --delete \
|
||||
--exclude '.venv' --exclude 'data' --exclude '__pycache__' --exclude '*.pyc' \
|
||||
--exclude '.env' --exclude '.env.bak-*' \
|
||||
./ jay@192.168.50.8:/home/jay/srv/parts/
|
||||
ssh jay@192.168.50.8 'cd ~/srv/parts && docker compose up -d --build'
|
||||
```
|
||||
|
||||
`.env` is untracked and gitignored, so it survives the pull. `~/srv` (the infra
|
||||
repo) ignores `parts/`, since this directory is its own repository.
|
||||
`.env` is excluded from the transfer, and rsync's `--delete` leaves excluded
|
||||
files alone, so the deployed credentials survive.
|
||||
|
||||
The reason it isn't `git pull`: **gitea on `.8` cannot serve a clone or fetch to
|
||||
`.8` itself.** Auth succeeds and gitea logs `git-upload-pack ... 200 OK`, then
|
||||
the transfer dies with `fetch-pack: unexpected disconnect while reading sideband
|
||||
packet`. Reproduced against both `git.tjm77.com:2222` and `127.0.0.1:2222`, with
|
||||
`--depth 1`, with protocol v0, and with fsck disabled; `git ls-remote` succeeds
|
||||
every time, and cloning the same repo from a laptop works. Undiagnosed. Until
|
||||
it's fixed, the checkout in `~/srv/parts` is placed there by rsyncing the
|
||||
working tree *including* `.git`, which is why `git status` there is clean.
|
||||
|
||||
`~/srv` (the infra repo) ignores `parts/`, since this directory is its own
|
||||
repository.
|
||||
|
||||
The database is in the `parts_parts_data` docker volume, which survives
|
||||
rebuilds.
|
||||
|
||||
Reference in New Issue
Block a user