Files
wild-pc/castle-api/src/castle_api/secrets.py

65 lines
1.7 KiB
Python

"""Secrets management — read and write ~/.castle/secrets/."""
from __future__ import annotations
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel
from castle_core.config import SECRETS_DIR
router = APIRouter(prefix="/secrets", tags=["secrets"])
class SecretValue(BaseModel):
value: str
@router.get("")
def list_secrets() -> list[str]:
"""List all secret names (not values)."""
if not SECRETS_DIR.exists():
return []
return sorted(f.name for f in SECRETS_DIR.iterdir() if f.is_file())
@router.get("/{name}")
def get_secret(name: str) -> dict:
"""Get a secret value."""
_validate_name(name)
path = SECRETS_DIR / name
if not path.exists():
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Secret '{name}' not found",
)
return {"name": name, "value": path.read_text().strip()}
@router.put("/{name}")
def set_secret(name: str, body: SecretValue) -> dict:
"""Set a secret value."""
_validate_name(name)
SECRETS_DIR.mkdir(parents=True, exist_ok=True)
path = SECRETS_DIR / name
path.write_text(body.value.strip() + "\n")
return {"name": name, "ok": True}
@router.delete("/{name}")
def delete_secret(name: str) -> dict:
"""Delete a secret."""
_validate_name(name)
path = SECRETS_DIR / name
if path.exists():
path.unlink()
return {"name": name, "ok": True}
def _validate_name(name: str) -> None:
"""Reject path traversal attempts."""
if "/" in name or "\\" in name or ".." in name or not name:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid secret name",
)