Files
Alexander Eichhorn 3e72b88839 Harden/router auth and bugfixes (#9200)
* harden(api): enforce auth on 4 routers, fix 4 router bugs, add route tests

Add auth dependencies to download_queue, style_presets, model_relationships,
and utilities routers, which previously had none — under multiuser=True these
endpoints were anonymously reachable. CurrentUserOrDefault for read/per-resource
routes; AdminUserOrDefault for global actions (prune/cancel-all downloads,
style preset import/export, model relationship add/remove). Single-user mode
behavior is unchanged.

Fix four bugs in the same routers:
- model_relationships: HTTPException(400) from the self-relationship check was
  caught by `except Exception` and re-raised as 500. Move the check before the
  try-block and drop the broad except so the 400 reaches the client.
- style_presets update_style_preset: image was persisted before json.loads
  could fail, leaving the preset image partially updated on bad payloads.
  Validate `data` first, then mutate image state.
- style_presets create/update: json.JSONDecodeError was not caught alongside
  pydantic.ValidationError; malformed JSON surfaced as 500. Catch both → 400.
- download_queue download(): `Path(dest)` with no validation. Reject absolute
  paths, '..' segments, and empty strings with 400 before the service is called.

Add shared multiuser test fixtures in tests/app/routers/conftest.py and new
route-level tests for download_queue, style_presets, model_relationships,
utilities, and virtual_boards covering anonymous-rejection, role gating, and
the four bug regressions.

Enable --cov=invokeai in pytest addopts so the existing fail_under = 85
threshold is actually enforced.

* Ruff

* chore: openapi.json

* harden(api): enforce auth on 4 routers, fix 4 router bugs, scope style presets per-user, add route tests

Add auth dependencies to download_queue, style_presets, model_relationships,
and utilities routers, which previously had none — under multiuser=True these
endpoints were anonymously reachable. CurrentUserOrDefault for read/per-resource
routes; AdminUserOrDefault for global actions (prune/cancel-all downloads,
style preset import/export, model relationship add/remove). Single-user mode
behavior is unchanged.

Fix four bugs in the same routers:
- model_relationships: HTTPException(400) from the self-relationship check was
  caught by `except Exception` and re-raised as 500. Move the check before the
  try-block and drop the broad except so the 400 reaches the client.
- style_presets update_style_preset: image was persisted before json.loads
  could fail, leaving the preset image partially updated on bad payloads.
  Validate `data` first, then mutate image state.
- style_presets create/update: json.JSONDecodeError was not caught alongside
  pydantic.ValidationError; malformed JSON surfaced as 500. Catch both -> 400.
- download_queue download(): `Path(dest)` with no validation. Reject absolute
  paths, '..' segments, and empty strings with 400 before the service is called.

Scope style presets per-user. Migration 27 added `user_id` and `is_public`
columns months ago, but the service layer never read or wrote them — every
authenticated user saw and could mutate every preset. This commit wires them up:

- StylePresetRecordDTO gains `user_id`; StylePresetWithoutId / StylePresetChanges
  / StylePresetFormData gain `is_public`.
- SqliteStylePresetRecordsStorage.create / create_many / _sync_default_style_presets
  write user_id (system for defaults). get_many takes user_id + is_admin and
  filters SQL to: admin sees all, otherwise own ∪ default ∪ public.
- style_presets router gains _assert_preset_read / _assert_preset_write helpers
  mirroring _assert_image_owner in routers/images.py. Get/update/delete/image
  load the record first, then enforce: 403 for non-owner on private presets,
  403 for non-admin attempting to mutate a Default preset, 403 for non-admin
  trying to create a Default preset.

Add shared multiuser test fixtures in tests/app/routers/conftest.py: enable_multiuser
patches ApiDependencies across the relevant router modules and swaps None-valued
services for MagicMock, plus wires a real SqliteStylePresetRecordsStorage on the
in-memory DB so cross-user SQL filtering is actually exercised. Add route-level
tests for download_queue, style_presets, model_relationships, utilities, and
virtual_boards covering anonymous-rejection, role gating, the four bug regressions,
and the full ownership matrix for style presets (own/public/default/admin) on
get, list, update, delete, image fetch, and is_public flip.

Rename tests/app/routers/test_download_queue.py to test_download_queue_router.py
to avoid a pytest collection collision with the long-standing
tests/app/services/download/test_download_queue.py.

Enable --cov=invokeai in pytest addopts so the existing fail_under = 85
threshold is actually enforced.

* chore(pytest): drop --cov=invokeai from this PR

Reviewer pointed out that current full-suite coverage is ~48% while
fail_under is set to 85, so activating --cov=invokeai on this PR would
trip CI the moment it lands.

Tests added in this PR remain (they cover the bug regressions and auth
gating). Coverage-gate activation will be bundled into a follow-up PR
together with whatever additional tests are needed to clear the
threshold.

* refactor(api): share access helpers, tighten conftest, polish review nits

Address remaining feedback from the code review (issues 9200#issuecomment-4480805221):

- Extract _assert_image_owner / _assert_image_read_access / _assert_board_read_access
  from routers/images.py into a new routers/_access.py module (functions exported
  without the underscore prefix). routers/images.py keeps local _assert_* aliases
  so its 13 internal call sites stay untouched. routers/utilities.py imports the
  helper at module top instead of reaching into routers/images.py privates from
  inside image_to_prompt.
- Reorder current_user to come before the body parameter in expand_prompt and
  image_to_prompt, matching the convention used by the rest of the routers in
  this PR and by routers/images.py.
- Simplify _validate_dest: '..' in posix.parts or '..' in windows.parts is more
  direct than the set-union allocation.
- Harden tests/app/routers/conftest.py: pull the eight monkeypatched module paths
  into a _PATCHED_API_DEPENDENCIES_MODULES tuple so adding a new router is one
  list entry instead of one more setattr call. Add two WARNING blocks to the
  module docstring — one calling out the unconditional MagicMock replacements
  (style_preset_records is the one real-SQLite exception), and one reminding
  future authors that new routers must be appended to the tuple.
- test_multiuser_authorization.py: its local enable_multiuser fixture now also
  patches routers/_access.ApiDependencies, since the image read-access check
  used by its image-mutation tests now resolves names in _access's globals.

* chore: openapi.json and typegen

* Chore Ruff

---------

Co-authored-by: Jonathan <34005131+JPPhoto@users.noreply.github.com>
Co-authored-by: JPPhoto <jpollack@jpollackphoto.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
2026-05-21 21:30:44 -04:00
..