diff --git a/app.py b/app.py index b63c574..19b2d80 100644 --- a/app.py +++ b/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",