fix(workflows): F12 (Astra P1) — approve/reject an zentralen Approval-Vertrag anpassen
Vorher: Beide Routen behandelten die Rueckgabe von create_approval_request als Dictionary (approval["id"] -> TypeError: ApprovalRequest object is not subscriptable, Astra-Repro), riefen resolve_approval_request mit nicht existierendem decided_by statt approver_id und ohne tenant_id auf — und erzeugten bei JEDEM Aufruf eine NEUE Anfrage, die sie sofort selbst genehmigten, statt die wartende Engine-Anfrage aufzuloesen. Fix (beide Routen, approve + reject): - Suchen die BESTEHENDE pending ApprovalRequest der Engine (entity_type=workflow_instance, entity_id, status=pending, neueste zuerst) und loesen genau diese auf — keine Selbst-Genehmigung mehr - Korrekte F11-Signatur: (db, tenant_id, request_id, decision=, approver_id=, comment=, is_system_admin=) + ApprovalDecisionError- Behandlung (403/409/410) Keine wartende Anfrage -> 409 no_pending_approval (kla rer Zustand statt stiller Neubau) - advance_instance/cancel_instance laufen wie gehabt NACH erfolgreicher Aufloesung Abnahme (Astra): Beide URLs funktionieren; Zustandswechsel, Audit und Freigabe stimmen; Wiederholung erzeugt keinen zweiten Fortschritt — erfuellt (resolve wirft 409 not_pending bei Zweitentscheid). Tests: test_phase_g_workflows + test_s1_security_guards 60/60. ruff clean.
This commit is contained in:
+96
-34
@@ -464,7 +464,14 @@ async def approve_workflow_step(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: dict = Depends(require_permission("workflows:write")),
|
current_user: dict = Depends(require_permission("workflows:write")),
|
||||||
):
|
):
|
||||||
"""Approve the current approval step of a workflow instance."""
|
"""Approve the current approval step of a workflow instance.
|
||||||
|
|
||||||
|
F12 (Astra P1): resolves the EXISTING pending approval request that
|
||||||
|
the workflow engine created when the decision guard paused the flow —
|
||||||
|
instead of creating a NEW request and self-approving it (old behaviour:
|
||||||
|
TypeError on approval["id"] — create returns an ORM object, and the
|
||||||
|
call used a nonexistent ``decided_by`` kwarg).
|
||||||
|
"""
|
||||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
user_id = uuid.UUID(current_user["user_id"])
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
|
|
||||||
@@ -476,7 +483,11 @@ async def approve_workflow_step(
|
|||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.core.approval import create_approval_request, resolve_approval_request
|
from app.core.approval import (
|
||||||
|
ApprovalDecisionError,
|
||||||
|
ApprovalRequest,
|
||||||
|
resolve_approval_request,
|
||||||
|
)
|
||||||
from app.models.workflow import WorkflowInstance
|
from app.models.workflow import WorkflowInstance
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
@@ -493,22 +504,44 @@ async def approve_workflow_step(
|
|||||||
detail={"detail": "Instance not found", "code": "not_found"},
|
detail={"detail": "Instance not found", "code": "not_found"},
|
||||||
)
|
)
|
||||||
|
|
||||||
approval = await create_approval_request(
|
# F12: find the EXISTING pending approval the engine created for this
|
||||||
db=db,
|
# instance — do not create a new, instantly self-approved one.
|
||||||
tenant_id=tenant_id,
|
pending_result = await db.execute(
|
||||||
entity_type="workflow_instance",
|
select(ApprovalRequest)
|
||||||
entity_id=uuid.UUID(instance_id),
|
.where(
|
||||||
action="workflow_step_approval",
|
ApprovalRequest.tenant_id == tenant_id,
|
||||||
requested_by=user_id,
|
ApprovalRequest.entity_type == "workflow_instance",
|
||||||
requested_by_type="user",
|
ApprovalRequest.entity_id == instance.id,
|
||||||
)
|
ApprovalRequest.status == "pending",
|
||||||
await resolve_approval_request(
|
)
|
||||||
db=db,
|
.order_by(ApprovalRequest.created_at.desc())
|
||||||
request_id=approval["id"],
|
.limit(1)
|
||||||
decision="approved",
|
|
||||||
decided_by=user_id,
|
|
||||||
comment=comment,
|
|
||||||
)
|
)
|
||||||
|
pending = pending_result.scalar_one_or_none()
|
||||||
|
if pending is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail={
|
||||||
|
"detail": "No pending approval request for this workflow instance",
|
||||||
|
"code": "no_pending_approval",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await resolve_approval_request(
|
||||||
|
db,
|
||||||
|
tenant_id,
|
||||||
|
pending.id,
|
||||||
|
decision="approved",
|
||||||
|
approver_id=user_id,
|
||||||
|
comment=comment,
|
||||||
|
is_system_admin=bool(current_user.get("is_system_admin", False)),
|
||||||
|
)
|
||||||
|
except ApprovalDecisionError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=exc.http_status,
|
||||||
|
detail={"detail": str(exc), "code": exc.code},
|
||||||
|
) from exc
|
||||||
|
|
||||||
return await workflow_service.advance_instance(
|
return await workflow_service.advance_instance(
|
||||||
db,
|
db,
|
||||||
@@ -528,7 +561,11 @@ async def reject_workflow_step(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: dict = Depends(require_permission("workflows:write")),
|
current_user: dict = Depends(require_permission("workflows:write")),
|
||||||
):
|
):
|
||||||
"""Reject the current approval step of a workflow instance."""
|
"""Reject the current approval step of a workflow instance.
|
||||||
|
|
||||||
|
F12 (Astra P1): resolves the EXISTING pending approval request that
|
||||||
|
the workflow engine created — same contract fix as approve.
|
||||||
|
"""
|
||||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
user_id = uuid.UUID(current_user["user_id"])
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
|
|
||||||
@@ -540,7 +577,11 @@ async def reject_workflow_step(
|
|||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from app.core.approval import create_approval_request, resolve_approval_request
|
from app.core.approval import (
|
||||||
|
ApprovalDecisionError,
|
||||||
|
ApprovalRequest,
|
||||||
|
resolve_approval_request,
|
||||||
|
)
|
||||||
from app.models.workflow import WorkflowInstance
|
from app.models.workflow import WorkflowInstance
|
||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
@@ -557,22 +598,43 @@ async def reject_workflow_step(
|
|||||||
detail={"detail": "Instance not found", "code": "not_found"},
|
detail={"detail": "Instance not found", "code": "not_found"},
|
||||||
)
|
)
|
||||||
|
|
||||||
approval = await create_approval_request(
|
# F12: resolve the EXISTING pending approval for this instance.
|
||||||
db=db,
|
pending_result = await db.execute(
|
||||||
tenant_id=tenant_id,
|
select(ApprovalRequest)
|
||||||
entity_type="workflow_instance",
|
.where(
|
||||||
entity_id=uuid.UUID(instance_id),
|
ApprovalRequest.tenant_id == tenant_id,
|
||||||
action="workflow_step_approval",
|
ApprovalRequest.entity_type == "workflow_instance",
|
||||||
requested_by=user_id,
|
ApprovalRequest.entity_id == instance.id,
|
||||||
requested_by_type="user",
|
ApprovalRequest.status == "pending",
|
||||||
)
|
)
|
||||||
await resolve_approval_request(
|
.order_by(ApprovalRequest.created_at.desc())
|
||||||
db=db,
|
.limit(1)
|
||||||
request_id=approval["id"],
|
|
||||||
decision="rejected",
|
|
||||||
decided_by=user_id,
|
|
||||||
comment=comment,
|
|
||||||
)
|
)
|
||||||
|
pending = pending_result.scalar_one_or_none()
|
||||||
|
if pending is None:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_409_CONFLICT,
|
||||||
|
detail={
|
||||||
|
"detail": "No pending approval request for this workflow instance",
|
||||||
|
"code": "no_pending_approval",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await resolve_approval_request(
|
||||||
|
db,
|
||||||
|
tenant_id,
|
||||||
|
pending.id,
|
||||||
|
decision="rejected",
|
||||||
|
approver_id=user_id,
|
||||||
|
comment=comment,
|
||||||
|
is_system_admin=bool(current_user.get("is_system_admin", False)),
|
||||||
|
)
|
||||||
|
except ApprovalDecisionError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=exc.http_status,
|
||||||
|
detail={"detail": str(exc), "code": exc.code},
|
||||||
|
) from exc
|
||||||
|
|
||||||
return await workflow_service.cancel_instance(
|
return await workflow_service.cancel_instance(
|
||||||
db,
|
db,
|
||||||
|
|||||||
Reference in New Issue
Block a user