171 lines
6.5 KiB
Python
171 lines
6.5 KiB
Python
"""Read-only Authentik directory, access metadata, and messaging router.
|
|
|
|
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
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from pydantic import BaseModel
|
|
|
|
from media_library_viewer_api.clients.authentik import AuthentikClient
|
|
from media_library_viewer_api.config import get_settings
|
|
from media_library_viewer_api.dependencies import get_mail_queue, get_settings_store
|
|
from media_library_viewer_api.services.mail_queue import MailQueue
|
|
from media_library_viewer_api.services.mailer import validate_smtp_settings
|
|
from media_library_viewer_api.services.service_resolution import resolve_service_record
|
|
from media_library_viewer_api.services.settings_store import SettingsStore
|
|
from media_library_viewer_api.widgets.sources import ServiceRecord
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/api/services/authentik", tags=["authentik"])
|
|
|
|
|
|
class MessageRequest(BaseModel):
|
|
"""Compose-request body for the existing Authentik messaging endpoint."""
|
|
|
|
recipient_emails: list[str]
|
|
subject: str
|
|
html_body: str
|
|
|
|
|
|
def _build_client(service: ServiceRecord) -> AuthentikClient:
|
|
try:
|
|
timeout = float(service.config.get("timeout_seconds") or 10)
|
|
except (TypeError, ValueError):
|
|
timeout = 10.0
|
|
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_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 = 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 raw directory users for the existing messaging surface."""
|
|
service = _service_or_error(store, service_id)
|
|
if service is None:
|
|
return _empty_directory("Authentik service not configured")
|
|
try:
|
|
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_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")
|
|
def get_authentik_message_status(
|
|
service_id: str,
|
|
store: SettingsStore = Depends(get_settings_store),
|
|
mail_queue: MailQueue = Depends(get_mail_queue),
|
|
) -> dict[str, Any]:
|
|
"""Mail-queue status snapshot for the Authentik messaging tab."""
|
|
if _service_or_error(store, service_id) is None:
|
|
return {"state": "stopped", "worker_running": False, "error": "Authentik service not configured"}
|
|
return mail_queue.status()
|
|
|
|
|
|
@router.post("/{service_id}/message")
|
|
def post_authentik_message(
|
|
service_id: str,
|
|
body: MessageRequest,
|
|
store: SettingsStore = Depends(get_settings_store),
|
|
mail_queue: MailQueue = Depends(get_mail_queue),
|
|
) -> dict[str, Any]:
|
|
"""Enqueue an email to Authentik-sourced recipients via the mail queue."""
|
|
if _service_or_error(store, service_id) is None:
|
|
return {"status": "error", "error": "Authentik service not configured"}
|
|
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,
|
|
subject=body.subject,
|
|
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)}
|