From 03225f36d0a2aef566c6167ba4c99c2f193b648b Mon Sep 17 00:00:00 2001 From: yekta yazar Date: Wed, 22 Apr 2026 15:19:15 -0700 Subject: [PATCH] SEC: Require write access for POST /v1/tags/bulk --- app/api/v1/tags.py | 2 +- tests/test_api/test_tags.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/app/api/v1/tags.py b/app/api/v1/tags.py index 8473df0..4b6ed4d 100644 --- a/app/api/v1/tags.py +++ b/app/api/v1/tags.py @@ -180,7 +180,7 @@ class BulkTagImportResponse(BaseModel): warnings: list[str] -@router.post("/bulk") +@router.post("/bulk", dependencies=[Security(require_write_access)]) async def bulk_import_tags( data: BulkTagImportRequest, service: TagServiceDep, diff --git a/tests/test_api/test_tags.py b/tests/test_api/test_tags.py index b42c8d3..0e5ee34 100644 --- a/tests/test_api/test_tags.py +++ b/tests/test_api/test_tags.py @@ -324,3 +324,35 @@ async def test_bulk_import_tags_empty_groups(self, client: AsyncClient): assert data["groupsCreated"] == 0 assert data["tagsCreated"] == 0 assert data["tagsSkipped"] == 0 + + @pytest.mark.asyncio + async def test_bulk_import_requires_write_access(self, client: AsyncClient): + """/v1/tags/bulk rejects keys that lack write access.""" + from datetime import datetime + + from app.main import app + from app.dependencies import get_api_key + from app.schemas.api_key import ApiKeyDTO + + async def read_only_key() -> ApiKeyDTO: + return ApiKeyDTO( + id="read-only-key-id", + appName="ReadOnlyClient", + isActive=True, + readAccess=True, + writeAccess=False, + createdAt=datetime.now(), + updatedAt=datetime.now(), + ) + + # Temporarily swap in a read-only key; restore the full-access override after. + original_override = app.dependency_overrides.get(get_api_key) + app.dependency_overrides[get_api_key] = read_only_key + try: + response = await client.post("/v1/tags/bulk", json={"groups": {}}) + assert response.status_code == 401 + finally: + if original_override is not None: + app.dependency_overrides[get_api_key] = original_override + else: + app.dependency_overrides.pop(get_api_key, None)