api_keys.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import logging
  2. from fastapi import APIRouter, Depends, HTTPException
  3. from sqlalchemy import select
  4. from sqlalchemy.ext.asyncio import AsyncSession
  5. from backend.app.core.auth import RequirePermissionIfAuthEnabled, generate_api_key
  6. from backend.app.core.database import get_db
  7. from backend.app.core.permissions import Permission
  8. from backend.app.core.printer_scope import ensure_printers_exist
  9. from backend.app.core.websocket import ws_manager
  10. from backend.app.models.api_key import APIKey
  11. from backend.app.models.user import User
  12. from backend.app.schemas.api_key import (
  13. APIKeyCreate,
  14. APIKeyCreateResponse,
  15. APIKeyResponse,
  16. APIKeyUpdate,
  17. )
  18. logger = logging.getLogger(__name__)
  19. router = APIRouter(prefix="/api-keys", tags=["api-keys"])
  20. @router.get("/", response_model=list[APIKeyResponse])
  21. async def list_api_keys(
  22. db: AsyncSession = Depends(get_db),
  23. _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_READ),
  24. ):
  25. """List all API keys (without full key values)."""
  26. result = await db.execute(select(APIKey).order_by(APIKey.created_at.desc()))
  27. return list(result.scalars().all())
  28. @router.post("/", response_model=APIKeyCreateResponse)
  29. async def create_api_key(
  30. data: APIKeyCreate,
  31. db: AsyncSession = Depends(get_db),
  32. current_user: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_CREATE),
  33. ):
  34. """Create a new API key.
  35. IMPORTANT: The full API key is only returned in this response.
  36. Store it securely - it cannot be retrieved again.
  37. """
  38. # Reject can_access_cloud on auth-disabled deployments — there's no per-user
  39. # cloud_token to read against, so the flag would just silently do nothing.
  40. # Surfacing the rejection at create time prevents the user from thinking
  41. # they've configured cloud access when they actually haven't.
  42. if data.can_access_cloud and current_user is None:
  43. raise HTTPException(
  44. status_code=400,
  45. detail="can_access_cloud requires authentication to be enabled (per-user cloud tokens)",
  46. )
  47. await ensure_printers_exist(db, data.printer_ids)
  48. # Generate the key
  49. full_key, key_hash, key_prefix = generate_api_key()
  50. api_key = APIKey(
  51. name=data.name,
  52. key_hash=key_hash,
  53. key_prefix=key_prefix,
  54. user_id=current_user.id if current_user else None,
  55. can_queue=data.can_queue,
  56. can_control_printer=data.can_control_printer,
  57. can_read_status=data.can_read_status,
  58. can_manage_library=data.can_manage_library,
  59. can_manage_inventory=data.can_manage_inventory,
  60. can_manage_maintenance=data.can_manage_maintenance,
  61. can_manage_archives=data.can_manage_archives,
  62. can_manage_projects=data.can_manage_projects,
  63. can_access_cloud=data.can_access_cloud,
  64. can_update_energy_cost=data.can_update_energy_cost,
  65. can_send_notifications=data.can_send_notifications,
  66. printer_ids=data.printer_ids,
  67. expires_at=data.expires_at,
  68. )
  69. db.add(api_key)
  70. await db.flush()
  71. await db.refresh(api_key)
  72. # Return with full key (only time it's shown)
  73. return APIKeyCreateResponse(
  74. id=api_key.id,
  75. name=api_key.name,
  76. key_prefix=api_key.key_prefix,
  77. key=full_key, # Only returned on creation
  78. user_id=api_key.user_id,
  79. can_queue=api_key.can_queue,
  80. can_control_printer=api_key.can_control_printer,
  81. can_read_status=api_key.can_read_status,
  82. can_manage_library=api_key.can_manage_library,
  83. can_manage_inventory=api_key.can_manage_inventory,
  84. can_manage_maintenance=api_key.can_manage_maintenance,
  85. can_manage_archives=api_key.can_manage_archives,
  86. can_manage_projects=api_key.can_manage_projects,
  87. can_access_cloud=api_key.can_access_cloud,
  88. can_update_energy_cost=api_key.can_update_energy_cost,
  89. can_send_notifications=api_key.can_send_notifications,
  90. printer_ids=api_key.printer_ids,
  91. enabled=api_key.enabled,
  92. last_used=api_key.last_used,
  93. created_at=api_key.created_at,
  94. expires_at=api_key.expires_at,
  95. )
  96. @router.get("/{key_id}", response_model=APIKeyResponse)
  97. async def get_api_key(
  98. key_id: int,
  99. db: AsyncSession = Depends(get_db),
  100. _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_READ),
  101. ):
  102. """Get an API key by ID."""
  103. result = await db.execute(select(APIKey).where(APIKey.id == key_id))
  104. api_key = result.scalar_one_or_none()
  105. if not api_key:
  106. raise HTTPException(status_code=404, detail="API key not found")
  107. return api_key
  108. @router.patch("/{key_id}", response_model=APIKeyResponse)
  109. async def update_api_key(
  110. key_id: int,
  111. data: APIKeyUpdate,
  112. db: AsyncSession = Depends(get_db),
  113. _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_UPDATE),
  114. ):
  115. """Update an API key."""
  116. result = await db.execute(select(APIKey).where(APIKey.id == key_id))
  117. api_key = result.scalar_one_or_none()
  118. if not api_key:
  119. raise HTTPException(status_code=404, detail="API key not found")
  120. # Update fields if provided
  121. if data.name is not None:
  122. api_key.name = data.name
  123. if data.can_queue is not None:
  124. api_key.can_queue = data.can_queue
  125. if data.can_control_printer is not None:
  126. api_key.can_control_printer = data.can_control_printer
  127. if data.can_read_status is not None:
  128. api_key.can_read_status = data.can_read_status
  129. if data.can_manage_library is not None:
  130. api_key.can_manage_library = data.can_manage_library
  131. if data.can_manage_inventory is not None:
  132. api_key.can_manage_inventory = data.can_manage_inventory
  133. if data.can_manage_maintenance is not None:
  134. api_key.can_manage_maintenance = data.can_manage_maintenance
  135. if data.can_manage_archives is not None:
  136. api_key.can_manage_archives = data.can_manage_archives
  137. if data.can_manage_projects is not None:
  138. api_key.can_manage_projects = data.can_manage_projects
  139. if data.can_access_cloud is not None:
  140. # Same constraint as create — flipping cloud access on a legacy key
  141. # without an owner would be silently broken; reject at the route layer.
  142. if data.can_access_cloud and api_key.user_id is None:
  143. raise HTTPException(
  144. status_code=400,
  145. detail="can_access_cloud requires the API key to have an owner; recreate the key after upgrading",
  146. )
  147. api_key.can_access_cloud = data.can_access_cloud
  148. if data.can_update_energy_cost is not None:
  149. api_key.can_update_energy_cost = data.can_update_energy_cost
  150. if data.can_send_notifications is not None:
  151. api_key.can_send_notifications = data.can_send_notifications
  152. if data.printer_ids is not None:
  153. await ensure_printers_exist(db, data.printer_ids)
  154. api_key.printer_ids = data.printer_ids
  155. if data.enabled is not None:
  156. api_key.enabled = data.enabled
  157. if data.expires_at is not None:
  158. api_key.expires_at = data.expires_at
  159. await db.flush()
  160. await db.refresh(api_key)
  161. if data.printer_ids is not None or data.enabled is not None or data.expires_at is not None:
  162. # Sockets opened with this key may now see fewer printers, or none
  163. # (#1727). Committed first so the refresh reads the new row.
  164. await db.commit()
  165. await ws_manager.refresh_printer_scopes()
  166. return api_key
  167. @router.delete("/{key_id}")
  168. async def delete_api_key(
  169. key_id: int,
  170. db: AsyncSession = Depends(get_db),
  171. _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_DELETE),
  172. ):
  173. """Delete (revoke) an API key."""
  174. result = await db.execute(select(APIKey).where(APIKey.id == key_id))
  175. api_key = result.scalar_one_or_none()
  176. if not api_key:
  177. raise HTTPException(status_code=404, detail="API key not found")
  178. await db.delete(api_key)
  179. # Sockets opened with this key lose their printers (#1727)
  180. await db.commit()
  181. await ws_manager.refresh_printer_scopes()
  182. return {"message": "API key deleted"}