Add board visibility (private/shared/public) for multiuser mode#122
Add board visibility (private/shared/public) for multiuser mode#122Copilot wants to merge 59 commits intolstein/feature/workflow-isolation-in-multiuser-modefrom
Conversation
|
@copilot I'm getting the following eslint errors: |
... Fixed in 0d7f7ea. Replaced the three inline arrow functions with dedicated |
lstein
left a comment
There was a problem hiding this comment.
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.
- 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.
- 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.
- 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.
- 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.
Addressed in a0b90b1. Here's what was changed:
New helpers: |
lstein
left a comment
There was a problem hiding this comment.
There are still some issues:
- 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.
- 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.
- 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).
- 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.
Fixed in ae9cca7. Here's what was addressed:
|
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>
1fd217f to
9f8f7a1
Compare
…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
cbc03f4 to
b86e289
Compare
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>
…to copilot/enhancement-allow-shared-boards
… 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>
…oRA, GGUF, quantization, and UI (invoke-ai#9000)
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>
6baa510 to
58cb8aa
Compare
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>
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
BoardVisibilityenum (private|shared|public) added toboard_records_common.py;board_visibilityfield added toBoardRecordandBoardChangesboard_visibility TEXT NOT NULL DEFAULT 'private'column toboardstable; migrates existingis_public=1rows to'public'board_records_sqlite.py:update()handlesboard_visibility;get_many()/get_all()queries useboard_visibility IN ('shared', 'public')instead ofis_public = 1boards.pyrouter:get_boardandlist_all_board_image_namesallow non-owner access for shared/public boards;update_boardanddelete_boardremain owner/admin-onlyFrontend
schema.ts:BoardVisibilityenum,board_visibilityfield onBoardDTOandBoardChangesBoardContextMenu.tsx: "Set Private / Set Shared / Set Public" menu items (owner and admins only); visibility handlers extracted into nameduseCallbackhooks to comply withreact/jsx-no-bind; Delete Board, Archive, and Unarchive menu items are disabled (greyed out) for non-owners of shared and public boardsBoardEditableTitle.tsx: pencil icon and double-click rename hidden for non-owners of shared and public boardsGalleryBoard.tsx: blue share icon badge for shared boards, green globe badge for public boards; DnD drop target disabled for non-owners of shared boardsGalleryImage.tsx: drag-out disabled for non-owners viewing a shared board, preventing images from being moved outGalleryItemDeleteIconButton.tsx: shift+hover trash icon hidden when viewing a shared board as a non-ownerContextMenuItemDeleteImage.tsx: delete image menu item hidden when viewing a shared board as a non-ownerContextMenuItemChangeBoard.tsx: "Change Board" menu item disabled when viewing a shared board as a non-ownerMultipleSelectionMenuItems.tsx: "Change Board" and "Delete Selection" disabled when viewing a shared board as a non-ownerInvokeQueueBackButton.tsx: main Invoke/generate button disabled when the auto-add board is a shared board the current user does not ownFloatingLeftPanelButtons.tsx: floating invoke icon button also disabled when the auto-add board is a shared board the current user does not ownChangeBoardModal.tsx: destination board list filtered to exclude shared boards the current user does not own, preventing moves into read-only boardsuseBoardAccess(board)returns{ canWriteImages, canRenameBoard, canDeleteBoard };useSelectedBoard()anduseAutoAddBoard()look up the relevantBoardDTOfrom the RTK Query cacheen.json: i18n strings for all new visibility UITests
10 new tests in
test_boards_multiuser.pycovering 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
multiuser: truein config)Merge Plan
Migration 29 adds a new column with a safe default (
'private'), so existing databases upgrade non-destructively. No redux slice changes.Checklist
What's Newcopy (if doing a release after this PR)Original prompt
💡 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.