feat: Kundenplattform-Service-Token + Organizations-Devices-Endpoint
Neue Env-Var TUXFLOTTE_KUNDENPLATTFORM_TOKEN, require_admin_token zu
require_service_token verallgemeinert (akzeptiert Admin- oder
Kundenplattform-Token). Neuer GET /api/v1/organizations/{id}/devices
Endpoint für Kundenplattforms Geräteliste + Besitz-Validierung (ADR-0011).
Bestehende Auftragskatalog-Endpoints akzeptieren jetzt beide Tokens.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
dc1637de7e
commit
0967f7369e
70
app.py
70
app.py
@ -20,6 +20,7 @@ DATABASE_URL = os.environ.get("TUXFLOTTE_DATABASE_URL")
|
||||
AGENT_POLL_INTERVAL_SECONDS = int(os.environ.get("TUXFLOTTE_AGENT_POLL_INTERVAL", "300"))
|
||||
ANSIBLE_CONTENT_REPO = os.environ.get("TUXFLOTTE_ANSIBLE_CONTENT_REPO", "")
|
||||
ADMIN_TOKEN = os.environ.get("TUXFLOTTE_ADMIN_TOKEN", "")
|
||||
KUNDENPLATTFORM_TOKEN = os.environ.get("TUXFLOTTE_KUNDENPLATTFORM_TOKEN", "")
|
||||
AGENT_REPORTABLE_EVENT_TYPES = {"applied", "apply_failed", "removed", "remove_failed"}
|
||||
app = FastAPI(title="Provisioning Activation Server", version="0.1.0")
|
||||
|
||||
@ -333,16 +334,26 @@ def fetch_templates(organization_id: str) -> list[dict]:
|
||||
for row in rows
|
||||
]
|
||||
|
||||
def require_admin_token(authorization: str | None) -> bool:
|
||||
if not ADMIN_TOKEN:
|
||||
return False
|
||||
def require_service_token(authorization: str | None) -> bool:
|
||||
"""
|
||||
Akzeptiert entweder den Betreiber-Admin-Token oder das separate
|
||||
Kundenplattform-Service-Token (siehe ADR-0011) - beide dürfen die
|
||||
Auftragskatalog-Endpoints aufrufen, Kompromittierung/Rotation des einen
|
||||
betrifft den anderen nicht.
|
||||
"""
|
||||
|
||||
if authorization is None or not authorization.startswith("Bearer "):
|
||||
return False
|
||||
|
||||
provided_token = authorization.removeprefix("Bearer ")
|
||||
|
||||
return hmac.compare_digest(provided_token, ADMIN_TOKEN)
|
||||
if ADMIN_TOKEN and hmac.compare_digest(provided_token, ADMIN_TOKEN):
|
||||
return True
|
||||
|
||||
if KUNDENPLATTFORM_TOKEN and hmac.compare_digest(provided_token, KUNDENPLATTFORM_TOKEN):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def hash_agent_secret(secret: str) -> str:
|
||||
return hashlib.sha256(secret.encode("utf-8")).hexdigest()
|
||||
@ -558,6 +569,36 @@ def set_auftrag_selection(device_id: str, merkmal_key: str, aktiv: bool, optione
|
||||
|
||||
return True
|
||||
|
||||
def fetch_devices_for_organization(organization_id: str):
|
||||
"""
|
||||
Für Kundenplattforms Geräteliste + Besitz-Validierung (siehe ADR-0011):
|
||||
alle Geräte einer Organisation, unabhängig von Bereitstellungsvorlagen-
|
||||
Zuweisung.
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, hostname, device_fingerprint, agent_last_checkin
|
||||
FROM devices
|
||||
WHERE organization_id = %s
|
||||
ORDER BY hostname NULLS LAST, created_at
|
||||
""",
|
||||
(organization_id,),
|
||||
)
|
||||
return [
|
||||
{
|
||||
"id": str(device_id),
|
||||
"hostname": hostname,
|
||||
"device_fingerprint": fingerprint,
|
||||
"agent_last_checkin": (
|
||||
last_checkin.isoformat() if last_checkin is not None else None
|
||||
),
|
||||
}
|
||||
for device_id, hostname, fingerprint, last_checkin in cur.fetchall()
|
||||
]
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {
|
||||
@ -848,12 +889,27 @@ def agent_report(
|
||||
return {"success": True}
|
||||
|
||||
|
||||
@app.get("/api/v1/organizations/{organization_id}/devices")
|
||||
def get_organization_devices(
|
||||
organization_id: str,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "unauthorized",
|
||||
"message": "Fehlendes oder ungültiges Service-Token.",
|
||||
}
|
||||
|
||||
return {"success": True, "devices": fetch_devices_for_organization(organization_id)}
|
||||
|
||||
|
||||
@app.get("/api/v1/devices/{device_id}/auftragskatalog")
|
||||
def get_device_auftragskatalog(
|
||||
device_id: str,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_admin_token(authorization):
|
||||
if not require_service_token(authorization):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "unauthorized",
|
||||
@ -879,7 +935,7 @@ def select_auftrag(
|
||||
payload: AuftragSelectRequest,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_admin_token(authorization):
|
||||
if not require_service_token(authorization):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "unauthorized",
|
||||
@ -902,7 +958,7 @@ def deselect_auftrag(
|
||||
merkmal_key: str,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_admin_token(authorization):
|
||||
if not require_service_token(authorization):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "unauthorized",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user