Skip to content

Commit 9134a38

Browse files
committed
Merge branch 'main' into queue-item-status-sequence
2 parents a510262 + 33ec16d commit 9134a38

166 files changed

Lines changed: 10302 additions & 516 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

invokeai/app/api/dependencies.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
CogView4ConditioningInfo,
5252
ConditioningFieldData,
5353
FLUXConditioningInfo,
54+
QwenImageConditioningInfo,
5455
SD3ConditioningInfo,
5556
SDXLConditioningInfo,
5657
ZImageConditioningInfo,
@@ -141,6 +142,7 @@ def initialize(
141142
SD3ConditioningInfo,
142143
CogView4ConditioningInfo,
143144
ZImageConditioningInfo,
145+
QwenImageConditioningInfo,
144146
AnimaConditioningInfo,
145147
],
146148
ephemeral=True,

invokeai/app/api/routers/auth.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ class SetupStatusResponse(BaseModel):
8080
setup_required: bool = Field(description="Whether initial setup is required")
8181
multiuser_enabled: bool = Field(description="Whether multiuser mode is enabled")
8282
strict_password_checking: bool = Field(description="Whether strict password requirements are enforced")
83+
admin_email: str | None = Field(default=None, description="Email of the first active admin user, if any")
8384

8485

8586
@auth_router.get("/status", response_model=SetupStatusResponse)
@@ -94,15 +95,25 @@ async def get_setup_status() -> SetupStatusResponse:
9495
# If multiuser is disabled, setup is never required
9596
if not config.multiuser:
9697
return SetupStatusResponse(
97-
setup_required=False, multiuser_enabled=False, strict_password_checking=config.strict_password_checking
98+
setup_required=False,
99+
multiuser_enabled=False,
100+
strict_password_checking=config.strict_password_checking,
101+
admin_email=None,
98102
)
99103

100104
# In multiuser mode, check if an admin exists
101105
user_service = ApiDependencies.invoker.services.users
102106
setup_required = not user_service.has_admin()
103107

108+
# Only expose admin_email during initial setup to avoid leaking
109+
# administrator identity on public deployments.
110+
admin_email = user_service.get_admin_email() if setup_required else None
111+
104112
return SetupStatusResponse(
105-
setup_required=setup_required, multiuser_enabled=True, strict_password_checking=config.strict_password_checking
113+
setup_required=setup_required,
114+
multiuser_enabled=True,
115+
strict_password_checking=config.strict_password_checking,
116+
admin_email=admin_email,
106117
)
107118

108119

invokeai/app/api/routers/board_images.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,53 @@
11
from fastapi import Body, HTTPException
22
from fastapi.routing import APIRouter
33

4+
from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
45
from invokeai.app.api.dependencies import ApiDependencies
56
from invokeai.app.services.images.images_common import AddImagesToBoardResult, RemoveImagesFromBoardResult
67

78
board_images_router = APIRouter(prefix="/v1/board_images", tags=["boards"])
89

910

11+
def _assert_board_write_access(board_id: str, current_user: CurrentUserOrDefault) -> None:
12+
"""Raise 403 if the current user may not mutate the given board.
13+
14+
Write access is granted when ANY of these hold:
15+
- The user is an admin.
16+
- The user owns the board.
17+
- The board visibility is Public (public boards accept contributions from any user).
18+
"""
19+
from invokeai.app.services.board_records.board_records_common import BoardVisibility
20+
21+
try:
22+
board = ApiDependencies.invoker.services.boards.get_dto(board_id=board_id)
23+
except Exception:
24+
raise HTTPException(status_code=404, detail="Board not found")
25+
if current_user.is_admin:
26+
return
27+
if board.user_id == current_user.user_id:
28+
return
29+
if board.board_visibility == BoardVisibility.Public:
30+
return
31+
raise HTTPException(status_code=403, detail="Not authorized to modify this board")
32+
33+
34+
def _assert_image_direct_owner(image_name: str, current_user: CurrentUserOrDefault) -> None:
35+
"""Raise 403 if the current user is not the direct owner of the image.
36+
37+
This is intentionally stricter than _assert_image_owner in images.py:
38+
board ownership is NOT sufficient here. Allowing a user to add someone
39+
else's image to their own board would grant them mutation rights via the
40+
board-ownership fallback in _assert_image_owner, escalating read access
41+
into write access.
42+
"""
43+
if current_user.is_admin:
44+
return
45+
owner = ApiDependencies.invoker.services.image_records.get_user_id(image_name)
46+
if owner is not None and owner == current_user.user_id:
47+
return
48+
raise HTTPException(status_code=403, detail="Not authorized to move this image")
49+
50+
1051
@board_images_router.post(
1152
"/",
1253
operation_id="add_image_to_board",
@@ -17,14 +58,17 @@
1758
response_model=AddImagesToBoardResult,
1859
)
1960
async def add_image_to_board(
61+
current_user: CurrentUserOrDefault,
2062
board_id: str = Body(description="The id of the board to add to"),
2163
image_name: str = Body(description="The name of the image to add"),
2264
) -> AddImagesToBoardResult:
2365
"""Creates a board_image"""
66+
_assert_board_write_access(board_id, current_user)
67+
_assert_image_direct_owner(image_name, current_user)
2468
try:
2569
added_images: set[str] = set()
2670
affected_boards: set[str] = set()
27-
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
71+
old_board_id = ApiDependencies.invoker.services.board_image_records.get_board_for_image(image_name) or "none"
2872
ApiDependencies.invoker.services.board_images.add_image_to_board(board_id=board_id, image_name=image_name)
2973
added_images.add(image_name)
3074
affected_boards.add(board_id)
@@ -48,13 +92,16 @@ async def add_image_to_board(
4892
response_model=RemoveImagesFromBoardResult,
4993
)
5094
async def remove_image_from_board(
95+
current_user: CurrentUserOrDefault,
5196
image_name: str = Body(description="The name of the image to remove", embed=True),
5297
) -> RemoveImagesFromBoardResult:
5398
"""Removes an image from its board, if it had one"""
5499
try:
100+
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
101+
if old_board_id != "none":
102+
_assert_board_write_access(old_board_id, current_user)
55103
removed_images: set[str] = set()
56104
affected_boards: set[str] = set()
57-
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
58105
ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
59106
removed_images.add(image_name)
60107
affected_boards.add("none")
@@ -64,6 +111,8 @@ async def remove_image_from_board(
64111
affected_boards=list(affected_boards),
65112
)
66113

114+
except HTTPException:
115+
raise
67116
except Exception:
68117
raise HTTPException(status_code=500, detail="Failed to remove image from board")
69118

@@ -78,16 +127,21 @@ async def remove_image_from_board(
78127
response_model=AddImagesToBoardResult,
79128
)
80129
async def add_images_to_board(
130+
current_user: CurrentUserOrDefault,
81131
board_id: str = Body(description="The id of the board to add to"),
82132
image_names: list[str] = Body(description="The names of the images to add", embed=True),
83133
) -> AddImagesToBoardResult:
84134
"""Adds a list of images to a board"""
135+
_assert_board_write_access(board_id, current_user)
85136
try:
86137
added_images: set[str] = set()
87138
affected_boards: set[str] = set()
88139
for image_name in image_names:
89140
try:
90-
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
141+
_assert_image_direct_owner(image_name, current_user)
142+
old_board_id = (
143+
ApiDependencies.invoker.services.board_image_records.get_board_for_image(image_name) or "none"
144+
)
91145
ApiDependencies.invoker.services.board_images.add_image_to_board(
92146
board_id=board_id,
93147
image_name=image_name,
@@ -96,12 +150,16 @@ async def add_images_to_board(
96150
affected_boards.add(board_id)
97151
affected_boards.add(old_board_id)
98152

153+
except HTTPException:
154+
raise
99155
except Exception:
100156
pass
101157
return AddImagesToBoardResult(
102158
added_images=list(added_images),
103159
affected_boards=list(affected_boards),
104160
)
161+
except HTTPException:
162+
raise
105163
except Exception:
106164
raise HTTPException(status_code=500, detail="Failed to add images to board")
107165

@@ -116,6 +174,7 @@ async def add_images_to_board(
116174
response_model=RemoveImagesFromBoardResult,
117175
)
118176
async def remove_images_from_board(
177+
current_user: CurrentUserOrDefault,
119178
image_names: list[str] = Body(description="The names of the images to remove", embed=True),
120179
) -> RemoveImagesFromBoardResult:
121180
"""Removes a list of images from their board, if they had one"""
@@ -125,15 +184,21 @@ async def remove_images_from_board(
125184
for image_name in image_names:
126185
try:
127186
old_board_id = ApiDependencies.invoker.services.images.get_dto(image_name).board_id or "none"
187+
if old_board_id != "none":
188+
_assert_board_write_access(old_board_id, current_user)
128189
ApiDependencies.invoker.services.board_images.remove_image_from_board(image_name=image_name)
129190
removed_images.add(image_name)
130191
affected_boards.add("none")
131192
affected_boards.add(old_board_id)
193+
except HTTPException:
194+
raise
132195
except Exception:
133196
pass
134197
return RemoveImagesFromBoardResult(
135198
removed_images=list(removed_images),
136199
affected_boards=list(affected_boards),
137200
)
201+
except HTTPException:
202+
raise
138203
except Exception:
139204
raise HTTPException(status_code=500, detail="Failed to remove images from board")

invokeai/app/api/routers/boards.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from invokeai.app.api.auth_dependencies import CurrentUserOrDefault
88
from invokeai.app.api.dependencies import ApiDependencies
9-
from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecordOrderBy
9+
from invokeai.app.services.board_records.board_records_common import BoardChanges, BoardRecordOrderBy, BoardVisibility
1010
from invokeai.app.services.boards.boards_common import BoardDTO
1111
from invokeai.app.services.image_records.image_records_common import ImageCategory
1212
from invokeai.app.services.shared.pagination import OffsetPaginatedResults
@@ -56,7 +56,14 @@ async def get_board(
5656
except Exception:
5757
raise HTTPException(status_code=404, detail="Board not found")
5858

59-
if not current_user.is_admin and result.user_id != current_user.user_id:
59+
# Admins can access any board.
60+
# Owners can access their own boards.
61+
# Shared and public boards are visible to all authenticated users.
62+
if (
63+
not current_user.is_admin
64+
and result.user_id != current_user.user_id
65+
and result.board_visibility == BoardVisibility.Private
66+
):
6067
raise HTTPException(status_code=403, detail="Not authorized to access this board")
6168

6269
return result
@@ -188,12 +195,27 @@ async def list_all_board_image_names(
188195
except Exception:
189196
raise HTTPException(status_code=404, detail="Board not found")
190197

191-
if not current_user.is_admin and board.user_id != current_user.user_id:
198+
if (
199+
not current_user.is_admin
200+
and board.user_id != current_user.user_id
201+
and board.board_visibility == BoardVisibility.Private
202+
):
192203
raise HTTPException(status_code=403, detail="Not authorized to access this board")
193204

194205
image_names = ApiDependencies.invoker.services.board_images.get_all_board_image_names_for_board(
195206
board_id,
196207
categories,
197208
is_intermediate,
198209
)
210+
211+
# For uncategorized images (board_id="none"), filter to only the caller's
212+
# images so that one user cannot enumerate another's uncategorized images.
213+
# Admin users can see all uncategorized images.
214+
if board_id == "none" and not current_user.is_admin:
215+
image_names = [
216+
name
217+
for name in image_names
218+
if ApiDependencies.invoker.services.image_records.get_user_id(name) == current_user.user_id
219+
]
220+
199221
return image_names

0 commit comments

Comments
 (0)