api_keys.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  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.models.api_key import APIKey
  9. from backend.app.models.user import User
  10. from backend.app.schemas.api_key import (
  11. APIKeyCreate,
  12. APIKeyCreateResponse,
  13. APIKeyResponse,
  14. APIKeyUpdate,
  15. )
  16. logger = logging.getLogger(__name__)
  17. router = APIRouter(prefix="/api-keys", tags=["api-keys"])
  18. @router.get("/", response_model=list[APIKeyResponse])
  19. async def list_api_keys(
  20. db: AsyncSession = Depends(get_db),
  21. _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_READ),
  22. ):
  23. """List all API keys (without full key values)."""
  24. result = await db.execute(select(APIKey).order_by(APIKey.created_at.desc()))
  25. return list(result.scalars().all())
  26. @router.post("/", response_model=APIKeyCreateResponse)
  27. async def create_api_key(
  28. data: APIKeyCreate,
  29. db: AsyncSession = Depends(get_db),
  30. current_user: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_CREATE),
  31. ):
  32. """Create a new API key.
  33. IMPORTANT: The full API key is only returned in this response.
  34. Store it securely - it cannot be retrieved again.
  35. """
  36. # Reject can_access_cloud on auth-disabled deployments — there's no per-user
  37. # cloud_token to read against, so the flag would just silently do nothing.
  38. # Surfacing the rejection at create time prevents the user from thinking
  39. # they've configured cloud access when they actually haven't.
  40. if data.can_access_cloud and current_user is None:
  41. raise HTTPException(
  42. status_code=400,
  43. detail="can_access_cloud requires authentication to be enabled (per-user cloud tokens)",
  44. )
  45. # Generate the key
  46. full_key, key_hash, key_prefix = generate_api_key()
  47. api_key = APIKey(
  48. name=data.name,
  49. key_hash=key_hash,
  50. key_prefix=key_prefix,
  51. user_id=current_user.id if current_user else None,
  52. can_queue=data.can_queue,
  53. can_control_printer=data.can_control_printer,
  54. can_read_status=data.can_read_status,
  55. can_access_cloud=data.can_access_cloud,
  56. printer_ids=data.printer_ids,
  57. expires_at=data.expires_at,
  58. )
  59. db.add(api_key)
  60. await db.flush()
  61. await db.refresh(api_key)
  62. # Return with full key (only time it's shown)
  63. return APIKeyCreateResponse(
  64. id=api_key.id,
  65. name=api_key.name,
  66. key_prefix=api_key.key_prefix,
  67. key=full_key, # Only returned on creation
  68. user_id=api_key.user_id,
  69. can_queue=api_key.can_queue,
  70. can_control_printer=api_key.can_control_printer,
  71. can_read_status=api_key.can_read_status,
  72. can_access_cloud=api_key.can_access_cloud,
  73. printer_ids=api_key.printer_ids,
  74. enabled=api_key.enabled,
  75. last_used=api_key.last_used,
  76. created_at=api_key.created_at,
  77. expires_at=api_key.expires_at,
  78. )
  79. @router.get("/{key_id}", response_model=APIKeyResponse)
  80. async def get_api_key(
  81. key_id: int,
  82. db: AsyncSession = Depends(get_db),
  83. _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_READ),
  84. ):
  85. """Get an API key by ID."""
  86. result = await db.execute(select(APIKey).where(APIKey.id == key_id))
  87. api_key = result.scalar_one_or_none()
  88. if not api_key:
  89. raise HTTPException(status_code=404, detail="API key not found")
  90. return api_key
  91. @router.patch("/{key_id}", response_model=APIKeyResponse)
  92. async def update_api_key(
  93. key_id: int,
  94. data: APIKeyUpdate,
  95. db: AsyncSession = Depends(get_db),
  96. _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_UPDATE),
  97. ):
  98. """Update an API key."""
  99. result = await db.execute(select(APIKey).where(APIKey.id == key_id))
  100. api_key = result.scalar_one_or_none()
  101. if not api_key:
  102. raise HTTPException(status_code=404, detail="API key not found")
  103. # Update fields if provided
  104. if data.name is not None:
  105. api_key.name = data.name
  106. if data.can_queue is not None:
  107. api_key.can_queue = data.can_queue
  108. if data.can_control_printer is not None:
  109. api_key.can_control_printer = data.can_control_printer
  110. if data.can_read_status is not None:
  111. api_key.can_read_status = data.can_read_status
  112. if data.can_access_cloud is not None:
  113. # Same constraint as create — flipping cloud access on a legacy key
  114. # without an owner would be silently broken; reject at the route layer.
  115. if data.can_access_cloud and api_key.user_id is None:
  116. raise HTTPException(
  117. status_code=400,
  118. detail="can_access_cloud requires the API key to have an owner; recreate the key after upgrading",
  119. )
  120. api_key.can_access_cloud = data.can_access_cloud
  121. if data.printer_ids is not None:
  122. api_key.printer_ids = data.printer_ids
  123. if data.enabled is not None:
  124. api_key.enabled = data.enabled
  125. if data.expires_at is not None:
  126. api_key.expires_at = data.expires_at
  127. await db.flush()
  128. await db.refresh(api_key)
  129. return api_key
  130. @router.delete("/{key_id}")
  131. async def delete_api_key(
  132. key_id: int,
  133. db: AsyncSession = Depends(get_db),
  134. _: User | None = RequirePermissionIfAuthEnabled(Permission.API_KEYS_DELETE),
  135. ):
  136. """Delete (revoke) an API key."""
  137. result = await db.execute(select(APIKey).where(APIKey.id == key_id))
  138. api_key = result.scalar_one_or_none()
  139. if not api_key:
  140. raise HTTPException(status_code=404, detail="API key not found")
  141. await db.delete(api_key)
  142. return {"message": "API key deleted"}