row_visibility.py 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. """Which owned rows (archives, queue jobs, library files) a caller reaches through something that holds them."""
  2. from backend.app.core.auth import ApiKeyActor
  3. from backend.app.core.permissions import Permission
  4. from backend.app.core.printer_scope import PrinterScope
  5. from backend.app.models.archive import PrintArchive
  6. from backend.app.models.print_queue import PrintQueueItem
  7. from backend.app.models.user import User
  8. def owned_row_clauses(
  9. model, actor: User | ApiKeyActor | None, all_permission: Permission, own_permission: Permission
  10. ) -> list | None:
  11. """WHERE clauses for the rows of ``model`` the caller may reach by ownership, or None for none.
  12. Their own unless they may reach everyone's (a row without an owner then
  13. stays out). Auth off (no actor) reaches all.
  14. """
  15. if actor is None:
  16. return []
  17. # A key acts with its owner's rights to these rows, not with its own scope
  18. # flags (a project key needs no archive or queue flag, #1893), so it never
  19. # reaches a row its owner could not. A legacy key without an owner keeps
  20. # every row.
  21. user = actor.owner if isinstance(actor, ApiKeyActor) else actor
  22. if user is None or user.has_permission(all_permission.value):
  23. return []
  24. if user.has_permission(own_permission.value):
  25. return [model.created_by_id == user.id]
  26. return None
  27. def visible_row_clauses(
  28. model: type[PrintArchive] | type[PrintQueueItem],
  29. printer_scope: PrinterScope,
  30. actor: User | ApiKeyActor | None,
  31. all_permission: Permission,
  32. own_permission: Permission,
  33. *,
  34. by_runs: bool = False,
  35. ) -> list | None:
  36. """WHERE clauses for the archive or queue rows the caller may reach, or None for none.
  37. The rows the caller reaches through the archives or the queue themselves:
  38. on their printers, and by ``owned_row_clauses``. ``by_runs``: archives
  39. printed on one of their printers too (``archive_scope_clause``), for a
  40. listing that shows them as the caller sees them (the archives routes'
  41. ``_scoped``); others show an archive's latest print as it is.
  42. """
  43. owned = owned_row_clauses(model, actor, all_permission, own_permission)
  44. if owned is None:
  45. return None
  46. clause = (
  47. archive_scope_clause(printer_scope)
  48. if model is PrintArchive and by_runs
  49. else printer_scope.where(model.printer_id)
  50. )
  51. if clause is not None:
  52. return [clause, *owned]
  53. return owned
  54. def archive_scope_clause(printer_scope: PrinterScope):
  55. """The archives a printer scope sees (#1727), as a WHERE clause; None
  56. when it sees every printer. An archive is a file printed as runs, maybe
  57. on several printers (archive redesign): it is seen on its own printer
  58. (the latest run's, or none) and on every printer one of its runs printed
  59. on, a print running now included.
  60. """
  61. from sqlalchemy import or_, select
  62. from backend.app.models.print_log import PrintLogEntry
  63. own = printer_scope.where(PrintArchive.printer_id)
  64. if own is None:
  65. return None
  66. # The table, not the entity: the ORM hides open runs from every read of
  67. # PrintLogEntry, and a print running on the caller's printer counts.
  68. runs = PrintLogEntry.__table__
  69. printed_here = select(runs.c.archive_id).where(
  70. runs.c.archive_id.is_not(None), runs.c.printer_id.in_(printer_scope.printer_ids or ())
  71. )
  72. return or_(own, PrintArchive.id.in_(printed_here))
  73. def whole_archive_scope_clause(printer_scope: PrinterScope):
  74. """The archives every print of which the scope sees, as a WHERE clause;
  75. None when it sees every printer. What changes or removes the whole
  76. archive (its file, its video, the archive itself) needs this: it would
  77. change other printers' prints too (archive redesign)."""
  78. from sqlalchemy import and_, not_, select
  79. from backend.app.models.print_log import PrintLogEntry
  80. own = printer_scope.where(PrintArchive.printer_id)
  81. if own is None:
  82. return None
  83. runs = PrintLogEntry.__table__
  84. printed_elsewhere = select(runs.c.archive_id).where(
  85. runs.c.archive_id.is_not(None),
  86. runs.c.printer_id.is_not(None),
  87. runs.c.printer_id.not_in(printer_scope.printer_ids or ()),
  88. )
  89. return and_(own, not_(PrintArchive.id.in_(printed_elsewhere)))
  90. def may_reach_row(
  91. created_by_id: int | None,
  92. actor: User | ApiKeyActor | None,
  93. all_permission: Permission,
  94. own_permission: Permission,
  95. ) -> bool:
  96. """Whether the caller may reach one row owned by ``created_by_id``, printer scope aside.
  97. The rule of ``visible_row_clauses`` for a row already loaded.
  98. """
  99. if actor is None:
  100. return True
  101. user = actor.owner if isinstance(actor, ApiKeyActor) else actor
  102. if user is None or user.has_permission(all_permission.value):
  103. return True
  104. return user.has_permission(own_permission.value) and created_by_id is not None and created_by_id == user.id