Skip to content

Add board visibility (private/shared/public) for multiuser mode#122

Open
Copilot wants to merge 59 commits intolstein/feature/workflow-isolation-in-multiuser-modefrom
copilot/enhancement-allow-shared-boards
Open

Add board visibility (private/shared/public) for multiuser mode#122
Copilot wants to merge 59 commits intolstein/feature/workflow-isolation-in-multiuser-modefrom
copilot/enhancement-allow-shared-boards

Conversation

Copy link
Copy Markdown

Copilot AI commented Mar 9, 2026

Adds three visibility levels for boards in multiuser mode: private (owner/admin only), shared (all users can view, owner/admin can modify), and public (all users can view and write; only owner/admin can rename or delete).

Summary

Backend

  • BoardVisibility enum (private | shared | public) added to board_records_common.py; board_visibility field added to BoardRecord and BoardChanges
  • Migration 29: adds board_visibility TEXT NOT NULL DEFAULT 'private' column to boards table; migrates existing is_public=1 rows to 'public'
  • board_records_sqlite.py: update() handles board_visibility; get_many()/get_all() queries use board_visibility IN ('shared', 'public') instead of is_public = 1
  • boards.py router: get_board and list_all_board_image_names allow non-owner access for shared/public boards; update_board and delete_board remain owner/admin-only

Frontend

  • schema.ts: BoardVisibility enum, board_visibility field on BoardDTO and BoardChanges
  • BoardContextMenu.tsx: "Set Private / Set Shared / Set Public" menu items (owner and admins only); visibility handlers extracted into named useCallback hooks to comply with react/jsx-no-bind; Delete Board, Archive, and Unarchive menu items are disabled (greyed out) for non-owners of shared and public boards
  • BoardEditableTitle.tsx: pencil icon and double-click rename hidden for non-owners of shared and public boards
  • GalleryBoard.tsx: blue share icon badge for shared boards, green globe badge for public boards; DnD drop target disabled for non-owners of shared boards
  • GalleryImage.tsx: drag-out disabled for non-owners viewing a shared board, preventing images from being moved out
  • GalleryItemDeleteIconButton.tsx: shift+hover trash icon hidden when viewing a shared board as a non-owner
  • ContextMenuItemDeleteImage.tsx: delete image menu item hidden when viewing a shared board as a non-owner
  • ContextMenuItemChangeBoard.tsx: "Change Board" menu item disabled when viewing a shared board as a non-owner
  • MultipleSelectionMenuItems.tsx: "Change Board" and "Delete Selection" disabled when viewing a shared board as a non-owner
  • InvokeQueueBackButton.tsx: main Invoke/generate button disabled when the auto-add board is a shared board the current user does not own
  • FloatingLeftPanelButtons.tsx: floating invoke icon button also disabled when the auto-add board is a shared board the current user does not own
  • ChangeBoardModal.tsx: destination board list filtered to exclude shared boards the current user does not own, preventing moves into read-only boards
  • New hooksuseBoardAccess(board) returns { canWriteImages, canRenameBoard, canDeleteBoard }; useSelectedBoard() and useAutoAddBoard() look up the relevant BoardDTO from the RTK Query cache
  • en.json: i18n strings for all new visibility UI

Tests

10 new tests in test_boards_multiuser.py covering default visibility, setting each level, cross-user access enforcement, reversion to private, non-owner restriction, and admin override. All 33 tests pass.

Related Issues / Discussions

QA Instructions

  1. Enable multiuser mode (multiuser: true in config)
  2. Create a board as User A — verify it defaults to private (User B cannot see it)
  3. Right-click board → Set Shared — verify User B can now view it but:
    • The pencil icon is hidden and double-clicking the name does nothing
    • The Delete Board, Archive, and Unarchive options in the context menu are greyed out
    • The trash icon on images and "Delete Image" context menu item are hidden
    • Both the main Invoke button and the floating invoke icon button are disabled if this shared board is the auto-add target
    • Dragging images onto the board is disabled
    • Dragging images out of the board is disabled
    • The "Change Board" destination list does not include this shared board
  4. Set to Public — verify User B can view and write images (including generating into it), but cannot rename, delete, or archive the board
  5. Set back to Private — verify User B loses access again
  6. Verify shared/public boards show the appropriate icon badge in the boards list
  7. Verify admins retain full access to all boards regardless of visibility

Merge Plan

Migration 29 adds a new column with a safe default ('private'), so existing databases upgrade non-destructively. No redux slice changes.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)
Original prompt

This section details on the original issue you should resolve

<issue_title>[enhancement]: Allow shared boards in multiuser mode</issue_title>
<issue_description>### Is there an existing issue for this?

  • I have searched the existing issues

Contact Details

No response

What should this feature add?

Currently when multiuser mode is active, all user-created boards are strictly private. They are entirely isolated from each other.

I would like it to be possible to designate a board as one of "Private," "Shared," or "Public".

  • Private board -- Only visible to the user who created it.
  • Shared board -- Visible to all users. Read/Write for the user who created it (they can add images to it and delete images from it). Read-only for other users.
  • Public board -- Visible and Read/Write to all users.

The Administrator should continue to be able to see all boards created by all users and have read/write access to them.

There is code in the lstein/feature/workflow-isolation-in-multiuser-mode that enables sharing of workflows. I have designated this branch as the base for this pull request.

In terms of user interface, try adding a discrete pulldown menu to the right of the board's name can be used to choose among the three board types. Something small like a caret (^) mark, and badges that appear when the board is shared or public.

Alternatives

No response

Additional Content

No response</issue_description>

Comments on the Issue (you are @copilot in this section)


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Add support for shared boards in multiuser mode Add board visibility (private/shared/public) for multiuser mode Mar 9, 2026
@lstein
Copy link
Copy Markdown
Owner

lstein commented Mar 9, 2026

@copilot I'm getting the following eslint errors:

/home/lstein/Projects/InvokeAI-lstein/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx
  130:17  error  JSX props should not use arrow functions  react/jsx-no-bind
  137:17  error  JSX props should not use arrow functions  react/jsx-no-bind
  144:17  error  JSX props should not use arrow functions  react/jsx-no-bind

Copy link
Copy Markdown
Author

Copilot AI commented Mar 9, 2026

@copilot I'm getting the following eslint errors:

/home/lstein/Projects/InvokeAI-lstein/invokeai/frontend/web/src/features/gallery/components/Boards/BoardContextMenu.tsx

...

Fixed in 0d7f7ea. Replaced the three inline arrow functions with dedicated useCallback handlers (handleSetVisibilityPrivate, handleSetVisibilityShared, handleSetVisibilityPublic) that each delegate to the shared handleSetVisibility callback.

@lstein lstein marked this pull request as ready for review March 9, 2026 22:24
@lstein lstein self-requested a review as a code owner March 9, 2026 22:24
Copy link
Copy Markdown
Owner

@lstein lstein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few glitches to correct. In these examples, "Board A Public" and "Board A Shared" are owned by user A and are public and shared respectively. User B is a non-administrative user.

  1. Shared boards are read-only for non-owners. So when B is logged in he should not be able to generate images and store them in Board A Shared, to move images in and out of Board A Shared, or delete images from Board A Shared. As much as possible, user interfaces that attempt to modify the board should be disabled, so that if Board A Shared is the auto save target for image generation, the generation buttons should be disabled.
  2. Disable the option to delete a public or shared board when a non-owner tries to do so. So when B is logged in and right clicks on Board A Shared or Board A Public, the Delete menu option is greyed out.
  3. Similarly, the trash-can icon to delete individual image files, and the delete image menu item should be deleted when B is logged in and working with Board A Shared.
  4. The pencil icon should be hidden from Board A Shared and Board A Public when B is logged in. Only the owner (or the administrator) should be able to edit the name of a shared or public board.

Copy link
Copy Markdown
Author

Copilot AI commented Mar 9, 2026

A few glitches to correct. In these examples, "Board A Public" and "Board A Shared" are owned by user A and are public and shared respectively. User B is a non-administrative user.

  1. Shared boards a...

Addressed in a0b90b1. Here's what was changed:

  1. Shared board read-only: GalleryItemDeleteIconButton (shift+hover trash icon) and ContextMenuItemDeleteImage hide when a non-owner views a shared board. ContextMenuItemChangeBoard and multi-select "Delete Selection"/"Change Board" are disabled too. The DnD drop target on shared boards is disabled for non-owners, preventing drag-in. The Invoke button is disabled when the auto-add board is shared and the user is not the owner.

  2. Delete board greyed out: BoardContextMenu now sets isDisabled={!canDeleteBoard} on the Delete Board menu item — greys it out for non-owners of both shared and public boards.

  3. Trash icon / delete image hidden for shared: Items 6–9 above all use the new useSelectedBoard()useBoardAccess() chain. When the selected board is shared and the current user is not the owner, canWriteImages is false and the delete controls are hidden/disabled.

  4. Pencil icon hidden for shared and public: BoardEditableTitle uses useBoardAccess(board).canRenameBoard; when false (non-owner of shared or public board), the pencil icon is not rendered and double-clicking the title has no effect.

New helpers: useBoardAccess(board) returns { canWriteImages, canRenameBoard, canDeleteBoard }. Logic: owner or admin → full access; public board non-owner → can write images but not rename/delete; shared board non-owner → read-only.

@lstein lstein self-requested a review March 10, 2026 02:19
Copy link
Copy Markdown
Owner

@lstein lstein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are still some issues:

  1. When a read-only shared board is selected, and I am not its owner, the "Add to Queue" button is active and lets me generate into the shared board. This button should be disabled when the target is a shared board and I am not the owner of the board or the administrator.
  2. I am able to drag an image out of a shared board and into a private board, thereby deleting it from the shared board, even when I am not the owner of the shared board. Dragging from a shared board when I am not the owner (or administrator) should be disabled.
  3. I can right click on an image in a private or public board and select "Change Board" to move it into a shared board, even when I am not the owner of the shared board. The drop target for a shared board should be disabled unless I am the owner (or administrator).
  4. I can archive a public or shared board, even when I am not the owner of that board. If I am not the owner, the archive menu selection should be disabled.

Copy link
Copy Markdown
Author

Copilot AI commented Mar 10, 2026

There are still some issues:

  1. When a read-only shared board is selected, and I am not its owner, the "Add to Queue" button is active and lets me generate into the shared board. This button should b...

Fixed in ae9cca7. Here's what was addressed:

  1. "Add to Queue" icon button still active — The floating InvokeIconButton in FloatingLeftPanelButtons.tsx was a second invoke entry point that wasn't covered. Now it also checks useBoardAccess(autoAddBoard).canWriteImages and is disabled for non-owners of shared boards.

  2. Dragging images out of shared boardGalleryImage.tsx now reads useSelectedBoard() + useBoardAccess() and conditionally registers the draggable() binding. When canDragFromBoard is false (non-owner viewing a shared board), the draggable is not attached, so the image cannot be picked up. The monitorForElements monitor is always registered so multi-image drag indicators still work.

  3. "Change Board" destination includes shared boardsChangeBoardModal.tsx now applies a canWriteToBoard filter to the options list. Boards where the current user is not the owner/admin and which are shared are excluded. Public boards and the user's own boards remain available.

  4. Archive/Unarchive accessible to non-owners — Both the Archive and Unarchive menu items in BoardContextMenu.tsx now carry isDisabled={!canDeleteBoard}, so non-owners of shared and public boards see them greyed out.

Copilot AI and others added 3 commits March 9, 2026 22:56
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
…-board filter, archive

Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
@lstein lstein force-pushed the copilot/enhancement-allow-shared-boards branch from 1fd217f to 9f8f7a1 Compare March 10, 2026 02:56
…rameters

 - Sanitize session queue information of all cross-user fields except for the timestamps and status.
 - Recall parameters are now user-scoped.
 - Queue status endpoints now report user-scoped activity rather than global activity
 - Tests added:

  TestSessionQueueSanitization (4 tests):
  1. test_owner_sees_all_fields - Owner sees complete queue item data
  2. test_admin_sees_all_fields - Admin sees complete queue item data
  3. test_non_owner_sees_only_status_timestamps_errors -
     Non-owner sees only item_id, queue_id, status, and timestamps; everything else is redacted
  4. test_sanitization_does_not_mutate_original - Sanitization doesn't modify the original object

  TestRecallParametersIsolation (2 tests):

  5. test_user1_write_does_not_leak_to_user2 - User1's recall params are not visible in user2's client state
  6. test_two_users_independent_state - Both users can write recall params independently without overwriting each other

fix(backend): queue status endpoints report user-scoped stats rather than global stats
@lstein lstein force-pushed the copilot/enhancement-allow-shared-boards branch from cbc03f4 to b86e289 Compare April 10, 2026 01:26
lstein and others added 16 commits April 9, 2026 21:31
  Problem: When categories=['user', 'default'] (or no category filter)
  and user_id was set for multiuser scoping, the SQL query became
     WHERE category IN ('user', 'default') AND user_id = ?,
     which  excluded default workflows (owned by "system").

  Fix: Changed user_id = ? to (user_id = ? OR category = 'default') in
  all 6 occurrences across workflow_records_sqlite.py — in get_many,
  counts_by_category, counts_by_tag, and get_all_tags. Default
  workflows are now always visible regardless of user scoping.

  Tests added (2):
  - test_default_workflows_visible_when_listing_user_and_default — categories=['user','default'] includes both
  - test_default_workflows_visible_when_no_category_filter — no filter still shows defaults
* test: add if-node execution coverage

* feat: short-circuit if-node branch execution

* test: cover iterated if-node pruning

* style: apply ruff fixes for if-node work

* refactor: track prepared exec node metadata

* fix: defer iterated if branches until resolution

* refactor: extract prepared exec registry

* refactor: extract if branch scheduler

* refactor: extract execution materializer

* refactor: extract execution scheduler

* refactor: extract execution runtime

* refactor: clarify if branch resolution

* refactor: clarify execution materialization

* docs: describe graph execution helpers

* refactor: clarify execution runtime

* refactor: clarify execution scheduling

* refactor: clarify iteration node selection

* docs: describe execution materializer flow

* refactor: clarify collector validation

* refactor: clarify iterator validation

* refactor: clarify graph validation flow

* docs: update shared graph design overview

* chore: typegen

* fix: harden if-node scheduler edge cases
… keys (invoke-ai#9013)

* fix(ui): replace all hardcoded frontend strings with i18n translation keys

Remove fallback/defaultValue strings from t() calls, replace hardcoded
English text in labels, tooltips, aria-labels, placeholders and JSX content
with proper t() calls, and add ~50 missing keys to en.json. Fix incorrect
i18n key paths in CanvasObjectImage.ts and a Zoom button aria-label bug
in CanvasToolbarScale.tsx.

* chore pnpm run fix

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
…invoke-ai#9037)

openapi-typescript computes enum types from `const` usage in
discriminated unions rather than from the enum definition itself,
dropping values that only appear in some union members (e.g. "anima"
from BaseModelType). Add a post-processing step that patches generated
string enum types to match the actual OpenAPI schema definitions.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Eichhorn <alex@eichhorn.dev>
* feat(model manager): redesign queue

* feat(model manager queue): improve ui/ux

- standardized table row widths
- sticky table header
- reverse table data direction (new items on top)
- queue empty state
- ui and icon tweaks
- add progress tooltip
- add code comments for sanity

* fix(model manager queue): add missing imports

dammit zed editor

* fix(model manager queue): play/pause button condition

* feat(model manager queue): remove backend status badge

* fix(model manager queue): remove unused useStore import

* fix(model manager queue): prettier lint

* feat(model meneger queue): backend disconnected visual feedback

* fix(model manager queue): qol list item ui tweaks

* feat(model manager queue): reorganize bulk actions

* feat(model manager queue): tweak column widths

* feat(model manager queue): disable actions dropdown if items disabled

* feat(model manager queue): optimistic updated and code qulity

- Treated downloads_done as an active install phase for row UI and bulk cancel.
- Stopped stale error text from overriding the badge after resume/restart by only showing the error label when
  the displayed status is actually error.
- Added row-level action locking to block duplicate pause/resume/cancel/restart submissions.
- Added optimistic row status handling so the UI does not briefly fall back to stale error/restart state
  before RTK Query/socket updates arrive.
- Fixed local-path basename parsing for both the main row title and restart-required file rows.
- Added an accessible aria-label to the overflow menu button.

* style(model manager queue): fix prettier lint

* feat(model manager queue): keep prune action visible

* feat(model manager queue): prune button ui tweak

---------

Co-authored-by: joshistoast <me@joshcorbett.com>
Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
* feat: add configurable shift parameter for Z-Image sigma schedule

Add a shift (mu) override to the Z-Image denoise invocation and expose
it in the UI. When left blank, shift is auto-calculated from image
dimensions (existing behavior). Users can override to fine-tune the
timestep schedule, with an inline X button to reset back to auto.

* refactor: switch Z-Image sigma schedule from exponential to linear time shift

Use shift directly as a linear multiplier instead of exp(mu), giving
more predictable and uniform control over the timestep schedule.
Auto-calculated values are converted via exp(mu) to preserve identical
default behavior.

* feat: recall Z-Image shift parameter from metadata

Write z_image_shift into graph metadata and add a ZImageShift recall
handler so the shift override can be restored from previously generated
images. Auto-mode (null) is omitted from metadata to avoid persisting a
stale value.

---------

Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
… user

Several read-only and event-emitting endpoints were leaking aggregate
cross-user activity in multiuser mode:

- recall_parameters_updated event was broadcast to every queue
  subscriber. Added user_id to the event and routed it to the owner +
  admin rooms only.
- get_queue_status, get_batch_status, counts_by_destination and
  get_intermediates_count now scope counts to the calling user
  (admins still see global state). Removed the now-redundant
  user_pending/user_in_progress fields and simplified QueueCountBadge.
- get_queue_status hides current item_id/session_id/batch_id when the
  current item belongs to another user.

Also fixes test_session_queue_sanitization assertions that lagged
behind the recently expanded redaction set.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Close three cross-user leaks in the websocket layer:

- _handle_connect() now rejects connections without a valid JWT in
  multiuser mode (previously fell through to user_id="system"), so
  anonymous clients can no longer subscribe to queue rooms and observe
  other users' activity. In single-user mode it still accepts as system
  admin.
- _handle_sub_queue() no longer silently falls back to the system user
  for an unknown sid in multiuser mode; it refuses the subscription.
- QueueItemStatusChangedEvent and BatchEnqueuedEvent are now routed to
  user:{user_id} + admin rooms instead of the full queue room. Both
  events carry unsanitized user_id, batch_id, origin, destination,
  session_id, and error metadata and must not be broadcast.
- BatchEnqueuedEvent gains a user_id field; emit_batch_enqueued and
  enqueue_batch thread it through.

New TestWebSocketAuth suite covers connect accept/reject for both
modes, sub_queue refusal, and private routing of the queue item and
batch events (plus a QueueClearedEvent sanity check).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
A deleted or deactivated user with an unexpired JWT could still open a
websocket and subscribe to queue rooms. Now _handle_connect() checks the
backing user record (exists + is_active) in multiuser mode, mirroring
the REST auth path in auth_dependencies.py. Fails closed if the user
service is unavailable.

Tests: added deleted-user and inactive-user rejection tests; updated
valid-token test to create the user in the database first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lstein and others added 8 commits April 12, 2026 14:06
Backend:
- POST /download now validates image read access (per-image) and board
  read access (per-board) before queuing the download.
- GET /download/{name} is intentionally unauthenticated because the
  browser triggers it via <a download> which cannot carry Authorization
  headers. Access control relies on POST-time checks, UUID filename
  unguessability, private socket event routing, and single-fetch deletion.
- Added _assert_board_read_access() helper to images router.
- Threaded user_id through bulk download handler, base class, event
  emission, and BulkDownloadEventBase so events carry the initiator.
- Bulk download service now tracks download ownership via _download_owners
  dict (cleaned up on delete).
- Socket bulk_download room subscription restricted to authenticated
  sockets in multiuser mode.
- Added error-catching in FastAPIEventService._dispatch_from_queue to
  prevent silent event dispatch failures.

Frontend:
- Fixed pre-existing race condition where the "Preparing Download" toast
  from the POST response overwrote the "Ready to Download" toast from the
  socket event (background task completes in ~17ms, so the socket event
  can arrive before Redux processes the HTTP response). Toast IDs are now
  distinct: "preparing:{name}" vs "{name}".
- bulk_download_complete/error handlers now dismiss the preparing toast.

Tests (8 new):
- Bulk download by image names rejected for non-owner (403)
- Bulk download by image names allowed for owner (202)
- Bulk download from private board rejected (403)
- Bulk download from shared board allowed (202)
- Admin can bulk download any images (202)
- Bulk download events carry user_id
- Bulk download event emitted to download room
- GET /download unauthenticated returns 404 for unknown files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GET /api/v1/images?board_id=... and GET /api/v1/images/names?board_id=...
passed board_id directly to the SQL layer without checking board
visibility. The SQL only applied user_id filtering for board_id="none"
(uncategorized images), so any authenticated user who knew a private
board ID could enumerate its images.

Both endpoints now call _assert_board_read_access() before querying,
returning 403 unless the caller is the board owner, an admin, or the
board is Shared/Public.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
add_image_to_board and add_images_to_board only checked write access to
the destination board, never verifying that the caller owned the source
image.  An attacker could add a victim's image to their own board, then
exploit the board-ownership fallback in _assert_image_owner to gain
delete/patch/star/unstar rights on the image.

Both endpoints now call _assert_image_direct_owner which requires direct
image ownership (image_records.user_id) or admin — board ownership is
intentionally not sufficient, preventing the escalation chain.

Also fixed a pre-existing bug where HTTPException from the inner loop in
add_images_to_board was caught by the outer except-Exception and returned
as 500 instead of propagating the correct status code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The recall endpoint loaded image files and ran ControlNet preprocessors
on any image_name supplied in control_layers or ip_adapters without
checking that the caller could read the image.  An attacker who knew
another user's image UUID could extract dimensions and, for supported
preprocessors, mint a derived processed image they could then fetch.

Added _assert_recall_image_access() which validates read access for every
image referenced in the request before any resolution or processing
occurs.  Access is granted to the image owner, admins, or when the image
sits on a Shared/Public board.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
list_model_installs, get_model_install_job, pause, resume,
restart_failed, and restart_file were unauthenticated — any caller who
could reach the API could view sensitive install job fields (source,
local_path, error_traceback) and interfere with installation state.

All six endpoints now require AdminUserOrDefault, consistent with the
neighboring cancel and prune routes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…w findings

Bulk download capability token exfiltration:
- Socket events now route to user:{user_id} + admin rooms instead of the
  shared 'default' room (the earlier toast race that blocked this approach
  was fixed in a prior commit).
- GET /download/{name} re-requires CurrentUserOrDefault and enforces
  ownership via get_owner().
- Frontend download handler replaced <a download> (which cannot carry auth
  headers) with fetch() + Authorization header + programmatic blob download.

Additional fixes from reviewer tests:
- Public boards now grant write access in _assert_board_write_access and
  mutation rights in _assert_image_owner (BoardVisibility.Public).
- Uncategorized image listing (GET /boards/none/image_names) now filters
  to the caller's images only, preventing cross-user enumeration.
- board_images router uses board_image_records.get_board_for_image()
  instead of images.get_dto() to avoid dependency on image_files service.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lstein lstein force-pushed the copilot/enhancement-allow-shared-boards branch from 6baa510 to 58cb8aa Compare April 12, 2026 22:59
lstein and others added 2 commits April 12, 2026 19:22
Defense-in-depth: the route layer already checks ownership before
calling update/delete/update_is_public/update_opened_at, but the SQL
statements did not include AND user_id = ?, so a bypass of the route
check would allow cross-user mutations.

All four methods now accept an optional user_id parameter.  When
provided, the SQL WHERE clause is scoped to that user.  The route layer
passes current_user.user_id for non-admin callers and None for admins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
upload_image() blocked non-owner uploads even to public boards.  The
board write check now allows uploads when board_visibility is Public,
consistent with the public-board semantics in _assert_board_write_access
and _assert_image_owner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants