test_vp_user_codes.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. """Personal virtual-printer access codes: issuing them and the check at sign-in (#1430)."""
  2. import pytest
  3. from sqlalchemy import insert, select
  4. from backend.app.core.auth import get_password_hash
  5. from backend.app.models.group import Group, group_printers
  6. from backend.app.models.settings import Settings
  7. from backend.app.models.user import User
  8. from backend.app.models.virtual_printer import VirtualPrinter
  9. from backend.app.services.virtual_printer.user_codes import code_for, new_code, user_for_code
  10. pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
  11. async def _auth(db, on: bool = True) -> None:
  12. db.add(Settings(key="auth_enabled", value="true" if on else "false"))
  13. await db.commit()
  14. async def _user(db, permissions, *, active=True, code="11112222", name="student", restrict_to=None) -> User:
  15. group = Group(name=f"g-{name}", description="t", permissions=permissions, is_system=False)
  16. if restrict_to is not None:
  17. group.restrict_printers = True
  18. db.add(group)
  19. await db.flush()
  20. if restrict_to is not None:
  21. for printer_id in restrict_to:
  22. await db.execute(insert(group_printers).values(group_id=group.id, printer_id=printer_id))
  23. user = User(
  24. username=name,
  25. password_hash=get_password_hash("Pass1word!"),
  26. is_active=active,
  27. groups=[group],
  28. slicer_access_code=code,
  29. )
  30. db.add(user)
  31. await db.commit()
  32. return user
  33. UPLOAD = ["library:upload"]
  34. QUEUE = ["library:upload", "queue:create"]
  35. @pytest.mark.parametrize(
  36. ("mode", "permissions", "allowed"),
  37. [
  38. ("library", UPLOAD, True),
  39. ("review", UPLOAD, True),
  40. ("queue", UPLOAD, False),
  41. ("queue", QUEUE, True),
  42. ("library", ["queue:create"], False),
  43. ("archive", UPLOAD, True), # the old name of library mode
  44. ("proxy", QUEUE, False), # proxy sign-ins go to the real printer
  45. ],
  46. )
  47. async def test_the_mode_decides_what_the_user_must_be_allowed(db_session, mode, permissions, allowed):
  48. await _auth(db_session)
  49. user = await _user(db_session, permissions)
  50. found = await user_for_code(db_session, "11112222", mode=mode, target_printer_id=None)
  51. assert (found is not None and found.id == user.id) is allowed
  52. async def test_refused_while_auth_is_off(db_session):
  53. await _auth(db_session, on=False)
  54. await _user(db_session, QUEUE)
  55. assert await user_for_code(db_session, "11112222", mode="queue", target_printer_id=None) is None
  56. async def test_refused_for_a_disabled_account_and_an_unknown_code(db_session):
  57. await _auth(db_session)
  58. await _user(db_session, QUEUE, active=False)
  59. assert await user_for_code(db_session, "11112222", mode="queue", target_printer_id=None) is None
  60. assert await user_for_code(db_session, "99998888", mode="queue", target_printer_id=None) is None
  61. assert await user_for_code(db_session, "short", mode="queue", target_printer_id=None) is None
  62. async def test_the_target_printer_must_be_in_the_users_scope(db_session, printer_factory):
  63. await _auth(db_session)
  64. mine = await printer_factory(name="Mine")
  65. other = await printer_factory(name="Other")
  66. await _user(db_session, [*QUEUE, "printers:read"], restrict_to=[mine.id])
  67. assert await user_for_code(db_session, "11112222", mode="queue", target_printer_id=mine.id) is not None
  68. assert await user_for_code(db_session, "11112222", mode="queue", target_printer_id=other.id) is None
  69. # No target: any printer of the model, which the queue checks per printer.
  70. assert await user_for_code(db_session, "11112222", mode="queue", target_printer_id=None) is not None
  71. async def test_a_code_is_made_once_and_renewed_on_request(db_session):
  72. user = await _user(db_session, QUEUE, code=None)
  73. first = await code_for(db_session, user)
  74. again = await code_for(db_session, user)
  75. renewed = await code_for(db_session, user, renew=True)
  76. assert first == again
  77. assert renewed != first
  78. assert len(renewed) == 8 and renewed.isdigit()
  79. assert (await db_session.execute(select(User.slicer_access_code).where(User.id == user.id))).scalar() == renewed
  80. async def test_a_new_code_is_nobody_elses(db_session, monkeypatch):
  81. # The first candidates are taken by a user and by a virtual printer.
  82. await _user(db_session, QUEUE, code="00000001")
  83. db_session.add(VirtualPrinter(name="VP", mode="library", access_code="00000002"))
  84. await db_session.commit()
  85. candidates = iter("00000001" + "00000002" + "00000003")
  86. monkeypatch.setattr(
  87. "backend.app.services.virtual_printer.user_codes.secrets.choice", lambda _digits: next(candidates)
  88. )
  89. assert await new_code(db_session) == "00000003"
  90. async def _login(async_client, name="student") -> dict:
  91. response = await async_client.post("/api/v1/auth/login", json={"username": name, "password": "Pass1word!"})
  92. assert response.status_code == 200, response.text
  93. return {"Authorization": f"Bearer {response.json()['access_token']}"}
  94. async def test_a_user_reads_and_renews_their_own_code(async_client, db_session):
  95. db_session.add(Settings(key="advanced_auth_enabled", value="false"))
  96. await _auth(db_session)
  97. await _user(db_session, ["library:read_own"], code=None)
  98. headers = await _login(async_client)
  99. first = await async_client.get("/api/v1/virtual-printers/my-access-code", headers=headers)
  100. again = await async_client.get("/api/v1/virtual-printers/my-access-code", headers=headers)
  101. renewed = await async_client.post("/api/v1/virtual-printers/my-access-code", headers=headers)
  102. assert first.status_code == 200, first.text
  103. assert first.json() == again.json()
  104. assert renewed.json()["code"] != first.json()["code"]
  105. async def test_no_code_without_signing_in(async_client, db_session):
  106. await _auth(db_session)
  107. response = await async_client.get("/api/v1/virtual-printers/my-access-code")
  108. assert response.status_code == 401
  109. async def test_an_approved_upload_and_a_reused_file_stay_the_senders(db_session, tmp_path, monkeypatch):
  110. # The File Manager file is the sender's, whoever approves it, and a later
  111. # upload by someone else never takes it over.
  112. from backend.app.core.config import settings
  113. from backend.app.services.virtual_printer.library_store import store_upload
  114. monkeypatch.setattr(settings, "archive_dir", tmp_path / "archive")
  115. student = await _user(db_session, UPLOAD, name="student")
  116. other = await _user(db_session, UPLOAD, name="other", code="33334444")
  117. source = tmp_path / "plate.3mf"
  118. source.write_bytes(b"plate")
  119. mine, _ = await store_upload(
  120. db_session, source_path=source, filename="plate.3mf", folder_id=None, owner_id=student.id
  121. )
  122. theirs, reused = await store_upload(
  123. db_session, source_path=source, filename="plate.3mf", folder_id=None, owner_id=other.id
  124. )
  125. again, reused_again = await store_upload(
  126. db_session, source_path=source, filename="plate.3mf", folder_id=None, owner_id=student.id
  127. )
  128. assert mine.created_by_id == student.id
  129. assert reused is False and theirs.created_by_id == other.id
  130. assert reused_again is True and again.id == mine.id
  131. async def test_a_vp_with_a_target_printer_needs_printers_read_too(db_session, printer_factory):
  132. # The slicer shows that printer's live state through the VP.
  133. await _auth(db_session)
  134. printer = await printer_factory(name="Mine")
  135. await _user(db_session, UPLOAD)
  136. assert await user_for_code(db_session, "11112222", mode="library", target_printer_id=None) is not None
  137. assert await user_for_code(db_session, "11112222", mode="library", target_printer_id=printer.id) is None
  138. @pytest.mark.parametrize(
  139. ("permissions", "active", "in_scope", "allowed"),
  140. [
  141. (["printers:control"], True, True, True),
  142. (["printers:read"], True, True, False),
  143. (["printers:control"], False, True, False),
  144. (["printers:control"], True, False, False),
  145. ],
  146. ids=["control", "read-only", "disabled", "other-printer"],
  147. )
  148. async def test_commands_to_the_printer_need_printers_control_on_it(
  149. db_session, printer_factory, permissions, active, in_scope, allowed
  150. ):
  151. from backend.app.services.virtual_printer.user_codes import may_control_printer
  152. await _auth(db_session)
  153. mine = await printer_factory(name="Mine")
  154. other = await printer_factory(name="Other")
  155. user = await _user(db_session, permissions, active=active, restrict_to=[mine.id])
  156. assert await may_control_printer(db_session, user.id, mine.id if in_scope else other.id) is allowed