Files
bambuddy/backend/app/schemas/api_key.py
T
maziggy ec51394196 fix(security): GHSA-r2qv-8222-hqg3 — allowlist API-key permissions (CVSS 9.9)
API-key permission gates went from a 17-entry admin denylist with the three
  documented scope flags (can_read_status / can_queue / can_control_printer)
  enforced only inside /api/v1/webhook/* to an explicit per-Permission
  allowlist consulted by every dependency:

    - core/auth.py: _APIKEY_SCOPE_BY_PERMISSION maps every non-admin
      Permission to one scope flag on APIKey; unmapped = 403.
      _check_apikey_permissions now takes the api_key and checks the flag.
    - require_any_permission_if_auth_enabled + require_ownership_permission
      were returning None for any valid key with zero scope check; both now
      invoke _check_apikey_permissions and fail closed.
    - Two new scope flags on api_keys: can_manage_library (LIBRARY_UPLOAD /
      UPDATE_OWN / DELETE_OWN / MAKERWORLD_IMPORT) and can_manage_inventory
      (INVENTORY_CREATE / UPDATE / DELETE / FORECAST_WRITE — required by
      SpoolBuddy kiosks). Default TRUE, backfilled from can_queue so existing
      "queue-only" keys keep working and hardened "read-only" keys do not
      silently gain writes.
    - CLOUD_AUTH now routed through can_access_cloud for defence-in-depth
      alongside the existing _cloud_api_key_gate.
    - Migration column-existence check (_api_keys_column_exists) gates the
      backfill so user-edited values are never overwritten on restart.

  Structural drift backstop: test_every_permission_has_a_classification fails
  CI on any new Permission added without an explicit scope mapping —
  prevents the denylist-shape regression that grew the prior surface.

  Backend 5469 tests green; ruff clean. Frontend build green; i18n parity
  green across 9 locales (5005 leaves each, +6 new keys). Wiki permissions
  table + allowlist callout + upgrade notes updated.
2026-06-02 08:28:24 +02:00

65 lines
2.1 KiB
Python

from datetime import datetime
from pydantic import BaseModel
class APIKeyCreate(BaseModel):
"""Schema for creating a new API key."""
name: str
can_queue: bool = True
can_control_printer: bool = False
can_read_status: bool = True
can_manage_library: bool = True # Upload / rename / delete own library files + MakerWorld import
can_manage_inventory: bool = True # Inventory writes — SpoolBuddy NFC/scale/system, manual stock edits via API
can_access_cloud: bool = False # Read /cloud/* on the creator's behalf — default off (#1182)
can_update_energy_cost: bool = False # POST /settings/electricity-price only (#1356)
printer_ids: list[int] | None = None # null = all printers
expires_at: datetime | None = None
class APIKeyUpdate(BaseModel):
"""Schema for updating an API key."""
name: str | None = None
can_queue: bool | None = None
can_control_printer: bool | None = None
can_read_status: bool | None = None
can_manage_library: bool | None = None
can_manage_inventory: bool | None = None
can_access_cloud: bool | None = None
can_update_energy_cost: bool | None = None
printer_ids: list[int] | None = None
enabled: bool | None = None
expires_at: datetime | None = None
class APIKeyResponse(BaseModel):
"""Schema for API key response (without full key)."""
id: int
name: str
key_prefix: str # First 8 chars for identification
user_id: int | None # Owner — NULL on legacy keys created before per-user ownership (#1182)
can_queue: bool
can_control_printer: bool
can_read_status: bool
can_manage_library: bool
can_manage_inventory: bool
can_access_cloud: bool
can_update_energy_cost: bool
printer_ids: list[int] | None
enabled: bool
last_used: datetime | None
created_at: datetime
expires_at: datetime | None
class Config:
from_attributes = True
class APIKeyCreateResponse(APIKeyResponse):
"""Response when creating a key - includes full key (shown only once)."""
key: str # Full API key, only shown on creation