|
@@ -0,0 +1,219 @@
|
|
|
|
|
+"""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]
|