Follows the portal: the noun is "instance" everywhere the app says it — UI strings, CLI output, error details, docs and comments. The wire keys (`instance_id`, `instance_token`) and the hub route this calls move with it. An existing cloud.json is adopted rather than refused: without the key alias the dataclass fails to parse, which the caller swallows and reads as "never enrolled" instead of "reconnect". `instance_key` on a node type becomes `target_key`. It means the outside thing a node points at, which is a different sense of the word, and keeping both would put two meanings of "instance" in one codebase.
58 lines
1.8 KiB
Python
58 lines
1.8 KiB
Python
"""Credentials for node integrations.
|
|
|
|
Values go in and are never handed back out — the API only ever lists names.
|
|
"""
|
|
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from fastapi.concurrency import run_in_threadpool
|
|
from pydantic import BaseModel
|
|
|
|
from fluksio.api.deps import get_current_active_superuser
|
|
from fluksio.flow.secrets import SecretNotFound, get_secrets
|
|
from fluksio.models import Message
|
|
|
|
# Superuser, not merely signed in. The names alone say what this instance
|
|
# talks to, and `PUT /{name}` takes any name — so an ordinary account could
|
|
# overwrite the credential a flow authenticates with. `/search` already gates
|
|
# secret names this way and said so; this router was the half that did not.
|
|
router = APIRouter(
|
|
prefix="/secrets",
|
|
tags=["secrets"],
|
|
dependencies=[Depends(get_current_active_superuser)],
|
|
)
|
|
|
|
|
|
class SecretNames(BaseModel):
|
|
data: list[str]
|
|
count: int
|
|
|
|
|
|
class SecretValue(BaseModel):
|
|
value: str
|
|
|
|
|
|
@router.get("/", response_model=SecretNames)
|
|
def read_secrets() -> Any:
|
|
"""List the names of stored secrets."""
|
|
names = get_secrets().list()
|
|
return SecretNames(data=names, count=len(names))
|
|
|
|
|
|
@router.put("/{name}", response_model=Message)
|
|
async def save_secret(name: str, body: SecretValue) -> Any:
|
|
"""Store a secret under a name that nodes can reference."""
|
|
await run_in_threadpool(get_secrets().set, name, body.value)
|
|
return Message(message=f"Saved secret '{name}'")
|
|
|
|
|
|
@router.delete("/{name}", response_model=Message)
|
|
async def delete_secret(name: str) -> Any:
|
|
"""Delete a secret."""
|
|
try:
|
|
await run_in_threadpool(get_secrets().delete, name)
|
|
except SecretNotFound:
|
|
raise HTTPException(status_code=404, detail=f"No secret named '{name}'")
|
|
return Message(message=f"Deleted secret '{name}'")
|