|
|
@@ -0,0 +1,81 @@
|
|
|
+"""Routes that change shared state need a write permission, not a read one.
|
|
|
+
|
|
|
+Each route is called by a user holding only the permission it used to accept
|
|
|
+(refused) and by one holding the permission it needs now (let through; it may
|
|
|
+still fail later for its own reasons, e.g. no camera or no such device).
|
|
|
+"""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+from unittest.mock import AsyncMock, patch
|
|
|
+
|
|
|
+import pytest
|
|
|
+from httpx import AsyncClient
|
|
|
+
|
|
|
+from backend.tests.integration.test_printer_scope_1727 import _admin_token, _auth, _group, _user
|
|
|
+
|
|
|
+pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
|
|
+
|
|
|
+# (method, path template, permission that used to suffice, permission needed now)
|
|
|
+ROUTES = [
|
|
|
+ ("POST", "/api/v1/printers/{printer}/camera/plate-detection/calibrate", "camera:view", "printers:update"),
|
|
|
+ ("DELETE", "/api/v1/printers/{printer}/camera/plate-detection/calibrate", "camera:view", "printers:update"),
|
|
|
+ ("PUT", "/api/v1/printers/{printer}/camera/plate-detection/references/0", "camera:view", "printers:update"),
|
|
|
+ ("DELETE", "/api/v1/printers/{printer}/camera/plate-detection/references/0", "camera:view", "printers:update"),
|
|
|
+ ("POST", "/api/v1/bug-report/stop-logging?was_debug=true", "settings:read", "settings:update"),
|
|
|
+ ("POST", "/api/v1/smart-plugs/discover/scan", "smart_plugs:read", "smart_plugs:create"),
|
|
|
+ ("POST", "/api/v1/smart-plugs/discover/stop", "smart_plugs:read", "smart_plugs:create"),
|
|
|
+ ("POST", "/api/v1/spoolbuddy/diagnostics/no-such-device/run", "inventory:read", "inventory:update"),
|
|
|
+]
|
|
|
+
|
|
|
+
|
|
|
+async def _call(client: AsyncClient, headers: dict[str, str], method: str, path: str):
|
|
|
+ kwargs = {"json": {"label": "x"}} if method == "PUT" else {}
|
|
|
+ return await client.request(method, path, headers=headers, **kwargs)
|
|
|
+
|
|
|
+
|
|
|
+@pytest.mark.parametrize(("method", "path", "read_permission", "write_permission"), ROUTES)
|
|
|
+async def test_a_read_permission_no_longer_changes_anything(
|
|
|
+ async_client: AsyncClient, printer_factory, method, path, read_permission, write_permission
|
|
|
+):
|
|
|
+ printer = await printer_factory()
|
|
|
+ admin = await _admin_token(async_client)
|
|
|
+ reader = _auth(
|
|
|
+ (
|
|
|
+ await _user(
|
|
|
+ async_client,
|
|
|
+ admin,
|
|
|
+ "reader",
|
|
|
+ [await _group(async_client, admin, "r", permissions=["printers:read", read_permission])],
|
|
|
+ )
|
|
|
+ )[0]
|
|
|
+ )
|
|
|
+ writer = _auth(
|
|
|
+ (
|
|
|
+ await _user(
|
|
|
+ async_client,
|
|
|
+ admin,
|
|
|
+ "writer",
|
|
|
+ [
|
|
|
+ await _group(
|
|
|
+ async_client, admin, "w", permissions=["printers:read", read_permission, write_permission]
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ )
|
|
|
+ )[0]
|
|
|
+ )
|
|
|
+ url = path.format(printer=printer.id)
|
|
|
+
|
|
|
+ # Keep the let-through calls from touching the network or the log level.
|
|
|
+ with (
|
|
|
+ patch("backend.app.api.routes.smart_plugs.tasmota_scanner") as scanner,
|
|
|
+ patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", new=AsyncMock(return_value="")),
|
|
|
+ ):
|
|
|
+ scanner.scan_range = AsyncMock()
|
|
|
+ scanner.progress = (0, 0)
|
|
|
+ scanner.is_running = False
|
|
|
+ refused = await _call(async_client, reader, method, url)
|
|
|
+ allowed = await _call(async_client, writer, method, url)
|
|
|
+
|
|
|
+ assert refused.status_code == 403, refused.text
|
|
|
+ assert allowed.status_code != 403, allowed.text
|