test_writes_need_write_permissions.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. """Routes that change shared state need a write permission, not a read one.
  2. Each route is called by a user holding only the permission it used to accept
  3. (refused) and by one holding the permission it needs now (let through; it may
  4. still fail later for its own reasons, e.g. no camera or no such device).
  5. """
  6. from __future__ import annotations
  7. from unittest.mock import AsyncMock, patch
  8. import pytest
  9. from httpx import AsyncClient
  10. from backend.tests.integration.test_printer_scope_1727 import _admin_token, _auth, _group, _user
  11. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  12. # (method, path template, permission that used to suffice, permission needed now)
  13. ROUTES = [
  14. ("POST", "/api/v1/printers/{printer}/camera/plate-detection/calibrate", "camera:view", "printers:update"),
  15. ("DELETE", "/api/v1/printers/{printer}/camera/plate-detection/calibrate", "camera:view", "printers:update"),
  16. ("PUT", "/api/v1/printers/{printer}/camera/plate-detection/references/0", "camera:view", "printers:update"),
  17. ("DELETE", "/api/v1/printers/{printer}/camera/plate-detection/references/0", "camera:view", "printers:update"),
  18. ("POST", "/api/v1/bug-report/stop-logging?was_debug=true", "settings:read", "settings:update"),
  19. ("POST", "/api/v1/smart-plugs/discover/scan", "smart_plugs:read", "smart_plugs:create"),
  20. ("POST", "/api/v1/smart-plugs/discover/stop", "smart_plugs:read", "smart_plugs:create"),
  21. ("POST", "/api/v1/spoolbuddy/diagnostics/no-such-device/run", "inventory:read", "inventory:update"),
  22. ]
  23. async def _call(client: AsyncClient, headers: dict[str, str], method: str, path: str):
  24. kwargs = {"json": {"label": "x"}} if method == "PUT" else {}
  25. return await client.request(method, path, headers=headers, **kwargs)
  26. @pytest.mark.parametrize(("method", "path", "read_permission", "write_permission"), ROUTES)
  27. async def test_a_read_permission_no_longer_changes_anything(
  28. async_client: AsyncClient, printer_factory, method, path, read_permission, write_permission
  29. ):
  30. printer = await printer_factory()
  31. admin = await _admin_token(async_client)
  32. reader = _auth(
  33. (
  34. await _user(
  35. async_client,
  36. admin,
  37. "reader",
  38. [await _group(async_client, admin, "r", permissions=["printers:read", read_permission])],
  39. )
  40. )[0]
  41. )
  42. writer = _auth(
  43. (
  44. await _user(
  45. async_client,
  46. admin,
  47. "writer",
  48. [
  49. await _group(
  50. async_client, admin, "w", permissions=["printers:read", read_permission, write_permission]
  51. )
  52. ],
  53. )
  54. )[0]
  55. )
  56. url = path.format(printer=printer.id)
  57. # Keep the let-through calls from touching the network or the log level.
  58. with (
  59. patch("backend.app.api.routes.smart_plugs.tasmota_scanner") as scanner,
  60. patch("backend.app.api.routes.bug_report._get_recent_sanitized_logs", new=AsyncMock(return_value="")),
  61. ):
  62. scanner.scan_range = AsyncMock()
  63. scanner.progress = (0, 0)
  64. scanner.is_running = False
  65. refused = await _call(async_client, reader, method, url)
  66. allowed = await _call(async_client, writer, method, url)
  67. assert refused.status_code == 403, refused.text
  68. assert allowed.status_code != 403, allowed.text