Add an admin page for managing collector API keys
Admins (Microsoft 365 sign-in names in BACKUP_DASH_ADMINS) get an Admin link to /admin.html, where they can create, list and revoke the API keys collectors use, instead of running manage.py on the server. Backend - /api/v1/admin/keys (list, create, revoke), admins only (403 otherwise); the user comes from the headers nginx sets after sign-in - Changes need an X-Requested-By header, which another site's page can't add, so a signed-in admin's browser can't be used to forge them - A new key is returned once; only its hash is stored. Names are unique for good, so revoked keys' history stays unambiguous - Keys record who created/revoked them and the company they last reported for (schema v2; databases migrate automatically on start) - /api/v1/me reports is_admin; manage.py shares the key code and records itself as creator/revoker Frontend - admin.html / admin.js: name validation shown after interaction (:user-invalid, aria-invalid kept in sync), the one-time key with a copy button (falls back to selecting it if clipboard access is refused), a key table flagging keys quiet for over 24 hours, and revoking confirmed in a <dialog>; results announced in a status region - Shared el() and signed-in helpers moved to dom.js / signed-in.js - Mock mode (?mock=1) includes sample keys Tested: 15 new backend tests (access, forged requests, names, duplicates, revoking, last-used company, v1 -> v2 upgrade); in the browser, every admin flow in mock mode and at phone width; end to end through the nginx config with the real API (a key created on the API works for ingest; revoking it makes ingest 401; non-admins see an explanation and no Admin link). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
"""Admin API: managing collector API keys. Only for users in BACKUP_DASH_ADMINS."""
|
||||
|
||||
import logging
|
||||
import sqlite3
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, StringConstraints
|
||||
|
||||
from . import db, keys
|
||||
from .auth import require_admin, require_page_request
|
||||
|
||||
log = logging.getLogger("backup_dashboard")
|
||||
|
||||
router = APIRouter(prefix="/api/v1/admin", dependencies=[Depends(require_admin)])
|
||||
|
||||
|
||||
class NewKey(BaseModel):
|
||||
name: Annotated[str, StringConstraints(strip_whitespace=True, pattern=f"^{keys.NAME_PATTERN}$")]
|
||||
|
||||
|
||||
@router.get("/keys")
|
||||
def list_keys(conn: sqlite3.Connection = Depends(db.get_db)):
|
||||
return {"keys": keys.list_keys(conn)}
|
||||
|
||||
|
||||
@router.post("/keys", status_code=201, dependencies=[Depends(require_page_request)])
|
||||
def create_key(body: NewKey, admin: str = Depends(require_admin), conn: sqlite3.Connection = Depends(db.get_db)):
|
||||
try:
|
||||
record, secret = keys.create_key(conn, body.name, created_by=admin)
|
||||
except keys.KeyNameTaken:
|
||||
raise HTTPException(409, f"A key named {body.name!r} already exists (revoked keys keep their names)") from None
|
||||
log.info("admin %s created API key %r", admin, body.name)
|
||||
# The key itself is returned this once; only its hash is stored
|
||||
return {"key": record, "secret": secret}
|
||||
|
||||
|
||||
@router.post("/keys/{key_id}/revoke", dependencies=[Depends(require_page_request)])
|
||||
def revoke_key(key_id: int, admin: str = Depends(require_admin), conn: sqlite3.Connection = Depends(db.get_db)):
|
||||
record = keys.revoke_key(conn, key_id, revoked_by=admin)
|
||||
if record is None:
|
||||
raise HTTPException(404, "Key not found")
|
||||
log.info("admin %s revoked API key %r", admin, record["name"])
|
||||
return {"key": record}
|
||||
Reference in New Issue
Block a user