Skip to content

Commit a6308b4

Browse files
lsteinclaude
andcommitted
fix(multiuser): require admin auth on model install job endpoints
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>
1 parent 9e7354d commit a6308b4

2 files changed

Lines changed: 56 additions & 5 deletions

File tree

invokeai/app/api/routers/model_manager.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -858,7 +858,7 @@ def generate_html(title: str, heading: str, repo_id: str, is_error: bool, messag
858858
"/install",
859859
operation_id="list_model_installs",
860860
)
861-
async def list_model_installs() -> List[ModelInstallJob]:
861+
async def list_model_installs(current_admin: AdminUserOrDefault) -> List[ModelInstallJob]:
862862
"""Return the list of model install jobs.
863863
864864
Install jobs have a numeric `id`, a `status`, and other fields that provide information on
@@ -890,7 +890,9 @@ async def list_model_installs() -> List[ModelInstallJob]:
890890
404: {"description": "No such job"},
891891
},
892892
)
893-
async def get_model_install_job(id: int = Path(description="Model install id")) -> ModelInstallJob:
893+
async def get_model_install_job(
894+
current_admin: AdminUserOrDefault, id: int = Path(description="Model install id")
895+
) -> ModelInstallJob:
894896
"""
895897
Return model install job corresponding to the given source. See the documentation for 'List Model Install Jobs'
896898
for information on the format of the return value.
@@ -933,7 +935,9 @@ async def cancel_model_install_job(
933935
},
934936
status_code=201,
935937
)
936-
async def pause_model_install_job(id: int = Path(description="Model install job ID")) -> ModelInstallJob:
938+
async def pause_model_install_job(
939+
current_admin: AdminUserOrDefault, id: int = Path(description="Model install job ID")
940+
) -> ModelInstallJob:
937941
"""Pause the model install job corresponding to the given job ID."""
938942
installer = ApiDependencies.invoker.services.model_manager.install
939943
try:
@@ -953,7 +957,9 @@ async def pause_model_install_job(id: int = Path(description="Model install job
953957
},
954958
status_code=201,
955959
)
956-
async def resume_model_install_job(id: int = Path(description="Model install job ID")) -> ModelInstallJob:
960+
async def resume_model_install_job(
961+
current_admin: AdminUserOrDefault, id: int = Path(description="Model install job ID")
962+
) -> ModelInstallJob:
957963
"""Resume a paused model install job corresponding to the given job ID."""
958964
installer = ApiDependencies.invoker.services.model_manager.install
959965
try:
@@ -973,7 +979,9 @@ async def resume_model_install_job(id: int = Path(description="Model install job
973979
},
974980
status_code=201,
975981
)
976-
async def restart_failed_model_install_job(id: int = Path(description="Model install job ID")) -> ModelInstallJob:
982+
async def restart_failed_model_install_job(
983+
current_admin: AdminUserOrDefault, id: int = Path(description="Model install job ID")
984+
) -> ModelInstallJob:
977985
"""Restart failed or non-resumable file downloads for the given job."""
978986
installer = ApiDependencies.invoker.services.model_manager.install
979987
try:
@@ -994,6 +1002,7 @@ async def restart_failed_model_install_job(id: int = Path(description="Model ins
9941002
status_code=201,
9951003
)
9961004
async def restart_model_install_file(
1005+
current_admin: AdminUserOrDefault,
9971006
id: int = Path(description="Model install job ID"),
9981007
file_source: AnyHttpUrl = Body(description="File download URL to restart"),
9991008
) -> ModelInstallJob:

tests/app/routers/test_multiuser_authorization.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ def enable_multiuser(monkeypatch: Any, mock_invoker: Invoker):
176176
monkeypatch.setattr("invokeai.app.api.routers.workflows.ApiDependencies", mock_deps)
177177
monkeypatch.setattr("invokeai.app.api.routers.session_queue.ApiDependencies", mock_deps)
178178
monkeypatch.setattr("invokeai.app.api.routers.recall_parameters.ApiDependencies", mock_deps)
179+
monkeypatch.setattr("invokeai.app.api.routers.model_manager.ApiDependencies", mock_deps)
179180
yield
180181

181182

@@ -1245,6 +1246,47 @@ def test_session_queue_status_no_user_fields(self):
12451246
assert "user_in_progress" not in fields
12461247

12471248

1249+
# ===========================================================================
1250+
# 10b. Model install job authorization
1251+
# ===========================================================================
1252+
1253+
1254+
class TestModelInstallAuth:
1255+
"""Tests that model install job endpoints require admin authentication."""
1256+
1257+
def test_list_model_installs_requires_auth(self, enable_multiuser: Any, client: TestClient):
1258+
r = client.get("/api/v2/models/install")
1259+
assert r.status_code == status.HTTP_401_UNAUTHORIZED
1260+
1261+
def test_get_model_install_job_requires_auth(self, enable_multiuser: Any, client: TestClient):
1262+
r = client.get("/api/v2/models/install/1")
1263+
assert r.status_code == status.HTTP_401_UNAUTHORIZED
1264+
1265+
def test_pause_model_install_requires_auth(self, enable_multiuser: Any, client: TestClient):
1266+
r = client.post("/api/v2/models/install/1/pause")
1267+
assert r.status_code == status.HTTP_401_UNAUTHORIZED
1268+
1269+
def test_resume_model_install_requires_auth(self, enable_multiuser: Any, client: TestClient):
1270+
r = client.post("/api/v2/models/install/1/resume")
1271+
assert r.status_code == status.HTTP_401_UNAUTHORIZED
1272+
1273+
def test_restart_failed_model_install_requires_auth(self, enable_multiuser: Any, client: TestClient):
1274+
r = client.post("/api/v2/models/install/1/restart_failed")
1275+
assert r.status_code == status.HTTP_401_UNAUTHORIZED
1276+
1277+
def test_restart_model_install_file_requires_auth(self, enable_multiuser: Any, client: TestClient):
1278+
r = client.post("/api/v2/models/install/1/restart_file", json="https://example.com/model.safetensors")
1279+
assert r.status_code == status.HTTP_401_UNAUTHORIZED
1280+
1281+
def test_non_admin_cannot_list_model_installs(self, enable_multiuser: Any, client: TestClient, user1_token: str):
1282+
r = client.get("/api/v2/models/install", headers=_auth(user1_token))
1283+
assert r.status_code == status.HTTP_403_FORBIDDEN
1284+
1285+
def test_non_admin_cannot_pause_model_install(self, enable_multiuser: Any, client: TestClient, user1_token: str):
1286+
r = client.post("/api/v2/models/install/1/pause", headers=_auth(user1_token))
1287+
assert r.status_code == status.HTTP_403_FORBIDDEN
1288+
1289+
12481290
# ===========================================================================
12491291
# 11. Bulk download access control
12501292
# ===========================================================================

0 commit comments

Comments
 (0)