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
|
||||
|
||||
|
||||
class CreateEnrollmentSessionRequest(BaseModel):
|
||||
bereitstellungsvorlage_id: str
|
||||
max_devices: int
|
||||
expires_at: str
|
||||
|
||||
|
||||
def write_log(entry: dict) -> None:
|
||||
with LOG_PATH.open("a", encoding="utf-8") as f:
|
||||
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()
|
||||
]
|
||||
|
||||
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")
|
||||
def health():
|
||||
return {
|
||||
@ -1427,6 +1521,59 @@ def list_blueprints(authorization: str | None = Header(default=None)):
|
||||
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")
|
||||
def get_device_auftragskatalog(
|
||||
device_id: str,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user