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
+20 -4
View File
@@ -1,12 +1,14 @@
"""Shared API dependencies: current user extraction from JWT."""
"""Shared API dependencies: dashboard user auth (JWT) and satellite auth (API key)."""
from fastapi import Depends, Request
from fastapi import Depends, Header, Request
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.core.exceptions import ForbiddenError, UnauthorizedError
from app.core.security import decode_token
from app.models.satellite import Satellite, hash_api_key
from app.models.user import User
bearer_scheme = HTTPBearer(auto_error=False)
@@ -23,8 +25,6 @@ async def get_current_user(
if not username:
raise UnauthorizedError("Token enthält keinen Benutzer")
from sqlalchemy import select
result = await db.execute(select(User).where(User.username == username))
user = result.scalar_one_or_none()
if not user or not user.is_active:
@@ -38,5 +38,21 @@ async def require_admin(user: User = Depends(get_current_user)) -> User:
return user
async def get_current_satellite(
x_api_key: str | None = Header(default=None),
db: AsyncSession = Depends(get_db),
) -> Satellite:
"""Authenticate a satellite by its API key (X-Api-Key header)."""
if not x_api_key:
raise UnauthorizedError("X-Api-Key header fehlt")
result = await db.execute(
select(Satellite).where(Satellite.api_key_hash == hash_api_key(x_api_key))
)
satellite = result.scalar_one_or_none()
if not satellite or not satellite.is_active:
raise UnauthorizedError("Satellite unbekannt oder deaktiviert")
return satellite
def client_ip(request: Request) -> str | None:
return request.client.host if request.client else None