Workspace-Klon-Endpunkt für "Diesen Standard übernehmen"

Live gefundene Lücke (11.09.2026): das neue Besitz-Modell von
routers/workspace.py bindet Webapps strikt an Eigentum - ein globaler,
schreibgeschützter Standard-Workspace hat nie eigene Webapps, und eine
Organisation ohne einen einzigen eigenen Workspace hatte dadurch gar
keinen Weg mehr zu Webapps (Regression gegenüber dem alten, aber
unsicheren Verhalten, das jede nutzende Organisation an geteilten
Workspaces mitschreiben liess und damit ein Cross-Org-Leck riskierte).

clone_workspace() kopiert die Merkmal-Zusammensetzung eines sichtbaren
Workspace in einen neuen, garantiert der aufrufenden Organisation
gehörenden Workspace - der Standard selbst bleibt für alle unangetastet.
Neuer Service-Endpunkt POST /api/v1/workspaces/{id}/clone (Service-Token
wie die übrigen Workspace-Endpunkte).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VLk93dhDMrkPXefVdknMV9
This commit is contained in:
adminandClaude Sonnet 5 committed 2026-09-11 10:36:05 +02:00
1 parent 965db2b541
commit b686d87c4b
1 file changed
+64
+64
View File
@@ -205,6 +205,10 @@ class CreateWorkspaceRequest(BaseModel):
organization_id: str | None = None
class CloneWorkspaceRequest(BaseModel):
organization_id: str
class CreateWebappRequest(BaseModel):
name: str
url: str
@@ -2598,6 +2602,51 @@ def create_workspace(key: str, name: str, description: str | None, organization_
return {"id": str(workspace_id), "key": key, "name": name}
def clone_workspace(source_workspace_id: str, organization_id: str):
"""
"Diesen Standard übernehmen" (GUI-Umbau Phase 3, Nachtrag 11.09.2026):
live gefundene Lücke - Webapps sind an Besitz gebunden (siehe
fetch_sichtbaren_workspace() in der Kundenplattform), ein global
schreibgeschützter Standard-Workspace hat also nie eigene Webapps,
und eine Organisation ohne einen einzigen eigenen Workspace hatte
dadurch gar keinen Weg mehr zu Webapps (anders als das alte, aber
unsichere Verhalten, wo jede Organisation, die einen geteilten
Workspace NUTZTE, dort auch Webapps eintragen konnte - das hätte
Webapps zwischen fremden Organisationen sichtbar gemacht, die
zufällig denselben globalen Standard verwenden).
Löst das auf, ohne den schreibgeschützten Standard selbst anzutasten:
kopiert die Merkmal-Zusammensetzung in einen neuen, der Organisation
gehörenden Workspace - der Standard bleibt für alle unverändert,
die Kopie ist von Anfang an voll editierbar (inkl. eigener Webapps).
"""
quelle = fetch_workspace_detail(source_workspace_id)
if quelle is None:
return None
with get_database_connection() as conn:
with conn.cursor() as cur:
cur.execute("SELECT key FROM workspaces WHERE organization_id = %s", (organization_id,))
vergebene_keys = {row[0] for row in cur.fetchall()}
basis_key = f"{quelle['key']}-eigene-kopie"
neuer_key = basis_key
zaehler = 2
while neuer_key in vergebene_keys:
neuer_key = f"{basis_key}-{zaehler}"
zaehler += 1
neuer_workspace = create_workspace(
neuer_key, f"{quelle['name']} (eigene Kopie)", quelle["description"], organization_id
)
for merkmal in quelle["merkmale"]:
add_merkmal_to_workspace(neuer_workspace["id"], merkmal["id"])
return neuer_workspace
def fetch_workspace_detail(workspace_id: str):
with get_database_connection() as conn:
with conn.cursor() as cur:
@@ -4177,6 +4226,21 @@ def create_workspace_endpoint(payload: CreateWorkspaceRequest, authorization: st
}
@app.post("/api/v1/workspaces/{workspace_id}/clone")
def clone_workspace_endpoint(
workspace_id: str, payload: CloneWorkspaceRequest, authorization: str | None = Header(default=None)
):
if not require_service_token(authorization):
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
neuer_workspace = clone_workspace(workspace_id, payload.organization_id)
if neuer_workspace is None:
return {"success": False, "error": "not_found", "message": "Workspace wurde nicht gefunden."}
return {"success": True, "workspace": neuer_workspace}
@app.get("/api/v1/workspaces/{workspace_id}")
def get_workspace(workspace_id: str, authorization: str | None = Header(default=None)):
if not require_service_token(authorization):