cc4c3fcecb
- 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
53 lines
1.7 KiB
Python
53 lines
1.7 KiB
Python
"""Satellite model - remote agent at a customer site.
|
|
|
|
The API key is stored as a SHA-256 hash; the plaintext key is shown
|
|
exactly once at creation time.
|
|
"""
|
|
|
|
import hashlib
|
|
import secrets
|
|
from datetime import UTC, datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, ForeignKey, String
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from app.core.database import Base
|
|
|
|
|
|
def generate_api_key() -> str:
|
|
"""Generate a new satellite API key (plaintext, show once)."""
|
|
return f"ius_{secrets.token_urlsafe(32)}"
|
|
|
|
|
|
def hash_api_key(key: str) -> str:
|
|
return hashlib.sha256(key.encode()).hexdigest()
|
|
|
|
|
|
class Satellite(Base):
|
|
__tablename__ = "satellites"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
customer_id: Mapped[int] = mapped_column(
|
|
ForeignKey("customers.id"), index=True
|
|
)
|
|
customer: Mapped["Customer"] = relationship(back_populates="satellites") # noqa: F821
|
|
|
|
name: Mapped[str] = mapped_column(String(255))
|
|
api_key_hash: Mapped[str] = mapped_column(String(64), unique=True, index=True)
|
|
api_key_prefix: Mapped[str] = mapped_column(String(12)) # for display in UI
|
|
|
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
|
|
# Filled by heartbeat
|
|
last_seen_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
|
version: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
|
hostname: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
|
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=lambda: datetime.now(UTC)
|
|
)
|
|
|
|
jobs: Mapped[list["UpdateJob"]] = relationship( # noqa: F821
|
|
back_populates="satellite"
|
|
)
|