mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
181 lines
7.3 KiB
Python
181 lines
7.3 KiB
Python
"""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
|