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
This commit is contained in:
B0rbor4d
2026-08-07 03:42:06 +00:00
parent b91dd66fee
commit cc4c3fcecb
72 changed files with 2759 additions and 1642 deletions
+111
View File
@@ -0,0 +1,111 @@
"""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)