Files
B0rbor4d cc4c3fcecb Hub-and-Spoke Umbau: Multi-Tenant Zentrale + Satellite-Agent
- Backend: Customer/Satellite Models, customer_id auf Server/Job/Audit
- Satellite-API: heartbeat, poll (atomares Claiming), logs, result,
  scan-result, health-report - Auth via X-Api-Key (SHA-256 gehasht)
- Job-Queue: pending/claimed/running/success/failed + Stale-Janitor
- Batch-Trigger: ein Job pro Server, Satellite arbeitet sequenziell ab
- Credentials bleiben lokal: nur symbolische credential_ref zentral
- Neues Paket satellite/: Pull-Loop, WinRM/SSH/CAU/Scanner, PyInstaller-tauglich
- Frontend: Kunden-Switcher, Satelliten-View, Polling statt WebSocket
- Entfernt: WebSocket/Socket.io, Redis, zentrale Credentials, JobRunner
- Docs: README/AGENTS/PROMPT auf neue Architektur aktualisiert
2026-08-07 03:42:06 +00:00

112 lines
3.6 KiB
Python

"""Satellite management routes (dashboard side).
The plaintext API key is returned exactly once on creation.
"""
from fastapi import APIRouter, Depends, Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import client_ip, get_current_user
from app.core.database import get_db
from app.core.exceptions import NotFoundError
from app.models.customer import Customer
from app.models.satellite import Satellite, generate_api_key, hash_api_key
from app.models.user import User
from app.schemas.satellite import SatelliteCreate, SatelliteCreated, SatelliteRead
from app.services.audit import AuditService
router = APIRouter()
def _created_response(satellite: Satellite, api_key: str) -> SatelliteCreated:
data = SatelliteRead.model_validate(satellite).model_dump()
return SatelliteCreated(**data, api_key=api_key)
@router.get("", response_model=list[SatelliteRead])
async def list_satellites(
customer_id: int | None = None,
db: AsyncSession = Depends(get_db),
_user: User = Depends(get_current_user),
) -> list[Satellite]:
stmt = select(Satellite).order_by(Satellite.id)
if customer_id is not None:
stmt = stmt.where(Satellite.customer_id == customer_id)
result = await db.execute(stmt)
return list(result.scalars().all())
@router.post("", response_model=SatelliteCreated, status_code=201)
async def create_satellite(
payload: SatelliteCreate,
request: Request,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
) -> SatelliteCreated:
customer = await db.get(Customer, payload.customer_id)
if not customer:
raise NotFoundError("Kunde nicht gefunden")
api_key = generate_api_key()
satellite = Satellite(
customer_id=payload.customer_id,
name=payload.name,
api_key_hash=hash_api_key(api_key),
api_key_prefix=api_key[:11],
)
db.add(satellite)
await db.flush()
await AuditService(db).log(
username=user.username,
action="satellite.create",
target=f"{customer.name}/{satellite.name}",
customer_id=customer.id,
ip_address=client_ip(request),
)
return _created_response(satellite, api_key)
@router.delete("/{satellite_id}", status_code=204)
async def delete_satellite(
satellite_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
) -> None:
satellite = await db.get(Satellite, satellite_id)
if not satellite:
raise NotFoundError("Satellite nicht gefunden")
await AuditService(db).log(
username=user.username,
action="satellite.delete",
target=satellite.name,
customer_id=satellite.customer_id,
ip_address=client_ip(request),
)
await db.delete(satellite)
@router.post("/{satellite_id}/rotate-key", response_model=SatelliteCreated)
async def rotate_satellite_key(
satellite_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
) -> SatelliteCreated:
satellite = await db.get(Satellite, satellite_id)
if not satellite:
raise NotFoundError("Satellite nicht gefunden")
api_key = generate_api_key()
satellite.api_key_hash = hash_api_key(api_key)
satellite.api_key_prefix = api_key[:11]
await AuditService(db).log(
username=user.username,
action="satellite.rotate_key",
target=satellite.name,
customer_id=satellite.customer_id,
ip_address=client_ip(request),
)
return _created_response(satellite, api_key)