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"))
|
AGENT_POLL_INTERVAL_SECONDS = int(os.environ.get("TUXFLOTTE_AGENT_POLL_INTERVAL", "300"))
|
||||||
ANSIBLE_CONTENT_REPO = os.environ.get("TUXFLOTTE_ANSIBLE_CONTENT_REPO", "")
|
ANSIBLE_CONTENT_REPO = os.environ.get("TUXFLOTTE_ANSIBLE_CONTENT_REPO", "")
|
||||||
ADMIN_TOKEN = os.environ.get("TUXFLOTTE_ADMIN_TOKEN", "")
|
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"}
|
AGENT_REPORTABLE_EVENT_TYPES = {"applied", "apply_failed", "removed", "remove_failed"}
|
||||||
app = FastAPI(title="Provisioning Activation Server", version="0.1.0")
|
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
|
for row in rows
|
||||||
]
|
]
|
||||||
|
|
||||||
def require_admin_token(authorization: str | None) -> bool:
|
def require_service_token(authorization: str | None) -> bool:
|
||||||
if not ADMIN_TOKEN:
|
"""
|
||||||
return False
|
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 "):
|
if authorization is None or not authorization.startswith("Bearer "):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
provided_token = authorization.removeprefix("Bearer ")
|
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:
|
def hash_agent_secret(secret: str) -> str:
|
||||||
return hashlib.sha256(secret.encode("utf-8")).hexdigest()
|
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
|
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")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
return {
|
return {
|
||||||
@ -848,12 +889,27 @@ def agent_report(
|
|||||||
return {"success": True}
|
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")
|
@app.get("/api/v1/devices/{device_id}/auftragskatalog")
|
||||||
def get_device_auftragskatalog(
|
def get_device_auftragskatalog(
|
||||||
device_id: str,
|
device_id: str,
|
||||||
authorization: str | None = Header(default=None),
|
authorization: str | None = Header(default=None),
|
||||||
):
|
):
|
||||||
if not require_admin_token(authorization):
|
if not require_service_token(authorization):
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "unauthorized",
|
"error": "unauthorized",
|
||||||
@ -879,7 +935,7 @@ def select_auftrag(
|
|||||||
payload: AuftragSelectRequest,
|
payload: AuftragSelectRequest,
|
||||||
authorization: str | None = Header(default=None),
|
authorization: str | None = Header(default=None),
|
||||||
):
|
):
|
||||||
if not require_admin_token(authorization):
|
if not require_service_token(authorization):
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "unauthorized",
|
"error": "unauthorized",
|
||||||
@ -902,7 +958,7 @@ def deselect_auftrag(
|
|||||||
merkmal_key: str,
|
merkmal_key: str,
|
||||||
authorization: str | None = Header(default=None),
|
authorization: str | None = Header(default=None),
|
||||||
):
|
):
|
||||||
if not require_admin_token(authorization):
|
if not require_service_token(authorization):
|
||||||
return {
|
return {
|
||||||
"success": False,
|
"success": False,
|
||||||
"error": "unauthorized",
|
"error": "unauthorized",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user