65 lines
1.7 KiB
Python
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",
|
|
)
|