test_printer_scope_1727.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. """Printer-scoped access (#1727).
  2. A group with ``restrict_printers`` set limits its members to the printers it
  3. lists. These tests pin the contract end to end:
  4. * a member sees and acts only on the team's printers; any other printer reads
  5. as missing (404), never as forbidden, so its id isn't confirmed;
  6. * groups without the flag narrow nothing, so a user in no restricted group
  7. keeps every printer (upgrades are a no-op) and a permission group combined
  8. with a team group doesn't widen the team;
  9. * a restricted group with no printers grants none -- it does not fall back to
  10. every printer;
  11. * API keys, camera-stream, Cam Wall and WebSocket tokens all carry the scope of
  12. whoever created them.
  13. """
  14. from __future__ import annotations
  15. from unittest.mock import AsyncMock, patch
  16. import pytest
  17. from httpx import AsyncClient
  18. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  19. TEAM_PERMISSIONS = [
  20. "printers:read",
  21. "printers:control",
  22. "camera:view",
  23. "queue:read_all",
  24. "queue:create",
  25. "archives:read_all",
  26. "archives:reprint_all",
  27. "archives:delete_all",
  28. "websocket:connect",
  29. "api_keys:create",
  30. ]
  31. def _auth(jwt: str) -> dict[str, str]:
  32. return {"Authorization": f"Bearer {jwt}"}
  33. async def _admin_token(async_client: AsyncClient) -> str:
  34. await async_client.post(
  35. "/api/v1/auth/setup",
  36. json={"auth_enabled": True, "admin_username": "scopeadmin", "admin_password": "AdminPass1!"},
  37. )
  38. login = await async_client.post("/api/v1/auth/login", json={"username": "scopeadmin", "password": "AdminPass1!"})
  39. assert login.status_code == 200, login.text
  40. return login.json()["access_token"]
  41. async def _group(
  42. async_client: AsyncClient,
  43. admin_jwt: str,
  44. name: str,
  45. *,
  46. permissions: list[str] | None = None,
  47. printer_ids: list[int] | None = None,
  48. ) -> int:
  49. body: dict = {"name": name, "permissions": permissions or []}
  50. if printer_ids is not None:
  51. body.update(restrict_printers=True, printer_ids=printer_ids)
  52. response = await async_client.post("/api/v1/groups/", headers=_auth(admin_jwt), json=body)
  53. assert response.status_code == 201, response.text
  54. return response.json()["id"]
  55. async def _user(async_client: AsyncClient, admin_jwt: str, username: str, group_ids: list[int]) -> tuple[str, int]:
  56. created = await async_client.post(
  57. "/api/v1/users/",
  58. headers=_auth(admin_jwt),
  59. json={"username": username, "password": "UserPass1!", "group_ids": group_ids},
  60. )
  61. assert created.status_code in (200, 201), created.text
  62. login = await async_client.post("/api/v1/auth/login", json={"username": username, "password": "UserPass1!"})
  63. assert login.status_code == 200, login.text
  64. return login.json()["access_token"], created.json()["id"]
  65. async def _team_member(async_client: AsyncClient, admin_jwt: str, username: str, printer_ids: list[int]):
  66. """A user in a permission group plus a team group restricted to *printer_ids*."""
  67. perms = await _group(async_client, admin_jwt, f"perms_{username}", permissions=TEAM_PERMISSIONS)
  68. team = await _group(async_client, admin_jwt, f"team_{username}", printer_ids=printer_ids)
  69. return await _user(async_client, admin_jwt, username, [perms, team])
  70. async def _listed_ids(async_client: AsyncClient, headers: dict[str, str]) -> set[int]:
  71. response = await async_client.get("/api/v1/printers/", headers=headers)
  72. assert response.status_code == 200, response.text
  73. return {p["id"] for p in response.json()}
  74. class TestVisibility:
  75. async def test_user_in_no_restricted_group_sees_every_printer(self, async_client, printer_factory):
  76. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  77. admin = await _admin_token(async_client)
  78. perms = await _group(async_client, admin, "plain", permissions=TEAM_PERMISSIONS)
  79. jwt, _ = await _user(async_client, admin, "plain_user", [perms])
  80. assert await _listed_ids(async_client, _auth(jwt)) == {a.id, b.id}
  81. async def test_team_member_sees_only_team_printers(self, async_client, printer_factory):
  82. a, _b = await printer_factory(name="A"), await printer_factory(name="B")
  83. admin = await _admin_token(async_client)
  84. jwt, _ = await _team_member(async_client, admin, "member", [a.id])
  85. # The permission group (no flag) must not widen the team group
  86. assert await _listed_ids(async_client, _auth(jwt)) == {a.id}
  87. async def test_hidden_printer_reads_as_missing(self, async_client, printer_factory, mock_printer_manager):
  88. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  89. admin = await _admin_token(async_client)
  90. jwt, _ = await _team_member(async_client, admin, "member", [a.id])
  91. headers = _auth(jwt)
  92. assert (await async_client.get(f"/api/v1/printers/{b.id}", headers=headers)).status_code == 404
  93. assert (await async_client.get(f"/api/v1/printers/{b.id}/status", headers=headers)).status_code == 404
  94. with patch("backend.app.api.routes.printers.printer_manager") as manager:
  95. stop = await async_client.post(f"/api/v1/printers/{b.id}/print/stop", headers=headers)
  96. assert stop.status_code == 404
  97. manager.stop_print.assert_not_called()
  98. # The same 404 a printer id that doesn't exist gets
  99. missing = await async_client.get("/api/v1/printers/999999", headers=headers)
  100. assert missing.status_code == 404
  101. assert (await async_client.get(f"/api/v1/printers/{a.id}", headers=headers)).status_code == 200
  102. async def test_scope_is_the_union_of_restricted_groups(self, async_client, printer_factory):
  103. a = await printer_factory(name="A")
  104. b = await printer_factory(name="B")
  105. await printer_factory(name="C")
  106. admin = await _admin_token(async_client)
  107. perms = await _group(async_client, admin, "perms", permissions=TEAM_PERMISSIONS)
  108. team_a = await _group(async_client, admin, "team_a", printer_ids=[a.id])
  109. team_b = await _group(async_client, admin, "team_b", printer_ids=[b.id])
  110. jwt, _ = await _user(async_client, admin, "both", [perms, team_a, team_b])
  111. assert await _listed_ids(async_client, _auth(jwt)) == {a.id, b.id}
  112. async def test_restricted_group_without_printers_grants_none(self, async_client, printer_factory):
  113. await printer_factory(name="A")
  114. admin = await _admin_token(async_client)
  115. jwt, _ = await _team_member(async_client, admin, "locked_out", [])
  116. assert await _listed_ids(async_client, _auth(jwt)) == set()
  117. async def test_admin_sees_every_printer(self, async_client, printer_factory):
  118. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  119. admin = await _admin_token(async_client)
  120. await _team_member(async_client, admin, "member", [a.id])
  121. assert await _listed_ids(async_client, _auth(admin)) == {a.id, b.id}
  122. class TestGroupApi:
  123. async def test_round_trip(self, async_client, printer_factory):
  124. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  125. admin = await _admin_token(async_client)
  126. group_id = await _group(async_client, admin, "team", printer_ids=[a.id])
  127. detail = (await async_client.get(f"/api/v1/groups/{group_id}", headers=_auth(admin))).json()
  128. assert detail["restrict_printers"] is True
  129. assert detail["printer_ids"] == [a.id]
  130. patched = await async_client.patch(
  131. f"/api/v1/groups/{group_id}", headers=_auth(admin), json={"printer_ids": [a.id, b.id]}
  132. )
  133. assert patched.status_code == 200, patched.text
  134. assert patched.json()["printer_ids"] == [a.id, b.id]
  135. listed = (await async_client.get("/api/v1/groups/", headers=_auth(admin))).json()
  136. assert next(g for g in listed if g["id"] == group_id)["printer_ids"] == [a.id, b.id]
  137. # Turning the flag off keeps the selection
  138. off = await async_client.patch(
  139. f"/api/v1/groups/{group_id}", headers=_auth(admin), json={"restrict_printers": False}
  140. )
  141. assert off.json()["restrict_printers"] is False
  142. assert off.json()["printer_ids"] == [a.id, b.id]
  143. async def test_unknown_printer_is_rejected(self, async_client, printer_factory):
  144. await printer_factory(name="A")
  145. admin = await _admin_token(async_client)
  146. response = await async_client.post(
  147. "/api/v1/groups/",
  148. headers=_auth(admin),
  149. json={"name": "team", "restrict_printers": True, "printer_ids": [424242]},
  150. )
  151. assert response.status_code == 400
  152. assert "424242" in response.json()["detail"]
  153. async def test_administrators_cannot_be_restricted(self, async_client):
  154. admin = await _admin_token(async_client)
  155. groups = (await async_client.get("/api/v1/groups/", headers=_auth(admin))).json()
  156. admins = next(g for g in groups if g["name"] == "Administrators")
  157. response = await async_client.patch(
  158. f"/api/v1/groups/{admins['id']}", headers=_auth(admin), json={"restrict_printers": True}
  159. )
  160. assert response.status_code == 400
  161. async def test_deleting_a_printer_drops_it_from_groups(self, async_client, printer_factory):
  162. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  163. admin = await _admin_token(async_client)
  164. group_id = await _group(async_client, admin, "team", printer_ids=[a.id, b.id])
  165. with patch("backend.app.api.routes.printers.printer_manager"):
  166. deleted = await async_client.delete(f"/api/v1/printers/{b.id}", headers=_auth(admin))
  167. assert deleted.status_code == 200, deleted.text
  168. detail = (await async_client.get(f"/api/v1/groups/{group_id}", headers=_auth(admin))).json()
  169. assert detail["printer_ids"] == [a.id]
  170. async def test_deleting_a_group_drops_its_printer_rows(self, async_client, printer_factory, db_session):
  171. from sqlalchemy import select
  172. from backend.app.models.group import group_printers
  173. a = await printer_factory(name="A")
  174. admin = await _admin_token(async_client)
  175. group_id = await _group(async_client, admin, "team", printer_ids=[a.id])
  176. assert (await async_client.delete(f"/api/v1/groups/{group_id}", headers=_auth(admin))).status_code == 204
  177. rows = await db_session.execute(select(group_printers).where(group_printers.c.group_id == group_id))
  178. assert rows.first() is None
  179. class TestApiKeys:
  180. async def test_key_is_narrowed_to_its_owners_printers(self, async_client, printer_factory):
  181. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  182. admin = await _admin_token(async_client)
  183. jwt, _ = await _team_member(async_client, admin, "member", [a.id])
  184. # The key itself is unrestricted, but it can't out-rank its owner
  185. created = await async_client.post(
  186. "/api/v1/api-keys/", headers=_auth(jwt), json={"name": "k", "can_read_status": True}
  187. )
  188. assert created.status_code == 200, created.text
  189. key_headers = {"X-API-Key": created.json()["key"]}
  190. assert await _listed_ids(async_client, key_headers) == {a.id}
  191. assert (await async_client.get(f"/api/v1/printers/{b.id}", headers=key_headers)).status_code == 404
  192. webhook = await async_client.get(f"/api/v1/webhook/printer/{b.id}/status", headers=key_headers)
  193. assert webhook.status_code == 404
  194. async def test_key_printer_ids_now_bind_every_printer_route(self, async_client, printer_factory):
  195. """``printer_ids`` used to be checked by the file routes and webhooks only."""
  196. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  197. admin = await _admin_token(async_client)
  198. created = await async_client.post(
  199. "/api/v1/api-keys/",
  200. headers=_auth(admin),
  201. json={"name": "k", "can_read_status": True, "can_control_printer": True, "printer_ids": [a.id]},
  202. )
  203. key_headers = {"X-API-Key": created.json()["key"]}
  204. assert await _listed_ids(async_client, key_headers) == {a.id}
  205. with patch("backend.app.api.routes.printers.printer_manager") as manager:
  206. stop = await async_client.post(f"/api/v1/printers/{b.id}/print/stop", headers=key_headers)
  207. assert stop.status_code == 404
  208. manager.stop_print.assert_not_called()
  209. class TestTokens:
  210. async def test_camera_stream_token_carries_its_minters_scope(self, async_client, printer_factory):
  211. from backend.app.core.auth import verify_camera_stream_token
  212. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  213. admin = await _admin_token(async_client)
  214. jwt, _ = await _team_member(async_client, admin, "member", [a.id])
  215. minted = await async_client.post("/api/v1/printers/camera/stream-token", headers=_auth(jwt))
  216. token = minted.json()["token"]
  217. scope = await verify_camera_stream_token(token)
  218. assert scope is not None and scope.printer_ids == frozenset({a.id})
  219. snapshot = await async_client.get(f"/api/v1/printers/{b.id}/camera/snapshot?token={token}")
  220. assert snapshot.status_code == 404
  221. async def test_camwall_token_lists_only_its_owners_printers(self, async_client, printer_factory):
  222. a, _b = await printer_factory(name="A"), await printer_factory(name="B")
  223. admin = await _admin_token(async_client)
  224. jwt, _ = await _team_member(async_client, admin, "member", [a.id])
  225. created = await async_client.post(
  226. "/api/v1/auth/tokens",
  227. headers=_auth(jwt),
  228. json={"name": "wall", "expires_in_days": 30, "scope": "camwall"},
  229. )
  230. assert created.status_code in (200, 201), created.text
  231. token = created.json()["token"]
  232. wall = await async_client.get(f"/api/v1/camwall/printers?token={token}")
  233. assert wall.status_code == 200, wall.text
  234. assert [p["id"] for p in wall.json()] == [a.id]
  235. async def test_websocket_token_carries_its_minters_scope(self, async_client, printer_factory):
  236. from backend.app.core.auth import principal_printer_scope, verify_websocket_token_principal
  237. from backend.app.core.database import async_session
  238. a, _b = await printer_factory(name="A"), await printer_factory(name="B")
  239. admin = await _admin_token(async_client)
  240. jwt, _ = await _team_member(async_client, admin, "member", [a.id])
  241. token = (await async_client.post("/api/v1/auth/ws-token", headers=_auth(jwt))).json()["token"]
  242. principal = await verify_websocket_token_principal(token)
  243. assert principal == ("member", None)
  244. async with async_session() as db:
  245. scope = await principal_printer_scope(db, *principal)
  246. assert scope.printer_ids == frozenset({a.id})
  247. async def test_websocket_token_minted_by_a_key_carries_the_keys_scope(self, async_client, printer_factory):
  248. from backend.app.core.auth import principal_printer_scope, verify_websocket_token_principal
  249. from backend.app.core.database import async_session
  250. a, _b = await printer_factory(name="A"), await printer_factory(name="B")
  251. admin = await _admin_token(async_client)
  252. created = await async_client.post(
  253. "/api/v1/api-keys/",
  254. headers=_auth(admin),
  255. json={"name": "k", "can_read_status": True, "printer_ids": [a.id]},
  256. )
  257. key_headers = {"X-API-Key": created.json()["key"]}
  258. token = (await async_client.post("/api/v1/auth/ws-token", headers=key_headers)).json()["token"]
  259. principal = await verify_websocket_token_principal(token)
  260. assert principal == ("", created.json()["id"])
  261. async with async_session() as db:
  262. scope = await principal_printer_scope(db, *principal)
  263. assert scope.printer_ids == frozenset({a.id})
  264. class TestQueueAndHistory:
  265. async def test_queue_hides_and_refuses_other_printers(self, async_client, printer_factory, archive_factory):
  266. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  267. archive = await archive_factory(a.id)
  268. admin = await _admin_token(async_client)
  269. jwt, _ = await _team_member(async_client, admin, "member", [a.id])
  270. on_b = await async_client.post(
  271. "/api/v1/queue/", headers=_auth(admin), json={"archive_id": archive.id, "printer_id": b.id}
  272. )
  273. assert on_b.status_code == 200, on_b.text
  274. listed = await async_client.get("/api/v1/queue/", headers=_auth(jwt))
  275. assert on_b.json()["id"] not in {item["id"] for item in listed.json()}
  276. item = await async_client.get(f"/api/v1/queue/{on_b.json()['id']}", headers=_auth(jwt))
  277. assert item.status_code == 404
  278. refused = await async_client.post(
  279. "/api/v1/queue/", headers=_auth(jwt), json={"archive_id": archive.id, "printer_id": b.id}
  280. )
  281. assert refused.status_code == 404
  282. async def test_limited_key_cannot_queue_to_any_printer_of_a_model(
  283. self, async_client, printer_factory, archive_factory
  284. ):
  285. """A key's jobs record no creator, so "any X1C" would escape its printers."""
  286. a = await printer_factory(name="A", model="X1C")
  287. await printer_factory(name="B", model="X1C")
  288. archive = await archive_factory(a.id)
  289. admin = await _admin_token(async_client)
  290. created = await async_client.post(
  291. "/api/v1/api-keys/",
  292. headers=_auth(admin),
  293. json={"name": "k", "can_queue": True, "printer_ids": [a.id]},
  294. )
  295. key_headers = {"X-API-Key": created.json()["key"]}
  296. refused = await async_client.post(
  297. "/api/v1/queue/", headers=key_headers, json={"archive_id": archive.id, "target_model": "X1C"}
  298. )
  299. assert refused.status_code == 400
  300. pinned = await async_client.post(
  301. "/api/v1/queue/", headers=key_headers, json={"archive_id": archive.id, "printer_id": a.id}
  302. )
  303. assert pinned.status_code == 200, pinned.text
  304. async def test_library_add_to_queue_keeps_to_the_scope(self, async_client, printer_factory):
  305. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  306. admin = await _admin_token(async_client)
  307. created = await async_client.post(
  308. "/api/v1/api-keys/",
  309. headers=_auth(admin),
  310. json={"name": "k", "can_queue": True, "printer_ids": [a.id]},
  311. )
  312. key_headers = {"X-API-Key": created.json()["key"]}
  313. # Both are refused for the whole request, before any file is looked at
  314. hidden = await async_client.post(
  315. "/api/v1/library/files/add-to-queue", headers=key_headers, json={"file_ids": [1], "printer_id": b.id}
  316. )
  317. assert hidden.status_code == 404
  318. any_model = await async_client.post(
  319. "/api/v1/library/files/add-to-queue", headers=key_headers, json={"file_ids": [1]}
  320. )
  321. assert any_model.status_code == 400
  322. async def test_limited_user_may_queue_to_any_printer_of_a_model(
  323. self, async_client, printer_factory, archive_factory
  324. ):
  325. """The job carries the user's id, so the scheduler keeps it to their printers."""
  326. a = await printer_factory(name="A", model="X1C")
  327. await printer_factory(name="B", model="X1C")
  328. archive = await archive_factory(a.id)
  329. admin = await _admin_token(async_client)
  330. jwt, user_id = await _team_member(async_client, admin, "member", [a.id])
  331. queued = await async_client.post(
  332. "/api/v1/queue/", headers=_auth(jwt), json={"archive_id": archive.id, "target_model": "X1C"}
  333. )
  334. assert queued.status_code == 200, queued.text
  335. assert queued.json()["created_by_id"] == user_id
  336. async def test_archive_list_keeps_to_the_team_printers(self, async_client, printer_factory, archive_factory):
  337. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  338. on_a = await archive_factory(a.id, print_name="on-a")
  339. on_b = await archive_factory(b.id, print_name="on-b")
  340. admin = await _admin_token(async_client)
  341. jwt, _ = await _team_member(async_client, admin, "member", [a.id])
  342. listed = await async_client.get("/api/v1/archives/", headers=_auth(jwt))
  343. ids = {row["id"] for row in listed.json()}
  344. assert on_a.id in ids
  345. assert on_b.id not in ids
  346. class TestScheduler:
  347. async def test_model_matching_stays_within_the_scope(self, db_session, printer_factory):
  348. from backend.app.core.printer_scope import PrinterScope
  349. from backend.app.services.print_scheduler import PrintScheduler
  350. a = await printer_factory(name="A", model="X1C")
  351. await printer_factory(name="B", model="X1C")
  352. printers = await PrintScheduler()._printers_for_model(
  353. db_session, "X1C", printer_scope=PrinterScope(frozenset({a.id}))
  354. )
  355. assert [p.id for p in printers] == [a.id]
  356. async def test_deactivated_creator_reaches_no_printer(self, async_client, db_session, printer_factory):
  357. from backend.app.core.printer_scope import resolve_user_id_printer_scope
  358. await printer_factory(name="A")
  359. admin = await _admin_token(async_client)
  360. perms = await _group(async_client, admin, "perms", permissions=TEAM_PERMISSIONS)
  361. _jwt, user_id = await _user(async_client, admin, "gone", [perms])
  362. await async_client.patch(f"/api/v1/users/{user_id}", headers=_auth(admin), json={"is_active": False})
  363. scope = await resolve_user_id_printer_scope(db_session, user_id)
  364. assert scope.printer_ids == frozenset()
  365. class TestWebSocketRefresh:
  366. async def test_group_change_rescopes_open_sockets(self, async_client, printer_factory):
  367. from types import SimpleNamespace
  368. from backend.app.core.printer_scope import ALL_PRINTERS
  369. from backend.app.core.websocket import ws_manager
  370. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  371. admin = await _admin_token(async_client)
  372. jwt, _ = await _team_member(async_client, admin, "member", [a.id])
  373. groups = (await async_client.get("/api/v1/groups/", headers=_auth(admin))).json()
  374. team_id = next(g["id"] for g in groups if g["name"] == "team_member")
  375. socket = SimpleNamespace(
  376. state=SimpleNamespace(bambuddy_printer_scope=ALL_PRINTERS, bambuddy_scope_principal=("member", None)),
  377. send_text=AsyncMock(),
  378. )
  379. ws_manager.active_connections.append(socket)
  380. try:
  381. await async_client.patch(f"/api/v1/groups/{team_id}", headers=_auth(admin), json={"printer_ids": [b.id]})
  382. assert socket.state.bambuddy_printer_scope.printer_ids == frozenset({b.id})
  383. await ws_manager.send_printer_status(a.id, {})
  384. socket.send_text.assert_not_awaited()
  385. await ws_manager.send_printer_status(b.id, {})
  386. socket.send_text.assert_awaited_once()
  387. finally:
  388. ws_manager.active_connections.remove(socket)
  389. class TestByIdAndMedia:
  390. """Rows reached by id or by an ``<img>`` media token follow the same scope."""
  391. async def _archive_with_thumbnail(self, archive_factory, printer_id: int, name: str):
  392. import os
  393. from pathlib import Path
  394. from backend.app.core.config import settings
  395. rel = f"test_thumbs_1727_{os.getpid()}/{name}.png"
  396. thumb = Path(settings.base_dir) / rel
  397. thumb.parent.mkdir(parents=True, exist_ok=True)
  398. thumb.write_bytes(b"\x89PNG\r\n\x1a\n" + b"0" * 32)
  399. return await archive_factory(printer_id, thumbnail_path=rel)
  400. async def test_archives_by_id_and_by_media_token(self, async_client, printer_factory, archive_factory):
  401. import os
  402. import shutil
  403. from pathlib import Path
  404. from backend.app.core.config import settings
  405. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  406. on_a = await self._archive_with_thumbnail(archive_factory, a.id, "on_a")
  407. on_b = await self._archive_with_thumbnail(archive_factory, b.id, "on_b")
  408. admin = await _admin_token(async_client)
  409. jwt, _ = await _team_member(async_client, admin, "member", [a.id])
  410. try:
  411. assert (await async_client.get(f"/api/v1/archives/{on_a.id}", headers=_auth(jwt))).status_code == 200
  412. assert (await async_client.get(f"/api/v1/archives/{on_b.id}", headers=_auth(jwt))).status_code == 404
  413. deleted = await async_client.delete(f"/api/v1/archives/{on_b.id}", headers=_auth(jwt))
  414. assert deleted.status_code == 404
  415. # <img> requests carry a media token and no headers
  416. member_token = (await async_client.post("/api/v1/auth/media-token", headers=_auth(jwt))).json()["token"]
  417. admin_token = (await async_client.post("/api/v1/auth/media-token", headers=_auth(admin))).json()["token"]
  418. own = await async_client.get(f"/api/v1/archives/{on_a.id}/thumbnail?token={member_token}")
  419. assert own.status_code == 200
  420. hidden = await async_client.get(f"/api/v1/archives/{on_b.id}/thumbnail?token={member_token}")
  421. assert hidden.status_code == 404
  422. # An admin's thumbnails keep loading: no headers must not mean "no printers"
  423. admin_view = await async_client.get(f"/api/v1/archives/{on_b.id}/thumbnail?token={admin_token}")
  424. assert admin_view.status_code == 200
  425. finally:
  426. shutil.rmtree(Path(settings.base_dir) / f"test_thumbs_1727_{os.getpid()}", ignore_errors=True)
  427. async def test_camera_stop_is_scoped(self, async_client, printer_factory):
  428. _a, b = await printer_factory(name="A"), await printer_factory(name="B")
  429. admin = await _admin_token(async_client)
  430. jwt, _ = await _team_member(async_client, admin, "member", [_a.id])
  431. response = await async_client.post(f"/api/v1/printers/{b.id}/camera/stop", headers=_auth(jwt))
  432. assert response.status_code == 404
  433. async def test_clearing_the_print_log_keeps_other_printers_entries(
  434. self, async_client, printer_factory, archive_factory, db_session
  435. ):
  436. from sqlalchemy import select
  437. from backend.app.models.print_log import PrintLogEntry
  438. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  439. await archive_factory(a.id)
  440. await archive_factory(b.id)
  441. admin = await _admin_token(async_client)
  442. perms = await _group(async_client, admin, "perms", permissions=TEAM_PERMISSIONS)
  443. team = await _group(async_client, admin, "team", printer_ids=[a.id])
  444. jwt, _ = await _user(async_client, admin, "member", [perms, team])
  445. cleared = await async_client.delete("/api/v1/print-log/", headers=_auth(jwt))
  446. assert cleared.status_code == 200, cleared.text
  447. left = (await db_session.execute(select(PrintLogEntry.printer_id))).scalars().all()
  448. assert left == [b.id]