test_project_batch_scope.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. """Adding archives or queue jobs to a project keeps to the caller's printers and rows.
  2. The project batch routes (add-archives, remove-archives, add-queue) loaded
  3. each row by id alone. A member of a group limited to printer A could link
  4. another team's archives and jobs to a project, or unlink them, and the
  5. "Added N" count told them which hidden ids exist. They also skipped the
  6. ownership rule of the archive and queue edits, so an Operator could relink
  7. other users' prints. Rows outside the caller's printers or ownership are now
  8. skipped like missing ones. An API key changes what its owner could, on its
  9. own printers.
  10. """
  11. from __future__ import annotations
  12. import pytest
  13. from httpx import AsyncClient
  14. from sqlalchemy import select
  15. from backend.app.models.archive import PrintArchive
  16. from backend.app.models.print_queue import PrintQueueItem
  17. from backend.tests.integration.test_printer_scope_1727 import TEAM_PERMISSIONS, _admin_token, _auth, _group, _user
  18. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  19. EDITOR = [*TEAM_PERMISSIONS, "projects:read", "projects:update", "archives:update_all", "queue:update_all"]
  20. async def _member(client: AsyncClient, admin: str, name: str, printer_ids: list[int] | None, permissions: list[str]):
  21. groups = [await _group(client, admin, f"perms_{name}", permissions=permissions)]
  22. if printer_ids is not None:
  23. groups.append(await _group(client, admin, f"team_{name}", printer_ids=printer_ids))
  24. jwt, user_id = await _user(client, admin, name, groups)
  25. return _auth(jwt), user_id
  26. async def _project(client: AsyncClient, admin: str) -> int:
  27. response = await client.post("/api/v1/projects/", headers=_auth(admin), json={"name": "P"})
  28. assert response.status_code == 200, response.text
  29. return response.json()["id"]
  30. async def _archive_projects(db_session, *ids: int) -> list[int | None]:
  31. db_session.expire_all()
  32. rows = dict((await db_session.execute(select(PrintArchive.id, PrintArchive.project_id))).all())
  33. return [rows[i] for i in ids]
  34. async def _queue_item(db_session, printer_id: int, archive_id: int, created_by_id: int | None = None) -> int:
  35. item = PrintQueueItem(printer_id=printer_id, archive_id=archive_id, created_by_id=created_by_id)
  36. db_session.add(item)
  37. await db_session.commit()
  38. return item.id
  39. async def test_a_member_adds_only_archives_of_the_team_printers(
  40. async_client: AsyncClient, printer_factory, archive_factory, db_session
  41. ):
  42. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  43. admin = await _admin_token(async_client)
  44. member, _ = await _member(async_client, admin, "member", [a.id], EDITOR)
  45. project = await _project(async_client, admin)
  46. on_a, on_b = await archive_factory(a.id), await archive_factory(b.id)
  47. response = await async_client.post(
  48. f"/api/v1/projects/{project}/add-archives", headers=member, json={"archive_ids": [on_a.id, on_b.id]}
  49. )
  50. assert response.status_code == 200, response.text
  51. assert response.json()["message"] == "Added 1 archives to project"
  52. assert await _archive_projects(db_session, on_a.id, on_b.id) == [project, None]
  53. async def test_a_member_cannot_remove_archives_of_other_printers(
  54. async_client: AsyncClient, printer_factory, archive_factory, db_session
  55. ):
  56. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  57. admin = await _admin_token(async_client)
  58. member, _ = await _member(async_client, admin, "member", [a.id], EDITOR)
  59. project = await _project(async_client, admin)
  60. on_a, on_b = await archive_factory(a.id), await archive_factory(b.id)
  61. added = await async_client.post(
  62. f"/api/v1/projects/{project}/add-archives", headers=_auth(admin), json={"archive_ids": [on_a.id, on_b.id]}
  63. )
  64. assert added.json()["message"] == "Added 2 archives to project"
  65. response = await async_client.post(
  66. f"/api/v1/projects/{project}/remove-archives", headers=member, json={"archive_ids": [on_a.id, on_b.id]}
  67. )
  68. assert response.json()["message"] == "Removed 1 archives from project"
  69. assert await _archive_projects(db_session, on_a.id, on_b.id) == [None, project]
  70. async def test_a_member_adds_only_queue_jobs_of_the_team_printers(
  71. async_client: AsyncClient, printer_factory, archive_factory, db_session
  72. ):
  73. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  74. admin = await _admin_token(async_client)
  75. member, _ = await _member(async_client, admin, "member", [a.id], EDITOR)
  76. project = await _project(async_client, admin)
  77. job_a = await _queue_item(db_session, a.id, (await archive_factory(a.id)).id)
  78. job_b = await _queue_item(db_session, b.id, (await archive_factory(b.id)).id)
  79. response = await async_client.post(
  80. f"/api/v1/projects/{project}/add-queue", headers=member, json={"queue_item_ids": [job_a, job_b]}
  81. )
  82. assert response.json()["message"] == "Added 1 queue items to project"
  83. db_session.expire_all()
  84. rows = dict((await db_session.execute(select(PrintQueueItem.id, PrintQueueItem.project_id))).all())
  85. assert [rows[job_a], rows[job_b]] == [project, None]
  86. async def test_with_own_permissions_only_own_rows_are_linked(
  87. async_client: AsyncClient, printer_factory, archive_factory, db_session
  88. ):
  89. a = await printer_factory(name="A")
  90. admin = await _admin_token(async_client)
  91. own_perms = [*TEAM_PERMISSIONS, "projects:read", "projects:update", "archives:update_own", "queue:update_own"]
  92. operator, operator_id = await _member(async_client, admin, "operator", None, own_perms)
  93. project = await _project(async_client, admin)
  94. mine = await archive_factory(a.id, created_by_id=operator_id)
  95. theirs = await archive_factory(a.id)
  96. my_job = await _queue_item(db_session, a.id, mine.id, created_by_id=operator_id)
  97. their_job = await _queue_item(db_session, a.id, theirs.id)
  98. archives = await async_client.post(
  99. f"/api/v1/projects/{project}/add-archives", headers=operator, json={"archive_ids": [mine.id, theirs.id]}
  100. )
  101. jobs = await async_client.post(
  102. f"/api/v1/projects/{project}/add-queue", headers=operator, json={"queue_item_ids": [my_job, their_job]}
  103. )
  104. assert archives.json()["message"] == "Added 1 archives to project"
  105. assert jobs.json()["message"] == "Added 1 queue items to project"
  106. assert await _archive_projects(db_session, mine.id, theirs.id) == [project, None]
  107. async def test_without_archive_permissions_nothing_is_linked(
  108. async_client: AsyncClient, printer_factory, archive_factory, db_session
  109. ):
  110. a = await printer_factory(name="A")
  111. admin = await _admin_token(async_client)
  112. editor, editor_id = await _member(
  113. async_client, admin, "editor", None, [*TEAM_PERMISSIONS, "projects:read", "projects:update"]
  114. )
  115. project = await _project(async_client, admin)
  116. own = await archive_factory(a.id, created_by_id=editor_id)
  117. response = await async_client.post(
  118. f"/api/v1/projects/{project}/add-archives", headers=editor, json={"archive_ids": [own.id]}
  119. )
  120. assert response.json()["message"] == "Added 0 archives to project"
  121. assert await _archive_projects(db_session, own.id) == [None]
  122. async def test_a_project_key_links_archives_of_its_printers(
  123. async_client: AsyncClient, printer_factory, archive_factory, db_session
  124. ):
  125. # A key has no per-row owner: past its project gate it may link any archive
  126. # of its printers, as API keys did before (#1893).
  127. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  128. admin = await _admin_token(async_client)
  129. project = await _project(async_client, admin)
  130. on_a, on_b = await archive_factory(a.id), await archive_factory(b.id)
  131. created = await async_client.post(
  132. "/api/v1/api-keys/",
  133. headers=_auth(admin),
  134. json={"name": "k", "can_manage_projects": True, "printer_ids": [a.id]},
  135. )
  136. assert created.status_code in (200, 201), created.text
  137. key = {"X-API-Key": created.json()["key"]}
  138. response = await async_client.post(
  139. f"/api/v1/projects/{project}/add-archives", headers=key, json={"archive_ids": [on_a.id, on_b.id]}
  140. )
  141. assert response.status_code == 200, response.text
  142. assert response.json()["message"] == "Added 1 archives to project"
  143. assert await _archive_projects(db_session, on_a.id, on_b.id) == [project, None]
  144. async def test_auth_off_links_every_archive(async_client: AsyncClient, printer_factory, archive_factory, db_session):
  145. a, b = await printer_factory(name="A"), await printer_factory(name="B")
  146. project = (await async_client.post("/api/v1/projects/", json={"name": "P"})).json()["id"]
  147. on_a, on_b = await archive_factory(a.id), await archive_factory(b.id)
  148. response = await async_client.post(
  149. f"/api/v1/projects/{project}/add-archives", json={"archive_ids": [on_a.id, on_b.id]}
  150. )
  151. assert response.json()["message"] == "Added 2 archives to project"
  152. async def test_a_key_links_only_what_its_owner_could(
  153. async_client: AsyncClient, printer_factory, archive_factory, db_session
  154. ):
  155. # A key acts with its owner's rights: an owner who may change only their own
  156. # archives cannot relink everyone's through a project key.
  157. a = await printer_factory(name="A")
  158. admin = await _admin_token(async_client)
  159. own_perms = [*TEAM_PERMISSIONS, "projects:read", "projects:update", "archives:update_own"]
  160. operator, operator_id = await _member(async_client, admin, "operator", None, own_perms)
  161. project = await _project(async_client, admin)
  162. mine, theirs = await archive_factory(a.id, created_by_id=operator_id), await archive_factory(a.id)
  163. created = await async_client.post(
  164. "/api/v1/api-keys/", headers=operator, json={"name": "k", "can_manage_projects": True}
  165. )
  166. assert created.status_code in (200, 201), created.text
  167. key = {"X-API-Key": created.json()["key"]}
  168. response = await async_client.post(
  169. f"/api/v1/projects/{project}/add-archives", headers=key, json={"archive_ids": [mine.id, theirs.id]}
  170. )
  171. assert response.status_code == 200, response.text
  172. assert response.json()["count"] == 1
  173. assert await _archive_projects(db_session, mine.id, theirs.id) == [project, None]