11from fastapi import Body , HTTPException
22from fastapi .routing import APIRouter
33
4+ from invokeai .app .api .auth_dependencies import CurrentUserOrDefault
45from invokeai .app .api .dependencies import ApiDependencies
56from invokeai .app .services .images .images_common import AddImagesToBoardResult , RemoveImagesFromBoardResult
67
78board_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" ,
1758 response_model = AddImagesToBoardResult ,
1859)
1960async 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)
5094async 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)
80129async 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)
118176async 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" )
0 commit comments