| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- """Adding archives or queue jobs to a project keeps to the caller's printers and rows.
- The project batch routes (add-archives, remove-archives, add-queue) loaded
- each row by id alone. A member of a group limited to printer A could link
- another team's archives and jobs to a project, or unlink them, and the
- "Added N" count told them which hidden ids exist. They also skipped the
- ownership rule of the archive and queue edits, so an Operator could relink
- other users' prints. Rows outside the caller's printers or ownership are now
- skipped like missing ones. An API key changes what its owner could, on its
- own printers.
- """
- from __future__ import annotations
- import pytest
- from httpx import AsyncClient
- from sqlalchemy import select
- from backend.app.models.archive import PrintArchive
- from backend.app.models.print_queue import PrintQueueItem
- from backend.tests.integration.test_printer_scope_1727 import TEAM_PERMISSIONS, _admin_token, _auth, _group, _user
- pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
- EDITOR = [*TEAM_PERMISSIONS, "projects:read", "projects:update", "archives:update_all", "queue:update_all"]
- async def _member(client: AsyncClient, admin: str, name: str, printer_ids: list[int] | None, permissions: list[str]):
- groups = [await _group(client, admin, f"perms_{name}", permissions=permissions)]
- if printer_ids is not None:
- groups.append(await _group(client, admin, f"team_{name}", printer_ids=printer_ids))
- jwt, user_id = await _user(client, admin, name, groups)
- return _auth(jwt), user_id
- async def _project(client: AsyncClient, admin: str) -> int:
- response = await client.post("/api/v1/projects/", headers=_auth(admin), json={"name": "P"})
- assert response.status_code == 200, response.text
- return response.json()["id"]
- async def _archive_projects(db_session, *ids: int) -> list[int | None]:
- db_session.expire_all()
- rows = dict((await db_session.execute(select(PrintArchive.id, PrintArchive.project_id))).all())
- return [rows[i] for i in ids]
- async def _queue_item(db_session, printer_id: int, archive_id: int, created_by_id: int | None = None) -> int:
- item = PrintQueueItem(printer_id=printer_id, archive_id=archive_id, created_by_id=created_by_id)
- db_session.add(item)
- await db_session.commit()
- return item.id
- async def test_a_member_adds_only_archives_of_the_team_printers(
- async_client: AsyncClient, printer_factory, archive_factory, db_session
- ):
- a, b = await printer_factory(name="A"), await printer_factory(name="B")
- admin = await _admin_token(async_client)
- member, _ = await _member(async_client, admin, "member", [a.id], EDITOR)
- project = await _project(async_client, admin)
- on_a, on_b = await archive_factory(a.id), await archive_factory(b.id)
- response = await async_client.post(
- f"/api/v1/projects/{project}/add-archives", headers=member, json={"archive_ids": [on_a.id, on_b.id]}
- )
- assert response.status_code == 200, response.text
- assert response.json()["message"] == "Added 1 archives to project"
- assert await _archive_projects(db_session, on_a.id, on_b.id) == [project, None]
- async def test_a_member_cannot_remove_archives_of_other_printers(
- async_client: AsyncClient, printer_factory, archive_factory, db_session
- ):
- a, b = await printer_factory(name="A"), await printer_factory(name="B")
- admin = await _admin_token(async_client)
- member, _ = await _member(async_client, admin, "member", [a.id], EDITOR)
- project = await _project(async_client, admin)
- on_a, on_b = await archive_factory(a.id), await archive_factory(b.id)
- added = await async_client.post(
- f"/api/v1/projects/{project}/add-archives", headers=_auth(admin), json={"archive_ids": [on_a.id, on_b.id]}
- )
- assert added.json()["message"] == "Added 2 archives to project"
- response = await async_client.post(
- f"/api/v1/projects/{project}/remove-archives", headers=member, json={"archive_ids": [on_a.id, on_b.id]}
- )
- assert response.json()["message"] == "Removed 1 archives from project"
- assert await _archive_projects(db_session, on_a.id, on_b.id) == [None, project]
- async def test_a_member_adds_only_queue_jobs_of_the_team_printers(
- async_client: AsyncClient, printer_factory, archive_factory, db_session
- ):
- a, b = await printer_factory(name="A"), await printer_factory(name="B")
- admin = await _admin_token(async_client)
- member, _ = await _member(async_client, admin, "member", [a.id], EDITOR)
- project = await _project(async_client, admin)
- job_a = await _queue_item(db_session, a.id, (await archive_factory(a.id)).id)
- job_b = await _queue_item(db_session, b.id, (await archive_factory(b.id)).id)
- response = await async_client.post(
- f"/api/v1/projects/{project}/add-queue", headers=member, json={"queue_item_ids": [job_a, job_b]}
- )
- assert response.json()["message"] == "Added 1 queue items to project"
- db_session.expire_all()
- rows = dict((await db_session.execute(select(PrintQueueItem.id, PrintQueueItem.project_id))).all())
- assert [rows[job_a], rows[job_b]] == [project, None]
- async def test_with_own_permissions_only_own_rows_are_linked(
- async_client: AsyncClient, printer_factory, archive_factory, db_session
- ):
- a = await printer_factory(name="A")
- admin = await _admin_token(async_client)
- own_perms = [*TEAM_PERMISSIONS, "projects:read", "projects:update", "archives:update_own", "queue:update_own"]
- operator, operator_id = await _member(async_client, admin, "operator", None, own_perms)
- project = await _project(async_client, admin)
- mine = await archive_factory(a.id, created_by_id=operator_id)
- theirs = await archive_factory(a.id)
- my_job = await _queue_item(db_session, a.id, mine.id, created_by_id=operator_id)
- their_job = await _queue_item(db_session, a.id, theirs.id)
- archives = await async_client.post(
- f"/api/v1/projects/{project}/add-archives", headers=operator, json={"archive_ids": [mine.id, theirs.id]}
- )
- jobs = await async_client.post(
- f"/api/v1/projects/{project}/add-queue", headers=operator, json={"queue_item_ids": [my_job, their_job]}
- )
- assert archives.json()["message"] == "Added 1 archives to project"
- assert jobs.json()["message"] == "Added 1 queue items to project"
- assert await _archive_projects(db_session, mine.id, theirs.id) == [project, None]
- async def test_without_archive_permissions_nothing_is_linked(
- async_client: AsyncClient, printer_factory, archive_factory, db_session
- ):
- a = await printer_factory(name="A")
- admin = await _admin_token(async_client)
- editor, editor_id = await _member(
- async_client, admin, "editor", None, [*TEAM_PERMISSIONS, "projects:read", "projects:update"]
- )
- project = await _project(async_client, admin)
- own = await archive_factory(a.id, created_by_id=editor_id)
- response = await async_client.post(
- f"/api/v1/projects/{project}/add-archives", headers=editor, json={"archive_ids": [own.id]}
- )
- assert response.json()["message"] == "Added 0 archives to project"
- assert await _archive_projects(db_session, own.id) == [None]
- async def test_a_project_key_links_archives_of_its_printers(
- async_client: AsyncClient, printer_factory, archive_factory, db_session
- ):
- # A key has no per-row owner: past its project gate it may link any archive
- # of its printers, as API keys did before (#1893).
- a, b = await printer_factory(name="A"), await printer_factory(name="B")
- admin = await _admin_token(async_client)
- project = await _project(async_client, admin)
- on_a, on_b = await archive_factory(a.id), await archive_factory(b.id)
- created = await async_client.post(
- "/api/v1/api-keys/",
- headers=_auth(admin),
- json={"name": "k", "can_manage_projects": True, "printer_ids": [a.id]},
- )
- assert created.status_code in (200, 201), created.text
- key = {"X-API-Key": created.json()["key"]}
- response = await async_client.post(
- f"/api/v1/projects/{project}/add-archives", headers=key, json={"archive_ids": [on_a.id, on_b.id]}
- )
- assert response.status_code == 200, response.text
- assert response.json()["message"] == "Added 1 archives to project"
- assert await _archive_projects(db_session, on_a.id, on_b.id) == [project, None]
- async def test_auth_off_links_every_archive(async_client: AsyncClient, printer_factory, archive_factory, db_session):
- a, b = await printer_factory(name="A"), await printer_factory(name="B")
- project = (await async_client.post("/api/v1/projects/", json={"name": "P"})).json()["id"]
- on_a, on_b = await archive_factory(a.id), await archive_factory(b.id)
- response = await async_client.post(
- f"/api/v1/projects/{project}/add-archives", json={"archive_ids": [on_a.id, on_b.id]}
- )
- assert response.json()["message"] == "Added 2 archives to project"
- async def test_a_key_links_only_what_its_owner_could(
- async_client: AsyncClient, printer_factory, archive_factory, db_session
- ):
- # A key acts with its owner's rights: an owner who may change only their own
- # archives cannot relink everyone's through a project key.
- a = await printer_factory(name="A")
- admin = await _admin_token(async_client)
- own_perms = [*TEAM_PERMISSIONS, "projects:read", "projects:update", "archives:update_own"]
- operator, operator_id = await _member(async_client, admin, "operator", None, own_perms)
- project = await _project(async_client, admin)
- mine, theirs = await archive_factory(a.id, created_by_id=operator_id), await archive_factory(a.id)
- created = await async_client.post(
- "/api/v1/api-keys/", headers=operator, json={"name": "k", "can_manage_projects": True}
- )
- assert created.status_code in (200, 201), created.text
- key = {"X-API-Key": created.json()["key"]}
- response = await async_client.post(
- f"/api/v1/projects/{project}/add-archives", headers=key, json={"archive_ids": [mine.id, theirs.id]}
- )
- assert response.status_code == 200, response.text
- assert response.json()["count"] == 1
- assert await _archive_projects(db_session, mine.id, theirs.id) == [project, None]
|