feat(app): Secrets management page + backend-aware API
- new Secrets page (/secrets, nav entry): lists vault secrets, reveal/copy, add/edit/delete, shows active backend + addr + role; read-only on followers - GET /secrets/info (backend/addr/role/writable); fix /secrets CRUD to use the castle.yaml-selected backend (was defaulting to the now-empty file backend) - OpenBaoBackend.list_names drops folder entries (node-prefix groups) - tests: conftest forces file backend so host castle.yaml can't leak into tests app builds clean (tsc 0); endpoints verified live against the vault.
This commit is contained in:
@@ -5,14 +5,17 @@ from __future__ import annotations
|
||||
from fastapi import APIRouter, HTTPException, status
|
||||
from pydantic import BaseModel
|
||||
|
||||
from castle_core.config import SECRETS_DIR
|
||||
from castle_core.secret_backends import build_backend
|
||||
from castle_core.config import SECRETS_DIR, _secrets_settings
|
||||
from castle_core.secret_backends import OpenBaoBackend, build_backend
|
||||
|
||||
from castle_api.config import get_registry
|
||||
|
||||
router = APIRouter(prefix="/secrets", tags=["secrets"])
|
||||
|
||||
|
||||
def _backend():
|
||||
return build_backend(SECRETS_DIR)
|
||||
# Same selection as the rest of castle (castle.yaml `secrets:` block).
|
||||
return build_backend(SECRETS_DIR, _secrets_settings())
|
||||
|
||||
|
||||
class SecretValue(BaseModel):
|
||||
@@ -25,6 +28,23 @@ def list_secrets() -> list[str]:
|
||||
return _backend().list_names()
|
||||
|
||||
|
||||
@router.get("/info")
|
||||
def secrets_info() -> dict:
|
||||
"""The active backend + whether this node may write (for the UI)."""
|
||||
settings = _secrets_settings()
|
||||
backend = _backend()
|
||||
kind = "openbao" if isinstance(backend, OpenBaoBackend) else "file"
|
||||
role = get_registry().node.role
|
||||
# File is always writable; a vault follower holds a read-only token.
|
||||
writable = kind == "file" or role == "authority"
|
||||
return {
|
||||
"backend": kind,
|
||||
"addr": settings.get("addr") if kind == "openbao" else None,
|
||||
"role": role,
|
||||
"writable": writable,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{name}")
|
||||
def get_secret(name: str) -> dict:
|
||||
"""Get a secret value."""
|
||||
|
||||
Reference in New Issue
Block a user