feat: Enrollment Sessions + Neugerät-Bestätigung (Kundenplattform-Admin)
Endpoints für den Admin-Bereich "Verwaltung -> Flows": Enrollment
Sessions anlegen/auflisten/widerrufen, unbestätigte Geräte je
Organisation auflisten (Geräte ohne Bereitstellungsvorlagen-Zuweisung),
Bereitstellungsvorlagen einer Organisation abrufen (bisher nur intern
in fetch_templates() für /activate genutzt, jetzt auch als eigener
Endpoint). Die Bestätigung selbst nutzt den bereits bestehenden
/api/v1/templates/{id}/resolve-Endpoint - kein neuer Mechanismus nötig.
Deckt den serverseitigen Teil aus ADR-0008 ab. Der Installer-seitige
Wartemechanismus (Auto-Modus-Gate pollt auf Freigabe) ist bewusst nicht
Teil dieser Änderung - separates Folgeprojekt in tuxflotte-installer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
34f9c9b2ef
commit
1d9f51008e
147
app.py
147
app.py
@ -92,6 +92,12 @@ class CreateWorkspaceRequest(BaseModel):
|
|||||||
organization_id: str | None = None
|
organization_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class CreateEnrollmentSessionRequest(BaseModel):
|
||||||
|
bereitstellungsvorlage_id: str
|
||||||
|
max_devices: int
|
||||||
|
expires_at: str
|
||||||
|
|
||||||
|
|
||||||
def write_log(entry: dict) -> None:
|
def write_log(entry: dict) -> None:
|
||||||
with LOG_PATH.open("a", encoding="utf-8") as f:
|
with LOG_PATH.open("a", encoding="utf-8") as f:
|
||||||
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
||||||
@ -933,6 +939,94 @@ def fetch_blueprints():
|
|||||||
for merkmal_key, backend_key, ansible_role in cur.fetchall()
|
for merkmal_key, backend_key, ansible_role in cur.fetchall()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def fetch_unassigned_devices(organization_id: str):
|
||||||
|
"""
|
||||||
|
Geräte einer Organisation ohne Bereitstellungsvorlagen-Zuweisung - die
|
||||||
|
Kandidaten für die Neugerät-Bestätigung (ADR-0008) im Flows-Bereich.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with get_database_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT d.id, d.hostname, d.device_fingerprint, d.created_at
|
||||||
|
FROM devices d
|
||||||
|
LEFT JOIN assignments a ON a.device_id = d.id
|
||||||
|
WHERE d.organization_id = %s AND a.id IS NULL
|
||||||
|
ORDER BY d.created_at
|
||||||
|
""",
|
||||||
|
(organization_id,),
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": str(device_id),
|
||||||
|
"hostname": hostname,
|
||||||
|
"device_fingerprint": fingerprint,
|
||||||
|
"created_at": created_at.isoformat(),
|
||||||
|
}
|
||||||
|
for device_id, hostname, fingerprint, created_at in cur.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
def fetch_enrollment_sessions(organization_id: str):
|
||||||
|
with get_database_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT es.id, bv.label, es.max_devices, es.devices_used,
|
||||||
|
es.expires_at, es.revoked_at, es.created_at
|
||||||
|
FROM enrollment_sessions es
|
||||||
|
JOIN bereitstellungsvorlagen bv ON bv.id = es.bereitstellungsvorlage_id
|
||||||
|
WHERE es.organization_id = %s
|
||||||
|
ORDER BY es.created_at DESC
|
||||||
|
""",
|
||||||
|
(organization_id,),
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": str(sid),
|
||||||
|
"bereitstellungsvorlage_label": label,
|
||||||
|
"max_devices": max_devices,
|
||||||
|
"devices_used": devices_used,
|
||||||
|
"expires_at": expires_at.isoformat(),
|
||||||
|
"revoked_at": revoked_at.isoformat() if revoked_at else None,
|
||||||
|
"created_at": created_at.isoformat(),
|
||||||
|
}
|
||||||
|
for (
|
||||||
|
sid, label, max_devices, devices_used,
|
||||||
|
expires_at, revoked_at, created_at,
|
||||||
|
) in cur.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
def create_enrollment_session(organization_id: str, bereitstellungsvorlage_id: str, max_devices: int, expires_at: str):
|
||||||
|
session_id = uuid4()
|
||||||
|
|
||||||
|
with get_database_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO enrollment_sessions
|
||||||
|
(id, organization_id, bereitstellungsvorlage_id, max_devices, expires_at)
|
||||||
|
VALUES (%s, %s, %s, %s, %s)
|
||||||
|
""",
|
||||||
|
(session_id, organization_id, bereitstellungsvorlage_id, max_devices, expires_at),
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"id": str(session_id)}
|
||||||
|
|
||||||
|
def revoke_enrollment_session(session_id: str):
|
||||||
|
with get_database_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE enrollment_sessions
|
||||||
|
SET revoked_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = %s AND revoked_at IS NULL
|
||||||
|
RETURNING id
|
||||||
|
""",
|
||||||
|
(session_id,),
|
||||||
|
)
|
||||||
|
return cur.fetchone() is not None
|
||||||
|
|
||||||
@app.get("/health")
|
@app.get("/health")
|
||||||
def health():
|
def health():
|
||||||
return {
|
return {
|
||||||
@ -1427,6 +1521,59 @@ def list_blueprints(authorization: str | None = Header(default=None)):
|
|||||||
return {"success": True, "blueprints": fetch_blueprints()}
|
return {"success": True, "blueprints": fetch_blueprints()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/organizations/{organization_id}/bereitstellungsvorlagen")
|
||||||
|
def list_bereitstellungsvorlagen(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, "bereitstellungsvorlagen": fetch_templates(organization_id)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/organizations/{organization_id}/devices/unassigned")
|
||||||
|
def list_unassigned_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_unassigned_devices(organization_id)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/v1/organizations/{organization_id}/enrollment-sessions")
|
||||||
|
def list_enrollment_sessions(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, "enrollment_sessions": fetch_enrollment_sessions(organization_id)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/organizations/{organization_id}/enrollment-sessions")
|
||||||
|
def create_enrollment_session_endpoint(
|
||||||
|
organization_id: str,
|
||||||
|
payload: CreateEnrollmentSessionRequest,
|
||||||
|
authorization: str | None = Header(default=None),
|
||||||
|
):
|
||||||
|
if not require_service_token(authorization):
|
||||||
|
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||||
|
|
||||||
|
session = create_enrollment_session(
|
||||||
|
organization_id, payload.bereitstellungsvorlage_id, payload.max_devices, payload.expires_at
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"success": True, "enrollment_session": session}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/v1/enrollment-sessions/{session_id}/revoke")
|
||||||
|
def revoke_enrollment_session_endpoint(session_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."}
|
||||||
|
|
||||||
|
revoked = revoke_enrollment_session(session_id)
|
||||||
|
|
||||||
|
if not revoked:
|
||||||
|
return {"success": False, "error": "not_found", "message": "Enrollment Session wurde nicht gefunden oder ist bereits widerrufen."}
|
||||||
|
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
|
|
||||||
@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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user