| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180 |
- """The /makerworld/* permission gate with auth enabled.
- The gate moved out of the route signature and into the handler: the provider
- that a request actually uses comes from the body (``source_type`` on import,
- the pasted URL on resolve), and FastAPI resolves dependencies before the body
- exists, so a dependency could only ever name one provider's permission. What
- must not change is the enforcement itself, so these pin the outcomes rather
- than the wiring: anonymous callers are still refused before the body is read,
- and a signed-in user without the permission still gets a 403.
- """
- from __future__ import annotations
- from unittest.mock import AsyncMock, patch
- import pytest
- from httpx import AsyncClient
- from backend.app.services.model_providers.base import (
- ProviderDownload,
- ProviderDownloadInfo,
- ProviderResolvedModel,
- ProviderResourceRef,
- )
- async def _setup_auth_with_admin(client: AsyncClient) -> str:
- await client.post(
- "/api/v1/auth/setup",
- json={"auth_enabled": True, "admin_username": "mwadmin", "admin_password": "AdminPass1!"},
- )
- login = await client.post("/api/v1/auth/login", json={"username": "mwadmin", "password": "AdminPass1!"})
- assert login.status_code == 200, login.text
- return login.json()["access_token"]
- async def _make_user(client: AsyncClient, admin_jwt: str, *, username: str, permissions: list[str]) -> str:
- """Create a user in a fresh group holding exactly *permissions*."""
- group = await client.post(
- "/api/v1/groups/",
- headers={"Authorization": f"Bearer {admin_jwt}"},
- json={"name": f"grp_{username}", "permissions": permissions},
- )
- assert group.status_code in (200, 201), group.text
- created = await client.post(
- "/api/v1/users/",
- headers={"Authorization": f"Bearer {admin_jwt}"},
- json={"username": username, "password": "UserPass1!", "group_ids": [group.json()["id"]]},
- )
- assert created.status_code in (200, 201), created.text
- login = await client.post("/api/v1/auth/login", json={"username": username, "password": "UserPass1!"})
- assert login.status_code == 200, login.text
- return login.json()["access_token"]
- def _fake_service(**stubs):
- svc = AsyncMock()
- svc.close = AsyncMock()
- for name, value in stubs.items():
- setattr(svc, name, AsyncMock(return_value=value))
- return svc
- def _import_service():
- return _fake_service(
- get_download=ProviderDownloadInfo(
- ref=ProviderResourceRef(source_type="makerworld", external_id="1400373", sub_id="298919107"),
- url="https://makerworld.bblmw.com/makerworld/model/X/Y/cube.3mf?exp=1&key=k",
- suggested_filename="cube.3mf",
- ),
- download=ProviderDownload(file_bytes=b"PK\x03\x04fake-3mf-bytes", filename="cube.3mf"),
- )
- class TestAnonymousIsRefusedFirst:
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_anonymous_import_is_401(self, async_client: AsyncClient):
- await _setup_auth_with_admin(async_client)
- resp = await async_client.post("/api/v1/makerworld/import", json={"model_id": 1400373})
- assert resp.status_code == 401, resp.text
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_anonymous_resolve_is_401(self, async_client: AsyncClient):
- await _setup_auth_with_admin(async_client)
- resp = await async_client.post(
- "/api/v1/makerworld/resolve",
- json={"url": "https://makerworld.com/en/models/1400373"},
- )
- assert resp.status_code == 401, resp.text
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_anonymous_with_a_malformed_body_is_still_401_not_422(self, async_client: AsyncClient):
- """The permission moved into the handler, but authentication stayed a
- route dependency precisely so an unauthenticated caller cannot probe
- the request schema through validation errors."""
- await _setup_auth_with_admin(async_client)
- resp = await async_client.post("/api/v1/makerworld/import", json={"nonsense": True})
- assert resp.status_code == 401, resp.text
- class TestPermissionStillBites:
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_view_only_user_cannot_import(self, async_client: AsyncClient):
- admin = await _setup_auth_with_admin(async_client)
- jwt = await _make_user(async_client, admin, username="mwviewer", permissions=["makerworld:view"])
- with patch(
- "backend.app.api.routes.makerworld._build_service",
- AsyncMock(return_value=_import_service()),
- ):
- resp = await async_client.post(
- "/api/v1/makerworld/import",
- json={"model_id": 1400373},
- headers={"Authorization": f"Bearer {jwt}"},
- )
- assert resp.status_code == 403, resp.text
- assert "makerworld:import" in resp.json()["detail"]
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_user_without_view_cannot_resolve(self, async_client: AsyncClient):
- admin = await _setup_auth_with_admin(async_client)
- jwt = await _make_user(async_client, admin, username="mwnoview", permissions=["printers:read"])
- resp = await async_client.post(
- "/api/v1/makerworld/resolve",
- json={"url": "https://makerworld.com/en/models/1400373"},
- headers={"Authorization": f"Bearer {jwt}"},
- )
- assert resp.status_code == 403, resp.text
- assert "makerworld:view" in resp.json()["detail"]
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_user_holding_the_permission_gets_through(self, async_client: AsyncClient):
- admin = await _setup_auth_with_admin(async_client)
- jwt = await _make_user(
- async_client,
- admin,
- username="mwimporter",
- permissions=["makerworld:view", "makerworld:import"],
- )
- with patch(
- "backend.app.api.routes.makerworld._build_service",
- AsyncMock(return_value=_import_service()),
- ):
- resp = await async_client.post(
- "/api/v1/makerworld/import",
- json={"model_id": 1400373},
- headers={"Authorization": f"Bearer {jwt}"},
- )
- assert resp.status_code == 200, resp.text
- assert resp.json()["was_existing"] is False
- @pytest.mark.asyncio
- @pytest.mark.integration
- async def test_resolve_passes_for_a_viewer(self, async_client: AsyncClient):
- admin = await _setup_auth_with_admin(async_client)
- jwt = await _make_user(async_client, admin, username="mwviewer2", permissions=["makerworld:view"])
- svc = _fake_service(
- resolve=ProviderResolvedModel(
- ref=ProviderResourceRef(source_type="makerworld", external_id="1400373"),
- design={"id": 1400373},
- instances=[],
- )
- )
- with patch("backend.app.api.routes.makerworld._build_service", AsyncMock(return_value=svc)):
- resp = await async_client.post(
- "/api/v1/makerworld/resolve",
- json={"url": "https://makerworld.com/en/models/1400373"},
- headers={"Authorization": f"Bearer {jwt}"},
- )
- assert resp.status_code == 200, resp.text
- assert resp.json()["model_id"] == 1400373
|