Alerts: configuration, test and search entries are a superuser's; subscribing a browser stays open
Any signed-in account could save an ntfy channel with its own `server` and a
`{"$secret": ...}` token and press Test, which sent the stored secret to that
host. The config GET/PUT, the test route and the global search's alert entries
now answer to superusers, like /secrets. The webpush key and subscribe routes
stay open to every account, and the Alerts page shows a non-superuser only the
button for its own browser. The events table in the docs gains run_finished.
This commit is contained in:
@@ -7,15 +7,27 @@ from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
from pydantic import BaseModel
|
||||
|
||||
from fluksio.api.deps import FlowControllerDep, get_current_user
|
||||
from fluksio.api.deps import (
|
||||
FlowControllerDep,
|
||||
get_current_active_superuser,
|
||||
get_current_user,
|
||||
)
|
||||
from fluksio.core.config import settings
|
||||
from fluksio.flow import webpush
|
||||
from fluksio.flow.alerts import Alert, AlertsConfig
|
||||
from fluksio.models import Message
|
||||
|
||||
# Reading or changing where alerts go is a superuser's: a channel setting may
|
||||
# hold a `{"$secret": ...}` reference that `send` resolves, and the url or
|
||||
# server beside it is the sender's to choose, so an ordinary account could
|
||||
# point a stored credential at a host of its own and press Test. The topic of
|
||||
# an ntfy channel and a webhook's url are credentials too, so reading is gated
|
||||
# as well. Subscribing a browser stays open to every account: it only says
|
||||
# where *this* device can be reached.
|
||||
router = APIRouter(
|
||||
prefix="/alerts", tags=["alerts"], dependencies=[Depends(get_current_user)]
|
||||
)
|
||||
SUPERUSER = [Depends(get_current_active_superuser)]
|
||||
|
||||
|
||||
def _path() -> Path:
|
||||
@@ -41,13 +53,13 @@ def write_config(config: AlertsConfig) -> None:
|
||||
path.write_text(config.model_dump_json(indent=2))
|
||||
|
||||
|
||||
@router.get("/config", response_model=AlertsConfig)
|
||||
@router.get("/config", response_model=AlertsConfig, dependencies=SUPERUSER)
|
||||
async def read_alerts_config() -> Any:
|
||||
"""What the engine alerts on, and where it sends it."""
|
||||
return await run_in_threadpool(read_config)
|
||||
|
||||
|
||||
@router.put("/config", response_model=AlertsConfig)
|
||||
@router.put("/config", response_model=AlertsConfig, dependencies=SUPERUSER)
|
||||
async def save_alerts_config(body: AlertsConfig, controller: FlowControllerDep) -> Any:
|
||||
"""Replace the configuration. Takes effect immediately."""
|
||||
known = {channel.name for channel in body.channels}
|
||||
@@ -95,7 +107,7 @@ async def remove_webpush_subscription(body: Unsubscribe) -> Any:
|
||||
return Message(message="This browser will no longer be notified")
|
||||
|
||||
|
||||
@router.post("/test/{channel_name}", response_model=Message)
|
||||
@router.post("/test/{channel_name}", response_model=Message, dependencies=SUPERUSER)
|
||||
async def test_channel(channel_name: str, controller: FlowControllerDep) -> Any:
|
||||
"""Send one alert, to prove the channel works before relying on it."""
|
||||
if controller.alerts is None:
|
||||
|
||||
@@ -252,10 +252,12 @@ def _build(
|
||||
for worker in (hub.workers() if hub is not None else [])
|
||||
)
|
||||
|
||||
entries.extend(
|
||||
SearchEntry(category="alert", name=channel.name, kind=channel.kind)
|
||||
for channel in read_alerts_config().channels
|
||||
)
|
||||
# Named only to a superuser, like the secrets: `/alerts` answers to them.
|
||||
if secrets:
|
||||
entries.extend(
|
||||
SearchEntry(category="alert", name=channel.name, kind=channel.kind)
|
||||
for channel in read_alerts_config().channels
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
@@ -273,7 +275,8 @@ async def read_search_index(
|
||||
instance of any ordinary size, so one fetch when the panel opens beats a
|
||||
round trip per keystroke — and the client already has a matcher.
|
||||
|
||||
Secrets are named only to a superuser, which is who ``/secrets`` answers to.
|
||||
Secrets and alert channels are named only to a superuser, which is who
|
||||
``/secrets`` and ``/alerts`` answer to.
|
||||
"""
|
||||
# Absent when remote workers are switched off — not a reason to fail a search.
|
||||
hub = getattr(request.app.state, "worker_hub", None)
|
||||
|
||||
Reference in New Issue
Block a user