feat: add Authentik access widgets
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
"""Authentik directory + messaging router.
|
||||
"""Read-only Authentik directory, access metadata, and messaging router.
|
||||
|
||||
Resolves an ``authentik`` service instance from the registry, builds an
|
||||
:class:`AuthentikClient` from its config + decrypted ``api_token`` secret, and
|
||||
proxies paginated directory queries plus message-compose (email enqueue).
|
||||
Graceful "not configured" / "unreachable" payloads (matching the monitoring
|
||||
router's pattern) so the UI always renders.
|
||||
Directory data is service-scoped and fails gracefully so the service page can
|
||||
render a useful empty/error state when Authentik is unavailable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -12,7 +9,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from pydantic import BaseModel
|
||||
|
||||
from media_library_viewer_api.clients.authentik import AuthentikClient
|
||||
@@ -30,7 +27,7 @@ router = APIRouter(prefix="/api/services/authentik", tags=["authentik"])
|
||||
|
||||
|
||||
class MessageRequest(BaseModel):
|
||||
"""Compose-request body for the Authentik messaging endpoint."""
|
||||
"""Compose-request body for the existing Authentik messaging endpoint."""
|
||||
|
||||
recipient_emails: list[str]
|
||||
subject: str
|
||||
@@ -38,39 +35,99 @@ class MessageRequest(BaseModel):
|
||||
|
||||
|
||||
def _build_client(service: ServiceRecord) -> AuthentikClient:
|
||||
base_url = str(service.config.get("base_url") or "").rstrip("/")
|
||||
api_token = str(service.secrets.get("api_token") or "")
|
||||
try:
|
||||
timeout = float(service.config.get("timeout_seconds") or 10)
|
||||
except (TypeError, ValueError):
|
||||
timeout = 10.0
|
||||
return AuthentikClient(base_url=base_url, api_token=api_token, timeout=timeout)
|
||||
return AuthentikClient(
|
||||
base_url=str(service.config.get("base_url") or "").rstrip("/"),
|
||||
api_token=str(service.secrets.get("api_token") or ""),
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
|
||||
def _empty(error: str) -> dict[str, Any]:
|
||||
def _empty_directory(error: str) -> dict[str, Any]:
|
||||
return {"items": [], "total": 0, "page": 1, "page_size": 50, "error": error}
|
||||
|
||||
|
||||
def _empty_collection(error: str) -> dict[str, Any]:
|
||||
return {"items": [], "total": 0, "error": error}
|
||||
|
||||
|
||||
def _service_or_error(store: SettingsStore, service_id: str) -> ServiceRecord | None:
|
||||
return resolve_service_record(store, "authentik", service_id)
|
||||
|
||||
|
||||
@router.get("/{service_id}/users")
|
||||
def get_authentik_users(
|
||||
service_id: str,
|
||||
search: str | None = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=50, ge=1, le=200),
|
||||
store: SettingsStore = Depends(get_settings_store),
|
||||
) -> dict[str, Any]:
|
||||
"""Paginated Authentik user directory for a specific service instance."""
|
||||
service = resolve_service_record(store, "authentik", service_id)
|
||||
"""Paginated raw directory users for the existing messaging surface."""
|
||||
service = _service_or_error(store, service_id)
|
||||
if service is None:
|
||||
logger.info("Authentik users requested but no enabled authentik service for id=%s", service_id)
|
||||
return _empty("Authentik service not configured")
|
||||
|
||||
return _empty_directory("Authentik service not configured")
|
||||
try:
|
||||
client = _build_client(service)
|
||||
return client.users(search=search, page=page, page_size=page_size)
|
||||
return _build_client(service).users(search=search, page=page, page_size=page_size)
|
||||
except Exception:
|
||||
logger.exception("Authentik users query failed for service %s", service_id)
|
||||
return _empty("Authentik is unreachable")
|
||||
return _empty_directory("Authentik is unreachable")
|
||||
|
||||
|
||||
@router.get("/{service_id}/access-summary")
|
||||
def get_authentik_access_summary(
|
||||
service_id: str,
|
||||
search: str | None = None,
|
||||
page: int = Query(default=1, ge=1),
|
||||
page_size: int = Query(default=50, ge=1, le=200),
|
||||
store: SettingsStore = Depends(get_settings_store),
|
||||
) -> dict[str, Any]:
|
||||
"""User groups plus explicit staff/superuser flags, not effective permissions."""
|
||||
service = _service_or_error(store, service_id)
|
||||
if service is None:
|
||||
return _empty_directory("Authentik service not configured")
|
||||
try:
|
||||
return _build_client(service).access_summaries(search=search, page=page, page_size=page_size)
|
||||
except Exception:
|
||||
logger.exception("Authentik access summary query failed for service %s", service_id)
|
||||
return _empty_directory("Authentik is unreachable")
|
||||
|
||||
|
||||
@router.get("/{service_id}/groups")
|
||||
def get_authentik_groups(
|
||||
service_id: str,
|
||||
limit: int = Query(default=100, ge=1, le=200),
|
||||
store: SettingsStore = Depends(get_settings_store),
|
||||
) -> dict[str, Any]:
|
||||
"""Display-safe, service-scoped Authentik group list."""
|
||||
service = _service_or_error(store, service_id)
|
||||
if service is None:
|
||||
return _empty_collection("Authentik service not configured")
|
||||
try:
|
||||
return _build_client(service).groups(limit=limit)
|
||||
except Exception:
|
||||
logger.exception("Authentik groups query failed for service %s", service_id)
|
||||
return _empty_collection("Authentik is unreachable")
|
||||
|
||||
|
||||
@router.get("/{service_id}/applications")
|
||||
def get_authentik_applications(
|
||||
service_id: str,
|
||||
limit: int = Query(default=100, ge=1, le=200),
|
||||
store: SettingsStore = Depends(get_settings_store),
|
||||
) -> dict[str, Any]:
|
||||
"""Display-safe Authentik applications without provider or policy details."""
|
||||
service = _service_or_error(store, service_id)
|
||||
if service is None:
|
||||
return _empty_collection("Authentik service not configured")
|
||||
try:
|
||||
return _build_client(service).applications(limit=limit)
|
||||
except Exception:
|
||||
logger.exception("Authentik applications query failed for service %s", service_id)
|
||||
return _empty_collection("Authentik is unreachable")
|
||||
|
||||
|
||||
@router.get("/{service_id}/message/status")
|
||||
@@ -80,8 +137,7 @@ def get_authentik_message_status(
|
||||
mail_queue: MailQueue = Depends(get_mail_queue),
|
||||
) -> dict[str, Any]:
|
||||
"""Mail-queue status snapshot for the Authentik messaging tab."""
|
||||
service = resolve_service_record(store, "authentik", service_id)
|
||||
if service is None:
|
||||
if _service_or_error(store, service_id) is None:
|
||||
return {"state": "stopped", "worker_running": False, "error": "Authentik service not configured"}
|
||||
return mail_queue.status()
|
||||
|
||||
@@ -94,20 +150,16 @@ def post_authentik_message(
|
||||
mail_queue: MailQueue = Depends(get_mail_queue),
|
||||
) -> dict[str, Any]:
|
||||
"""Enqueue an email to Authentik-sourced recipients via the mail queue."""
|
||||
service = resolve_service_record(store, "authentik", service_id)
|
||||
if service is None:
|
||||
if _service_or_error(store, service_id) is None:
|
||||
return {"status": "error", "error": "Authentik service not configured"}
|
||||
|
||||
recipients = [r.strip() for r in body.recipient_emails if r.strip()]
|
||||
recipients = [recipient.strip() for recipient in body.recipient_emails if recipient.strip()]
|
||||
if not recipients:
|
||||
return {"status": "error", "error": "No recipients with valid email addresses."}
|
||||
|
||||
settings = get_settings()
|
||||
try:
|
||||
validate_smtp_settings(settings)
|
||||
except ValueError as exc:
|
||||
return {"status": "error", "error": f"SMTP settings invalid: {exc}"}
|
||||
|
||||
request_id = mail_queue.enqueue(
|
||||
settings=settings,
|
||||
recipients=recipients,
|
||||
@@ -115,8 +167,4 @@ def post_authentik_message(
|
||||
html_body=body.html_body,
|
||||
)
|
||||
logger.info("Authentik message enqueued for service %s (%d recipients)", service_id, len(recipients))
|
||||
return {
|
||||
"status": "queued",
|
||||
"request_id": request_id,
|
||||
"recipient_count": len(recipients),
|
||||
}
|
||||
return {"status": "queued", "request_id": request_id, "recipient_count": len(recipients)}
|
||||
|
||||
Reference in New Issue
Block a user