Skip to content

Refactor CLI ownership checks and fix bugs from full review - #15

Merged
SatvikPraveen merged 3 commits into
mainfrom
refactor/cli-task-ownership
Oct 12, 2026
Merged

SatvikPraveen merged 3 commits into
mainfrom
refactor/cli-task-ownership

Conversation

@SatvikPraveen

Copy link
Copy Markdown
Owner

Follow-up to #6. Refactors the CLI so ownership checks can't be forgotten, then fixes the bugs a full review of the project found.

Refactor

  • Single-task CLI commands (update, complete, delete) look tasks up through TaskManager._find_owned_task(), which only returns the logged-in user's task.
  • Login checks use the existing @require_login decorator instead of per-method copies (it was defined but unused).
  • This closed a second leak of the same kind: list-tasks while logged out printed every user's tasks.

Security fixes

  • Rate-limit bypass: login/register limits were keyed on the client-written X-Forwarded-For, so a new header per request gave unlimited attempts. The limiter now keys on the peer address, and the container trusts forwarded headers only from FORWARDED_ALLOW_IPS (default 127.0.0.1; it was *).
  • Username enumeration: a password over 72 bytes made login return 500 for unknown users and 401 for real ones. Registration now rejects such passwords (422), and login cuts input to bcrypt's 72 bytes.
  • Refresh race: parallel refreshes of one token each got a new token (5 of 8 in the repro). Revocation is now atomic and happens before issuing.
  • Migration accounts: every migrated user got the password <username>@temp123, and JSON tasks could attach to an unrelated existing account with the same name. Accounts now get random one-time passwords, existing usernames are skipped along with their tasks, and the new PUT /api/users/me/password lets users replace the password.

Correctness fixes

  • Concurrent registration of one username: 500 → 400.
  • PostgreSQL in a non-UTC session returned due dates one day early; SQLite timestamps had no UTC offset.
  • null can now clear description/email; required fields reject null.
  • Search treats % and _ literally.
  • JWT_ALGORITHM is restricted to HMAC algorithms.
  • CLI: one send_reminders run locked the user out (last_reminder_date crashed User(**data)).
  • CLI: reminders were emailed even when turned off; failed sends were recorded as delivered.
  • CLI: unvalidated --due corrupted the data file; bad rows crashed listing and the cron job for every user.
  • CLI: tasks.json is written atomically; corrupt files give a clear error.
  • CLI: login works without a TTY and honours --email.
  • Migration: completed tasks were stamped with the migration time, skewing stats.
  • demo_api.py crashed at step 5; the container CLI couldn't write its data.

Verification

  • ruff, ruff format --check, mypy, bandit -ll, pip-audit: clean. alembic check: no drift.
  • Tests: 101 → 135 passing (2 Redis tests skipped locally), coverage 91.7%. New regression tests are in tests/test_api_hardening.py and tests/test_cli_hardening.py. 31 of the 33 fail on the previous code; the other two guard behaviour that was already correct.
  • demo_api.py ran end to end against a live server: all 12 steps, no 500s.
  • Docker image builds; task-manager login works in the container without a TTY; the API reports healthy.

Not covered here: the Redis-backed limiter and denylist, which only run in CI's PostgreSQL+Redis job, and behaviour on a real PostgreSQL server in a non-UTC zone (tested by unit test only).

Single-task commands (update, complete, delete) now look tasks up through
TaskManager._find_owned_task(), which only returns a task owned by the
logged-in user, so a new command can't forget the check. Login checks use
the existing @require_login decorator instead of per-method copies.

This also closes a related leak: list-tasks while logged out printed
every user's tasks; it now requires a login like the other commands.
- Rate limit keyed on the peer address, not the client-written
  X-Forwarded-For; the container trusts forwarded headers only from
  FORWARDED_ALLOW_IPS (default 127.0.0.1) instead of everyone.
- Passwords over bcrypt's 72-byte limit are rejected at registration;
  login cuts input to 72 bytes so long passwords no longer 500 for
  unknown users only (which revealed which usernames exist).
- Token revocation is atomic (SET NX / locked check-and-set) and refresh
  revokes before issuing, so parallel refreshes of one token yield one.
- Concurrent registration of one username returns 400, not 500.
- Due dates are converted to UTC before taking the date (PostgreSQL in a
  non-UTC session returned the previous day); naive timestamps from
  SQLite are returned as UTC.
- PUT accepts null to clear description/email; required fields reject it.
- Task search escapes LIKE wildcards.
- JWT_ALGORITHM is limited to HS256/HS384/HS512.
- Migration: random one-time passwords instead of <username>@temp123;
  usernames that already exist are skipped with their tasks (no more
  tasks attached to someone else's account, re-runs are no-ops);
  created_at carried over, completed_at left unknown.
- New PUT /api/users/me/password so those passwords can be replaced.
- CLI tolerates extra keys in user records (send_reminders writes
  last_reminder_date, which crashed every later command).
- Email reminders honour email_reminders_enabled and report failures;
  the cron job records only delivered reminders and skips bad rows.
- Due dates are validated on add/update; a corrupt row no longer
  crashes listing or reminders.
- tasks.json is written atomically; corrupt files give a clear error.
- login works without a TTY and honours --email for existing users.
- demo_api.py reads the paged list response and uses httpx.
- Container runs task-manager from the writable /app/data.
@SatvikPraveen
SatvikPraveen merged commit 75c8089 into main Oct 12, 2026
9 checks passed
@SatvikPraveen
SatvikPraveen deleted the refactor/cli-task-ownership branch October 12, 2026 00:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant