Feature: Shared/private workflows and image boards in multiuser mode#9018
Feature: Shared/private workflows and image boards in multiuser mode#9018lstein wants to merge 28 commits intoinvoke-ai:mainfrom
Conversation
* Add per-user workflow isolation: migration 28, service updates, router ownership checks, is_public endpoint, schema regeneration, frontend UI Co-authored-by: lstein <111189+lstein@users.noreply.github.com> * feat: add shared workflow checkbox to Details panel, auto-tag, gate edit/delete, fix tests Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
…mode (#116) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
…ature/workflow-isolation-in-multiuser-mode
… mode (#120) * Disable Save when editing another user's shared workflow in multiuser mode Co-authored-by: lstein <111189+lstein@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: lstein <111189+lstein@users.noreply.github.com>
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>
…image, etc.) Previously, images in shared boards owned by another user could not be dragged at all — the draggable setup was completely skipped in GalleryImage.tsx when canWriteImages was false. This blocked ALL drop targets including the viewer, reference image pane, and canvas. Now images are always draggable. The board-move restriction is enforced in the dnd target isValid functions instead: - addImageToBoardDndTarget: rejects moves from shared boards the user doesn't own (unless admin or board is public) - removeImageFromBoardDndTarget: same check Other drop targets (viewer, reference images, canvas, comparison, etc.) remain fully functional for shared board images. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…to copilot/enhancement-allow-shared-boards
JPPhoto
left a comment
There was a problem hiding this comment.
Some issues:
-
Shared-board write protection is still bypassable through direct API calls. The PR only adds read-side visibility checks in boards.py, but the actual mutation routes in board_images.py and images.py still have no
CurrentUserOrDefaultdependency and no owner/visibility checks. In practice, another user can list image names on a shared board via the new allowedGET /boards/{id}/image_names, then callDELETE /images/i/{image_name}orPOST /board_images/to delete/move images anyway. That defeats the PR's core "shared = read-only" guarantee. This needs API-level authorization tests around board-image mutation and image deletion/update, not just UI gating. -
Private workflow thumbnails are still exposed despite the PR summary claiming thumbnail endpoint ownership enforcement. The write endpoints are guarded in the PR, but
GET /api/v1/workflows/i/{workflow_id}/thumbnailin workflows.py still has no user context and no ownership/public check. If a workflow id is known, a private workflow's thumbnail remains fetchable even whenGET /workflows/i/{workflow_id}is now correctly blocked. -
The new
admin_emailfield on unauthenticated setup status leaks administrator identity. The PR extendsGET /api/v1/auth/statusin auth.py to return the first active admin's email, and that route still has no auth requirement. On a public multiuser deployment, this becomes a trivial admin-enumeration/phishing aid. If the frontend needs this for onboarding copy, it should be limited to thesetup_required=truepath or otherwise gated/sanitized.
|
Some issues still, maybe they're not a big deal but maybe they are:
|
…-allow-shared-boards' into copilot/enhancement-allow-shared-boards
|
Apparently I didn't push my commit last night. Sorry for the trouble. |
|
I left comments on Discord but I'll also put them here:
IMO, testing needs to also expand for cross-user isolation - maybe this can be done via parameterization even so your testing code changes are minimal. |
|
Another issue: List scoping appears to be filtering out default workflows in multiuser mode. In The solution looks fairly straightforward:
|
Stacked on top of origin PR invoke-ai#9018 (shared/private workflows and boards) for multiuser workflow visibility semantics.
Stacked on top of origin PR invoke-ai#9018 (shared/private workflows and boards) for multiuser workflow visibility semantics.
…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
|
Recall parameters are now stored per-user, but the update event is still broadcast only by Several read-only queue/image status endpoints still leak aggregate cross-user activity because they authenticate the caller but do not scope the returned data to that caller. All of these cases need tests first, then fixes. |
Summary
This PR gives users the ability to share workflows and image boards when logged in to a multiuser instance.
Workflow Sharing
User can share workflows with each other as well as create private workflows that are invisible to other users. Each user has their own workflow library that is only accessible to them. Any workflow can be designated Shared, in which case it is readable by all users but only writable by the owner.
Backend
user_id(DEFAULT'system') andis_public(DEFAULTFALSE) columns + indexes toworkflow_library. Usesexecutescript()for the DDL statements so that transaction management is handled explicitly (executescript always issues COMMIT first), which avoids edge-cases in Python'ssqlite3implicit transaction handling for DDL on tables that contain VIRTUAL generated columns. A post-check raises a clearRuntimeErrorif the columns were not actually added, preventing silent failures. No cross-module imports in the migration callback.WORKFLOW_LIBRARY_DEFAULT_USER_IDconstant added toworkflow_records_common.pyto avoid magic strings across service and base layersworkflow_records_*: All query methods (create,get_many,counts_by_*,get_all_tags) acceptuser_idandis_publicfilters; newupdate_is_public()method — which also automatically adds the"shared"tag whenis_public=trueand removes it whenis_public=falseworkflows.pyrouter:CurrentUserOrDefaultlist_workflows/counts_by_*/get_all_tagsautomatically scopeusercategory results to the current user in multiuser mode (bypassed whenis_public=trueis explicitly requested)GET(non-owner blocked unless public/default/admin),PATCH,DELETE, and thumbnail endpointsPATCH /api/v1/workflows/i/{workflow_id}/is_publicendpointFrontend
openapi.jsonupdated;schema.tsregenerated viamake frontend-typegenWorkflowRecordOrderBygainsis_public;WorkflowLibraryViewgains'shared'WorkflowLibrarySideNav;WorkflowListroutes'shared'view tois_public=truequeryWorkflowGeneralTab(Details panel): newShareWorkflowCheckboxcomponent positioned between the workflow Name field and the Workflow Thumbnail section — visible to the workflow owner and admins in multiuser mode; label and checkbox are on the same horizontal line; togglesis_publicvia the APIWorkflowListItem: owners see aShareWorkflowToggleswitch; public workflows display a "Shared" badge;EditWorkflowandDeleteWorkflowbuttons are now gated behindisOwner || is_adminso non-owners cannot edit or delete others' workflowsSaveWorkflowAsDialog: "Share workflow" checkbox — marks new workflow public immediately after creationWorkflowSortControl+ sort options updated to includeis_publicsharedWorkflows,shareWorkflow("Shared workflow") added toen.jsonSingle-user mode behavior is completely unchanged — no workflow filtering is applied when
multiuserisfalse.QA Instructions
** Warning: ** be aware that this PR performs a database migration before testing.
Workflow Isolation
multiuser: trueand create two users: user1 and user2. If the user interface for managing users isn't yet merged in, use theinvoke-useraddCLI.user1 workflow sharedanduser1 workflow privateBoard Sharing
This PR implements image board sharing among users when multiuser mode is active. It adds three visibility levels for the boards:
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.QA Instructions
** Warning: ** be aware that this PR performs a database migration before testing.
multiuser: truein config)Additional Fixes in this PR
There are two small UI bugs that were found and fixed while working on this PR.
The splash screens that the user sees at startup time have been customized such that non-admin users are not prompted to install models. Instead they are referred to the first administrator to install models and make other global settings.
This PR also corrects a bug that allowed any user to run "sync models" and remove orphaned models. Only the administrator is allowed to do this.
QA
Splash Screens
To test, you should start with a fresh root directory that has no models.
Sync Models button
Confirm that as a non-admin user, the Models page does not show the "Sync Models" button, and that this button reappears when logged in as an administrator.
Merge Plan
Migrations 28 and 29 add new columns to the boards and workflows tables with safe defaults so existing databases upgrade non-destructively. No redux slice changes.
Checklist
What's Newcopy (if doing a release after this PR)